G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -0,0 +1,272 @@
from __future__ import annotations
import unittest
from typing import Any
from unittest.mock import AsyncMock, patch
from uuid import UUID, uuid4
from .routes import calibration_transfer, sessions
from .services import evaluator, session_learning_producer, state_machine
from .services.persona import P1
from .store import InProcSession, TurnRecord
SESSION_ID = UUID("00000000-0000-0000-0000-00000000a401")
LEARNER_ID = UUID("00000000-0000-0000-0000-00000000a402")
COUNSELOR_TURN_ID = UUID("00000000-0000-0000-0000-00000000a403")
CLIENT_TURN_ID = UUID("00000000-0000-0000-0000-00000000a404")
class FakeProducerConnection:
def __init__(self, *, locked_history: bool = False) -> None:
self.locked_history = locked_history
self.executed: list[tuple[str, tuple[Any, ...]]] = []
self.evaluation = {
"status": "ready",
"scope": "session_end",
"learner_id": LEARNER_ID,
"payload": {
"loop": "deep",
"scope": "session_end",
"intent_deviations": [
{
"dimension": "reflection",
"expected": "정서를 반영하고 이해를 확인한다",
"actual": "바로 다음 질문으로 이동했다",
"severity": "moderate",
}
],
},
}
async def fetchrow(self, query: str, *args: Any) -> dict[str, Any] | None:
if "FROM app.session_evaluation" in query:
return self.evaluation
if "FROM app.competency_graph_snapshot" in query:
return None
raise AssertionError(f"unexpected fetchrow: {query}")
async def fetch(self, query: str, *args: Any) -> list[dict[str, Any]]:
if "FROM app.turns t" in query:
return [
{
"turn_id": COUNSELOR_TURN_ID,
"turn_seq": 3,
"response_turn_id": CLIENT_TURN_ID,
"response_turn_seq": 4,
"intent_deviation": {
"dimension": "공감적 반영",
"expected": "정서를 반영하고 이해를 확인한다",
"actual": "바로 다음 질문으로 이동했다",
"severity": "moderate",
},
}
]
if "FROM app.calibration_prediction_history h" in query:
if not self.locked_history:
return []
return [
{
"history_id": UUID("00000000-0000-0000-0000-00000000a405"),
"competency_id": "competency.empathic_reflection",
"locked_sequence": 2,
}
]
raise AssertionError(f"unexpected fetch: {query}")
async def execute(self, query: str, *args: Any) -> str:
self.executed.append((query, args))
return "INSERT 0 1"
class SessionLearningProducerTests(unittest.IsolatedAsyncioTestCase):
async def test_g4_uses_durable_turns_and_replays_stable_submission(self) -> None:
conn = FakeProducerConnection()
append = AsyncMock(
side_effect=[
{"submission_id": uuid4(), "idempotent_replay": False},
{"submission_id": uuid4(), "idempotent_replay": True},
]
)
with patch.object(
session_learning_producer.deliberate_practice_store,
"append_prescription_submission",
append,
):
first = await session_learning_producer._produce_g4(
conn, session_id=SESSION_ID
)
second = await session_learning_producer._produce_g4(
conn, session_id=SESSION_ID
)
self.assertEqual(first["status"], "ready")
self.assertEqual(second["status"], "ready")
self.assertEqual(append.await_count, 2)
first_call = append.await_args_list[0].kwargs
second_call = append.await_args_list[1].kwargs
self.assertEqual(first_call["submission_id"], second_call["submission_id"])
self.assertEqual(first_call["coaching_cards"], second_call["coaching_cards"])
self.assertEqual(
tuple(first_call["evidence_turn_ids"]),
(COUNSELOR_TURN_ID, CLIENT_TURN_ID),
)
card = first_call["coaching_cards"][0]
self.assertEqual(
tuple(item.ref_id for item in card.evidence_refs),
(str(COUNSELOR_TURN_ID), str(CLIENT_TURN_ID)),
)
graph = first_call["graph"]
self.assertEqual(len(graph.states), 4)
self.assertTrue(all(state.band == "unassessed" for state in graph.states))
self.assertTrue(all(state.attempt_count == 0 for state in graph.states))
self.assertTrue(
all(state.unseen_transfer_demonstrations == 0 for state in graph.states)
)
async def test_g5_does_not_reveal_before_prediction_lock(self) -> None:
conn = FakeProducerConnection(locked_history=False)
append = AsyncMock()
with patch.object(
session_learning_producer.calibration_transfer_store,
"append_performance_observation",
append,
):
result = await session_learning_producer._produce_g5(
conn, session_id=SESSION_ID
)
self.assertEqual(
result, {"status": "skipped", "reason": "locked_prediction_missing"}
)
append.assert_not_awaited()
self.assertFalse(
any("INSERT INTO audit.model_run" in query for query, _ in conn.executed)
)
async def test_g5_locked_history_gets_failed_observation_with_real_provenance(
self,
) -> None:
conn = FakeProducerConnection(locked_history=True)
append = AsyncMock(
return_value={"observation_id": uuid4(), "idempotent_replay": False}
)
with patch.object(
session_learning_producer.calibration_transfer_store,
"append_performance_observation",
append,
):
result = await session_learning_producer._produce_g5(
conn, session_id=SESSION_ID
)
self.assertEqual(result["status"], "ready")
self.assertTrue(
any("INSERT INTO audit.model_run" in query for query, _ in conn.executed)
)
kwargs = append.await_args.kwargs
self.assertEqual(kwargs["status"], "failed")
self.assertEqual(kwargs["source_kind"], "model_inferred")
self.assertEqual(kwargs["perspective"], "independent_observer")
self.assertIsInstance(kwargs["model_run_id"], UUID)
self.assertEqual(
tuple(kwargs["evidence_turn_ids"]),
(COUNSELOR_TURN_ID, CLIENT_TURN_ID),
)
self.assertEqual(kwargs["revealed_sequence"], 3)
self.assertNotIn("master", repr(kwargs).lower())
self.assertNotIn("transfer_verified", repr(kwargs))
async def test_ready_evaluation_is_not_rolled_back_when_producer_fails(
self,
) -> None:
state = state_machine.init_state(params=P1.openness_params())
sess = InProcSession(
session_id=str(SESSION_ID),
case_id=str(uuid4()),
learner_id=str(LEARNER_ID),
persona_code=P1.code,
theory_mode="humanistic",
persona=P1,
state=state,
turns=[
TurnRecord(
turn_seq=1,
speaker="counselor",
stage=state.stage.value,
text="상담자 발화",
text_masked="상담자 발화",
)
],
ended=True,
)
ready = evaluator.SessionEvaluation(
session_id=str(SESSION_ID),
stage=state.stage.value,
scope="session_end",
improvements=["정서를 반영한 뒤 이해를 확인한다."],
)
save = AsyncMock(return_value=True)
notify = AsyncMock()
with (
patch.object(
sessions.evaluator, "evaluate_session", AsyncMock(return_value=ready)
),
patch.object(sessions.session_persistence, "save_session_evaluation", save),
patch.object(
sessions.session_learning_producer,
"produce_session_learning_artifacts",
AsyncMock(side_effect=RuntimeError("producer offline")),
),
patch.object(
sessions, "_enqueue_session_review_ready_notification", notify
),
):
await sessions._generate_and_save_session_evaluation(sess)
save.assert_awaited_once()
self.assertEqual(save.await_args.args[0].status, "ready")
notify.assert_awaited_once_with(str(SESSION_ID))
async def test_prediction_lock_invokes_same_session_worker(self) -> None:
history_id = uuid4()
body = calibration_transfer.PredictionLockRequest(
submission_id=uuid4(),
lock_id=uuid4(),
prediction_revision_id=uuid4(),
locked_sequence=2,
)
payload = {
"submission_id": body.submission_id,
"history_id": history_id,
"lock_id": body.lock_id,
"idempotent_replay": False,
}
principal = calibration_transfer.Principal(
user_id=str(LEARNER_ID),
role=calibration_transfer.Role.LEARNER,
cohort_ids=["g5-test"],
)
worker = AsyncMock(return_value={"g5": {"status": "ready"}})
with (
patch.object(
calibration_transfer.calibration_transfer_store,
"append_prediction_lock",
AsyncMock(return_value=payload),
),
patch.object(
calibration_transfer.session_learning_producer,
"produce_locked_prediction_history",
worker,
),
):
response = await calibration_transfer.lock_prediction_history(
history_id, body, principal
)
self.assertEqual(response.history_id, history_id)
worker.assert_awaited_once_with(history_id)
if __name__ == "__main__":
unittest.main()