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,319 @@
from __future__ import annotations
import unittest
from contextlib import asynccontextmanager
from types import ModuleType, SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import uuid4
from .services import rupture_runtime
def _window(
*,
dimension: str | None = None,
severity: str | None = "moderate",
techniques: tuple[str, ...] = (),
states: tuple[str, ...] = ("defensive",),
appropriateness: float = 1.0,
rapport: float | None = -0.4,
evaluation_error: bool = False,
safety_event_ids: tuple[int, ...] = (),
) -> rupture_runtime.DurableTurnWindow:
return rupture_runtime.DurableTurnWindow(
counselor_turn_id=uuid4(),
client_turn_id=uuid4(),
counselor_seq=1,
client_seq=2,
appropriateness_score=appropriateness,
rapport_signal=rapport,
techniques=techniques,
client_states=states,
intent_dimension=dimension,
intent_severity=severity,
evaluation_error=evaluation_error,
safety_event_ids=safety_event_ids,
)
class RuptureRuntimeDetectionTests(unittest.TestCase):
def test_sessions_install_one_shared_finalize_hook_for_voice(self) -> None:
from .routes import sessions as session_routes
from .routes import voice as voice_routes
self.assertIs(
session_routes.turn_runtime.finalize_completed_turn,
voice_routes.turn_runtime.finalize_completed_turn,
)
self.assertTrue(
getattr(
voice_routes.turn_runtime.finalize_completed_turn,
"__rupture_runtime_hook__",
False,
)
)
def test_all_nine_rupture_types_have_conservative_structured_rules(self) -> None:
cases = {
"withdrawal": _window(dimension=None),
"confrontation": _window(dimension=None, techniques=("confrontation",)),
"goal_mismatch": _window(dimension="goal"),
"task_mismatch": _window(dimension="pacing"),
"empathic_miss": _window(dimension="empathy"),
"cultural_miss": _window(dimension="cultural_context"),
"boundary_tension": _window(dimension="confidentiality"),
"premature_advice": _window(
dimension="advice", techniques=("psychoeducation",)
),
"over_disclosure": _window(
dimension="self_disclosure", techniques=("self_disclosure",)
),
}
for expected, window in cases.items():
with self.subTest(expected=expected):
result = rupture_runtime.detect_rupture(window)
self.assertIsNotNone(result)
assert result is not None
self.assertEqual(result.rupture_type, expected)
def test_insufficient_fast_evidence_fails_closed_without_normal_rupture(
self,
) -> None:
self.assertIsNone(
rupture_runtime.detect_rupture(
_window(
dimension=None,
states=(),
appropriateness=3.0,
rapport=None,
)
)
)
self.assertIsNone(
rupture_runtime.detect_rupture(_window(dimension="goal", severity="minor"))
)
self.assertIsNone(
rupture_runtime.detect_rupture(
_window(dimension="goal", evaluation_error=True)
)
)
def test_advice_and_disclosure_require_matching_technique_evidence(self) -> None:
self.assertIsNone(rupture_runtime.detect_rupture(_window(dimension="advice")))
self.assertIsNone(
rupture_runtime.detect_rupture(_window(dimension="self_disclosure"))
)
def test_safety_references_do_not_change_classifier_output(self) -> None:
without_safety = _window(dimension="goal")
with_safety = rupture_runtime.DurableTurnWindow(
counselor_turn_id=without_safety.counselor_turn_id,
client_turn_id=without_safety.client_turn_id,
counselor_seq=without_safety.counselor_seq,
client_seq=without_safety.client_seq,
appropriateness_score=without_safety.appropriateness_score,
rapport_signal=without_safety.rapport_signal,
techniques=without_safety.techniques,
client_states=without_safety.client_states,
intent_dimension=without_safety.intent_dimension,
intent_severity=without_safety.intent_severity,
safety_event_ids=(41, 42),
)
self.assertEqual(
rupture_runtime.detect_rupture(without_safety),
rupture_runtime.detect_rupture(with_safety),
)
def test_follow_up_requires_behavior_and_client_response_for_resolution(
self,
) -> None:
original = rupture_runtime.DetectionCandidate(
rupture_type="empathic_miss", confidence=0.9, uncertainty=0.1
)
resolved = rupture_runtime.assess_follow_up(
original,
_window(
dimension=None,
techniques=("empathy", "facilitative_question"),
states=("defense_loosening",),
appropriateness=5.0,
rapport=0.5,
),
)
missed = rupture_runtime.assess_follow_up(
original,
_window(
dimension=None,
techniques=("empathy",),
states=("defensive",),
appropriateness=2.0,
rapport=-0.2,
),
)
self.assertIsNotNone(resolved)
self.assertEqual(resolved.status, "resolved") # type: ignore[union-attr]
self.assertIsNotNone(missed)
self.assertEqual(missed.status, "missed") # type: ignore[union-attr]
def test_uuid5_keys_are_stable_for_reprocessing(self) -> None:
first = rupture_runtime._stable_uuid("observation", "episode", "resolved")
second = rupture_runtime._stable_uuid("observation", "episode", "resolved")
different = rupture_runtime._stable_uuid("observation", "episode", "partial")
self.assertEqual(first, second)
self.assertNotEqual(first, different)
class RuptureRuntimeAsyncTests(unittest.IsolatedAsyncioTestCase):
async def test_scan_uses_same_process_evaluator_context_and_real_provenance(
self,
) -> None:
session_id = uuid4()
window = _window(dimension="goal")
snapshot = rupture_runtime.RuntimeSnapshot(
session_id=session_id,
ended=False,
windows=(window,),
)
conn = AsyncMock()
acquired: list[dict[str, object]] = []
@asynccontextmanager
async def fake_acquire(**kwargs: object):
acquired.append(kwargs)
yield conn
observation_id = uuid4()
episode_id = uuid4()
append = AsyncMock(
return_value={
"episode_id": episode_id,
"observation_id": observation_id,
}
)
with (
patch.object(rupture_runtime.db, "acquire", fake_acquire),
patch.object(
rupture_runtime,
"_load_snapshot",
AsyncMock(return_value=snapshot),
),
patch.object(
rupture_runtime,
"_load_runtime_episodes",
AsyncMock(return_value={}),
),
patch.object(
rupture_runtime.rupture_repair_store,
"append_evaluator_observation",
append,
),
):
result = await rupture_runtime.run_session_scan(
str(session_id), trigger="turn_persisted"
)
self.assertEqual(result.status, "recorded")
self.assertEqual(
acquired,
[{"ai_view": "evaluator", "ai_context": True}],
)
insert_sql = str(conn.execute.await_args.args[0])
self.assertIn("INSERT INTO audit.model_run", insert_sql)
kwargs = append.await_args.kwargs
self.assertEqual(kwargs["source_kind"], "model_inferred")
self.assertEqual(kwargs["perspective"], "independent_observer")
self.assertIsNotNone(kwargs["model_run_id"])
self.assertEqual(kwargs["safety_event_ids"], window.safety_event_ids)
async def test_reprocessing_reuses_the_same_observation_idempotency_key(
self,
) -> None:
session_id = uuid4()
window = _window(dimension="goal")
snapshot = rupture_runtime.RuntimeSnapshot(
session_id=session_id,
ended=False,
windows=(window,),
)
conn = AsyncMock()
@asynccontextmanager
async def fake_acquire(**kwargs: object):
yield conn
append = AsyncMock(
return_value={"episode_id": uuid4(), "observation_id": uuid4()}
)
with (
patch.object(rupture_runtime.db, "acquire", fake_acquire),
patch.object(
rupture_runtime,
"_load_snapshot",
AsyncMock(return_value=snapshot),
),
patch.object(
rupture_runtime,
"_load_runtime_episodes",
AsyncMock(side_effect=[{}, {}]),
),
patch.object(
rupture_runtime.rupture_repair_store,
"append_evaluator_observation",
append,
),
):
await rupture_runtime.run_session_scan(str(session_id), trigger="one")
await rupture_runtime.run_session_scan(str(session_id), trigger="two")
first = append.await_args_list[0].kwargs["idempotency_key"]
second = append.await_args_list[1].kwargs["idempotency_key"]
self.assertEqual(first, second)
async def test_failure_is_metadata_only_and_does_not_escape(self) -> None:
session_id = str(uuid4())
with patch.object(
rupture_runtime,
"_process_session_scan",
AsyncMock(side_effect=RuntimeError("masked transcript must not leak")),
):
result = await rupture_runtime.run_session_scan(
session_id, trigger="turn_persisted"
)
self.assertEqual(result.status, "error")
self.assertEqual(result.error_code, "runtime_runtimeerror")
self.assertNotIn("masked transcript", repr(result))
self.assertEqual(rupture_runtime.last_runtime_result(session_id), result)
async def test_finalize_hook_schedules_after_original_for_any_caller(self) -> None:
module = ModuleType("fake_turn_runtime")
order: list[str] = []
async def finalize(session: object, *_args: object, **_kwargs: object) -> str:
order.append("persisted")
return "learner-turn"
module.finalize_completed_turn = finalize # type: ignore[attr-defined]
rupture_runtime.install_turn_finalize_hook(module)
installed = module.finalize_completed_turn # type: ignore[attr-defined]
with patch.object(
rupture_runtime,
"schedule_session_scan",
side_effect=lambda *_args, **_kwargs: order.append("scheduled"),
):
result = await installed(SimpleNamespace(session_id=str(uuid4())))
self.assertEqual(result, "learner-turn")
self.assertEqual(order, ["persisted", "scheduled"])
before = module.finalize_completed_turn # type: ignore[attr-defined]
rupture_runtime.install_turn_finalize_hook(module)
self.assertIs(before, module.finalize_completed_turn) # type: ignore[attr-defined]
if __name__ == "__main__":
unittest.main()