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:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
700
apps/api/app/services/rupture_repair_store.py
Normal file
700
apps/api/app/services/rupture_repair_store.py
Normal file
|
|
@ -0,0 +1,700 @@
|
|||
"""G3 rupture/repair append-only PostgreSQL store.
|
||||
|
||||
Internal evaluator writes run under an explicit evaluator AI view. Human reads and
|
||||
corrections run under RBAC/cohort RLS. Safety rows are only referenced and never
|
||||
used to derive lifecycle or reconciliation status.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import asyncpg
|
||||
|
||||
from .. import db
|
||||
from ..contracts.rupture_repair import RuptureLifecycleState, RuptureType
|
||||
from ..deps import Principal, Role
|
||||
|
||||
|
||||
class RuptureRepairNotFoundError(LookupError):
|
||||
pass
|
||||
|
||||
|
||||
class RuptureRepairStateError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class RuptureRepairConflictError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _value(row: Mapping[str, Any], key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _canonical_hash(payload: Mapping[str, Any]) -> str:
|
||||
serialized = json.dumps(
|
||||
payload,
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _human_view(principal: Principal) -> str:
|
||||
return "counselor" if principal.role == Role.LEARNER else "supervisor"
|
||||
|
||||
|
||||
def _created_role(principal: Principal) -> str:
|
||||
return "instructor" if principal.role == Role.TEACHER else principal.role.value
|
||||
|
||||
|
||||
def _ensure_unique_nonempty_evidence(evidence_turn_ids: Sequence[UUID]) -> None:
|
||||
if not evidence_turn_ids or len(set(evidence_turn_ids)) != len(evidence_turn_ids):
|
||||
raise RuptureRepairStateError(
|
||||
"evidence_turn_ids must be non-empty and unique"
|
||||
)
|
||||
|
||||
|
||||
def _ensure_visible_to(visible_to: Sequence[str], *, required_view: str) -> tuple[str, ...]:
|
||||
allowed = {"counselor", "evaluator", "supervisor", "research"}
|
||||
normalized = tuple(dict.fromkeys(item.strip() for item in visible_to if item.strip()))
|
||||
if not normalized or not set(normalized) <= allowed:
|
||||
raise RuptureRepairStateError("visible_to contains an unsupported AI view")
|
||||
if required_view not in normalized:
|
||||
raise RuptureRepairStateError(f"visible_to must include {required_view}")
|
||||
return normalized
|
||||
|
||||
|
||||
async def _visible_session(
|
||||
conn: asyncpg.Connection, session_id: UUID
|
||||
) -> Mapping[str, Any]:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id, case_id, learner_id
|
||||
FROM app.sessions
|
||||
WHERE id = $1
|
||||
""",
|
||||
session_id,
|
||||
)
|
||||
if row is None:
|
||||
raise RuptureRepairNotFoundError("session not found or not visible")
|
||||
if _value(row, "case_id") is None:
|
||||
raise RuptureRepairStateError("rupture episode requires a case_id")
|
||||
return row
|
||||
|
||||
|
||||
async def _existing_by_idempotency(
|
||||
conn: asyncpg.Connection,
|
||||
*,
|
||||
table: str,
|
||||
id_column: str,
|
||||
session_id: UUID,
|
||||
idempotency_key: UUID,
|
||||
content_hash: str,
|
||||
) -> UUID | None:
|
||||
if table not in {
|
||||
"app.rupture_observation_event",
|
||||
"app.rupture_reconciliation_revision",
|
||||
} or id_column not in {"observation_id", "revision_id"}:
|
||||
raise AssertionError("unsupported rupture idempotency lookup")
|
||||
row = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT {id_column}, content_hash
|
||||
FROM {table}
|
||||
WHERE session_id = $1 AND idempotency_key = $2
|
||||
""",
|
||||
session_id,
|
||||
idempotency_key,
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
if str(_value(row, "content_hash")) != content_hash:
|
||||
raise RuptureRepairConflictError(
|
||||
"idempotency key was already used with different rupture content"
|
||||
)
|
||||
return UUID(str(_value(row, id_column)))
|
||||
|
||||
|
||||
async def append_evaluator_observation(
|
||||
*,
|
||||
conn: asyncpg.Connection,
|
||||
session_id: UUID,
|
||||
episode_key: str,
|
||||
idempotency_key: UUID,
|
||||
event_kind: str,
|
||||
from_state: RuptureLifecycleState | None,
|
||||
to_state: RuptureLifecycleState,
|
||||
rupture_type: RuptureType,
|
||||
source_kind: str,
|
||||
perspective: str,
|
||||
ai_view: str,
|
||||
confidence: float | None,
|
||||
uncertainty: float,
|
||||
evidence_turn_ids: Sequence[UUID],
|
||||
counterevidence: Sequence[str],
|
||||
model_run_id: UUID | None,
|
||||
safety_event_ids: Sequence[int] = (),
|
||||
visible_to: Sequence[str] = (
|
||||
"counselor",
|
||||
"evaluator",
|
||||
"supervisor",
|
||||
"research",
|
||||
),
|
||||
) -> dict[str, UUID]:
|
||||
"""Append one evaluator/runtime lifecycle observation with de-duplication."""
|
||||
|
||||
if ai_view != "evaluator":
|
||||
raise RuptureRepairStateError("internal observation requires ai_view=evaluator")
|
||||
if source_kind == "model_inferred":
|
||||
if perspective != "independent_observer" or model_run_id is None:
|
||||
raise RuptureRepairStateError(
|
||||
"model observation requires independent_observer and model_run_id"
|
||||
)
|
||||
elif source_kind == "observed_runtime":
|
||||
if perspective != "runtime_observation":
|
||||
raise RuptureRepairStateError(
|
||||
"runtime observation requires runtime_observation perspective"
|
||||
)
|
||||
else:
|
||||
raise RuptureRepairStateError(
|
||||
"internal observation source must be model_inferred or observed_runtime"
|
||||
)
|
||||
_ensure_unique_nonempty_evidence(evidence_turn_ids)
|
||||
if len(set(safety_event_ids)) != len(safety_event_ids):
|
||||
raise RuptureRepairStateError("safety_event_ids must be unique")
|
||||
audiences = _ensure_visible_to(visible_to, required_view="evaluator")
|
||||
normalized_key = episode_key.strip()
|
||||
if not normalized_key:
|
||||
raise RuptureRepairStateError("episode_key must not be blank")
|
||||
|
||||
payload = {
|
||||
"session_id": str(session_id),
|
||||
"episode_key": normalized_key,
|
||||
"event_kind": event_kind,
|
||||
"from_state": from_state,
|
||||
"to_state": to_state,
|
||||
"rupture_type": rupture_type,
|
||||
"source_kind": source_kind,
|
||||
"perspective": perspective,
|
||||
"ai_view": ai_view,
|
||||
"confidence": confidence,
|
||||
"uncertainty": uncertainty,
|
||||
"evidence_turn_ids": sorted(str(item) for item in evidence_turn_ids),
|
||||
"counterevidence": list(counterevidence),
|
||||
"model_run_id": str(model_run_id) if model_run_id else None,
|
||||
"safety_event_ids": sorted(safety_event_ids),
|
||||
"visible_to": list(audiences),
|
||||
}
|
||||
content_hash = _canonical_hash(payload)
|
||||
await conn.execute(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
|
||||
f"rupture:{session_id}:{normalized_key}",
|
||||
)
|
||||
anchor = await _visible_session(conn, session_id)
|
||||
episode = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.rupture_episode (
|
||||
session_id, case_id, learner_id, episode_key, visible_to,
|
||||
created_by_role
|
||||
) VALUES ($1, $2, $3, $4, $5::text[], 'agent')
|
||||
ON CONFLICT (session_id, episode_key) DO NOTHING
|
||||
RETURNING episode_id
|
||||
""",
|
||||
session_id,
|
||||
_value(anchor, "case_id"),
|
||||
_value(anchor, "learner_id"),
|
||||
normalized_key,
|
||||
list(audiences),
|
||||
)
|
||||
if episode is None:
|
||||
episode = await conn.fetchrow(
|
||||
"""
|
||||
SELECT episode_id FROM app.rupture_episode
|
||||
WHERE session_id = $1 AND episode_key = $2
|
||||
""",
|
||||
session_id,
|
||||
normalized_key,
|
||||
)
|
||||
if episode is None:
|
||||
raise RuptureRepairConflictError("rupture episode could not be created")
|
||||
episode_id = UUID(str(_value(episode, "episode_id")))
|
||||
|
||||
existing = await _existing_by_idempotency(
|
||||
conn,
|
||||
table="app.rupture_observation_event",
|
||||
id_column="observation_id",
|
||||
session_id=session_id,
|
||||
idempotency_key=idempotency_key,
|
||||
content_hash=content_hash,
|
||||
)
|
||||
if existing is not None:
|
||||
return {"episode_id": episode_id, "observation_id": existing}
|
||||
|
||||
try:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.rupture_observation_event (
|
||||
episode_id, session_id, idempotency_key, content_hash,
|
||||
event_kind, from_state, to_state, rupture_type,
|
||||
source_kind, perspective, ai_view, confidence, uncertainty,
|
||||
evidence_turn_ids, counterevidence, model_run_id, visible_to,
|
||||
created_by_role
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,
|
||||
$14::uuid[],$15::text[],$16,$17::text[],'agent'
|
||||
)
|
||||
RETURNING observation_id
|
||||
""",
|
||||
episode_id,
|
||||
session_id,
|
||||
idempotency_key,
|
||||
content_hash,
|
||||
event_kind,
|
||||
from_state,
|
||||
to_state,
|
||||
rupture_type,
|
||||
source_kind,
|
||||
perspective,
|
||||
ai_view,
|
||||
confidence,
|
||||
uncertainty,
|
||||
list(evidence_turn_ids),
|
||||
list(counterevidence),
|
||||
model_run_id,
|
||||
list(audiences),
|
||||
)
|
||||
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
|
||||
raise RuptureRepairStateError(
|
||||
"rupture observation violated evidence/provenance/state invariants"
|
||||
) from exc
|
||||
except asyncpg.UniqueViolationError as exc:
|
||||
raise RuptureRepairConflictError(
|
||||
"rupture observation idempotency or supersession conflict"
|
||||
) from exc
|
||||
assert row is not None
|
||||
observation_id = UUID(str(_value(row, "observation_id")))
|
||||
|
||||
for safety_event_id in safety_event_ids:
|
||||
try:
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO app.rupture_safety_reference (
|
||||
episode_id, session_id, safety_event_id
|
||||
) VALUES ($1,$2,$3)
|
||||
ON CONFLICT (episode_id, safety_event_id) DO NOTHING
|
||||
""",
|
||||
episode_id,
|
||||
session_id,
|
||||
safety_event_id,
|
||||
)
|
||||
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
|
||||
raise RuptureRepairStateError(
|
||||
"safety reference must belong to the rupture session"
|
||||
) from exc
|
||||
return {"episode_id": episode_id, "observation_id": observation_id}
|
||||
|
||||
|
||||
async def append_reconciliation_revision(
|
||||
*,
|
||||
conn: asyncpg.Connection,
|
||||
session_id: UUID,
|
||||
episode_id: UUID,
|
||||
idempotency_key: UUID,
|
||||
fast_warning_observation_id: UUID,
|
||||
deep_observation_id: UUID | None,
|
||||
fast_warning_id: str,
|
||||
provisional_status: str,
|
||||
deep_status: str,
|
||||
disposition: str,
|
||||
uncertainty: float,
|
||||
evidence_turn_ids: Sequence[UUID],
|
||||
counterevidence: Sequence[str],
|
||||
model_run_id: UUID,
|
||||
ai_view: str,
|
||||
visible_to: Sequence[str] = (
|
||||
"counselor",
|
||||
"evaluator",
|
||||
"supervisor",
|
||||
"research",
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
if ai_view != "evaluator":
|
||||
raise RuptureRepairStateError("reconciliation requires ai_view=evaluator")
|
||||
audiences = _ensure_visible_to(visible_to, required_view="evaluator")
|
||||
if len(set(evidence_turn_ids)) != len(evidence_turn_ids):
|
||||
raise RuptureRepairStateError("evidence_turn_ids must be unique")
|
||||
warning = fast_warning_id.strip()
|
||||
if not warning:
|
||||
raise RuptureRepairStateError("fast_warning_id must not be blank")
|
||||
payload = {
|
||||
"session_id": str(session_id),
|
||||
"episode_id": str(episode_id),
|
||||
"fast_warning_observation_id": str(fast_warning_observation_id),
|
||||
"deep_observation_id": str(deep_observation_id) if deep_observation_id else None,
|
||||
"fast_warning_id": warning,
|
||||
"provisional_status": provisional_status,
|
||||
"deep_status": deep_status,
|
||||
"disposition": disposition,
|
||||
"uncertainty": uncertainty,
|
||||
"evidence_turn_ids": sorted(str(item) for item in evidence_turn_ids),
|
||||
"counterevidence": list(counterevidence),
|
||||
"model_run_id": str(model_run_id),
|
||||
"visible_to": list(audiences),
|
||||
}
|
||||
content_hash = _canonical_hash(payload)
|
||||
await conn.execute(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
|
||||
f"rupture-reconciliation:{episode_id}",
|
||||
)
|
||||
await _visible_session(conn, session_id)
|
||||
existing = await _existing_by_idempotency(
|
||||
conn,
|
||||
table="app.rupture_reconciliation_revision",
|
||||
id_column="revision_id",
|
||||
session_id=session_id,
|
||||
idempotency_key=idempotency_key,
|
||||
content_hash=content_hash,
|
||||
)
|
||||
if existing is not None:
|
||||
existing_row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT revision_no FROM app.rupture_reconciliation_revision
|
||||
WHERE revision_id = $1
|
||||
""",
|
||||
existing,
|
||||
)
|
||||
return {
|
||||
"episode_id": episode_id,
|
||||
"revision_id": existing,
|
||||
"revision_no": int(_value(existing_row or {}, "revision_no", 1)),
|
||||
}
|
||||
latest = await conn.fetchrow(
|
||||
"""
|
||||
SELECT revision_id, revision_no
|
||||
FROM app.rupture_reconciliation_revision
|
||||
WHERE episode_id = $1
|
||||
ORDER BY revision_no DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
episode_id,
|
||||
)
|
||||
revision_no = int(_value(latest or {}, "revision_no", 0)) + 1
|
||||
supersedes = _value(latest or {}, "revision_id")
|
||||
try:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.rupture_reconciliation_revision (
|
||||
episode_id, session_id, revision_no, idempotency_key, content_hash,
|
||||
supersedes_revision_id, fast_warning_observation_id, deep_observation_id,
|
||||
fast_warning_id, provisional_status, deep_status, disposition,
|
||||
uncertainty, evidence_turn_ids, counterevidence, model_run_id, visible_to
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,
|
||||
$14::uuid[],$15::text[],$16,$17::text[]
|
||||
)
|
||||
RETURNING revision_id, revision_no
|
||||
""",
|
||||
episode_id,
|
||||
session_id,
|
||||
revision_no,
|
||||
idempotency_key,
|
||||
content_hash,
|
||||
supersedes,
|
||||
fast_warning_observation_id,
|
||||
deep_observation_id,
|
||||
warning,
|
||||
provisional_status,
|
||||
deep_status,
|
||||
disposition,
|
||||
uncertainty,
|
||||
list(evidence_turn_ids),
|
||||
list(counterevidence),
|
||||
model_run_id,
|
||||
list(audiences),
|
||||
)
|
||||
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
|
||||
raise RuptureRepairStateError(
|
||||
"reconciliation violated evidence/provenance/direction invariants"
|
||||
) from exc
|
||||
except asyncpg.UniqueViolationError as exc:
|
||||
raise RuptureRepairConflictError(
|
||||
"reconciliation idempotency or revision conflict"
|
||||
) from exc
|
||||
assert row is not None
|
||||
return {
|
||||
"episode_id": episode_id,
|
||||
"revision_id": UUID(str(_value(row, "revision_id"))),
|
||||
"revision_no": int(_value(row, "revision_no")),
|
||||
}
|
||||
|
||||
|
||||
async def append_human_correction(
|
||||
*,
|
||||
principal: Principal,
|
||||
session_id: UUID,
|
||||
episode_id: UUID,
|
||||
idempotency_key: UUID,
|
||||
supersedes_observation_id: UUID,
|
||||
rupture_type: RuptureType,
|
||||
corrected_status: str,
|
||||
uncertainty: float,
|
||||
evidence_turn_ids: Sequence[UUID],
|
||||
counterevidence: Sequence[str],
|
||||
correction_reason: str,
|
||||
) -> UUID:
|
||||
if principal.role not in {Role.TEACHER, Role.ADMIN}:
|
||||
raise RuptureRepairStateError(
|
||||
"rupture correction requires teacher or admin role"
|
||||
)
|
||||
_ensure_unique_nonempty_evidence(evidence_turn_ids)
|
||||
reason = correction_reason.strip()
|
||||
if not reason:
|
||||
raise RuptureRepairStateError("correction_reason must not be blank")
|
||||
if corrected_status not in {"missed", "partial", "resolved"}:
|
||||
raise RuptureRepairStateError("human correction requires a terminal status")
|
||||
|
||||
payload = {
|
||||
"session_id": str(session_id),
|
||||
"episode_id": str(episode_id),
|
||||
"supersedes_observation_id": str(supersedes_observation_id),
|
||||
"rupture_type": rupture_type,
|
||||
"corrected_status": corrected_status,
|
||||
"uncertainty": uncertainty,
|
||||
"evidence_turn_ids": sorted(str(item) for item in evidence_turn_ids),
|
||||
"counterevidence": list(counterevidence),
|
||||
"correction_reason": reason,
|
||||
}
|
||||
content_hash = _canonical_hash(payload)
|
||||
async with db.acquire(
|
||||
role=principal.role.value,
|
||||
user_id=principal.user_id,
|
||||
cohort_ids=principal.cohort_ids,
|
||||
) as conn:
|
||||
await conn.execute(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))",
|
||||
f"rupture-correction:{episode_id}",
|
||||
)
|
||||
await _visible_session(conn, session_id)
|
||||
target = await conn.fetchrow(
|
||||
"""
|
||||
SELECT o.to_state
|
||||
FROM app.rupture_observation_event o
|
||||
WHERE o.observation_id = $1
|
||||
AND o.episode_id = $2
|
||||
AND o.session_id = $3
|
||||
""",
|
||||
supersedes_observation_id,
|
||||
episode_id,
|
||||
session_id,
|
||||
)
|
||||
if target is None:
|
||||
raise RuptureRepairNotFoundError(
|
||||
"superseded rupture observation not found or not visible"
|
||||
)
|
||||
existing = await _existing_by_idempotency(
|
||||
conn,
|
||||
table="app.rupture_observation_event",
|
||||
id_column="observation_id",
|
||||
session_id=session_id,
|
||||
idempotency_key=idempotency_key,
|
||||
content_hash=content_hash,
|
||||
)
|
||||
if existing is not None:
|
||||
return existing
|
||||
try:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
INSERT INTO app.rupture_observation_event (
|
||||
episode_id, session_id, idempotency_key, content_hash,
|
||||
event_kind, from_state, to_state, rupture_type,
|
||||
source_kind, perspective, ai_view, uncertainty,
|
||||
evidence_turn_ids, counterevidence, supersedes_observation_id,
|
||||
correction_reason, visible_to, created_by_uid, created_by_role
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,'human.corrected',$5,$6,$7,
|
||||
'human_rated','supervisor_human','supervisor',$8,
|
||||
$9::uuid[],$10::text[],$11,$12,
|
||||
ARRAY['counselor','evaluator','supervisor','research']::text[],$13,$14
|
||||
)
|
||||
RETURNING observation_id
|
||||
""",
|
||||
episode_id,
|
||||
session_id,
|
||||
idempotency_key,
|
||||
content_hash,
|
||||
_value(target, "to_state"),
|
||||
corrected_status,
|
||||
rupture_type,
|
||||
uncertainty,
|
||||
list(evidence_turn_ids),
|
||||
list(counterevidence),
|
||||
supersedes_observation_id,
|
||||
reason,
|
||||
UUID(principal.user_id),
|
||||
_created_role(principal),
|
||||
)
|
||||
except (asyncpg.CheckViolationError, asyncpg.ForeignKeyViolationError) as exc:
|
||||
raise RuptureRepairStateError(
|
||||
"human correction violated evidence or one-way resolution invariants"
|
||||
) from exc
|
||||
except asyncpg.UniqueViolationError as exc:
|
||||
raise RuptureRepairConflictError(
|
||||
"human correction idempotency or supersession conflict"
|
||||
) from exc
|
||||
assert row is not None
|
||||
return UUID(str(_value(row, "observation_id")))
|
||||
|
||||
|
||||
async def read_rupture_repairs(
|
||||
*, principal: Principal, session_id: UUID
|
||||
) -> dict[str, Any]:
|
||||
"""Return a role-safe, non-aggregated rupture/repair read model."""
|
||||
|
||||
view = _human_view(principal)
|
||||
async with db.acquire(
|
||||
role=principal.role.value,
|
||||
user_id=principal.user_id,
|
||||
cohort_ids=principal.cohort_ids,
|
||||
) as conn:
|
||||
await _visible_session(conn, session_id)
|
||||
episodes = list(
|
||||
await conn.fetch(
|
||||
"""
|
||||
SELECT episode_id, session_id, case_id, learner_id, episode_key, created_at
|
||||
FROM app.rupture_episode
|
||||
WHERE session_id = $1 AND $2 = ANY(visible_to)
|
||||
ORDER BY created_at, episode_id
|
||||
""",
|
||||
session_id,
|
||||
view,
|
||||
)
|
||||
)
|
||||
episode_ids = [UUID(str(_value(item, "episode_id"))) for item in episodes]
|
||||
if not episode_ids:
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"requested_view": view,
|
||||
"clinical_claim_allowed": False,
|
||||
"episodes": [],
|
||||
}
|
||||
observations = list(
|
||||
await conn.fetch(
|
||||
"""
|
||||
SELECT observation_id, episode_id, sequence_no, event_kind, from_state, to_state,
|
||||
rupture_type, source_kind, perspective, ai_view, confidence,
|
||||
uncertainty, evidence_turn_ids, counterevidence, model_run_id,
|
||||
supersedes_observation_id, correction_reason, created_at
|
||||
FROM app.rupture_observation_event
|
||||
WHERE episode_id = ANY($1::uuid[]) AND $2 = ANY(visible_to)
|
||||
ORDER BY episode_id, sequence_no
|
||||
""",
|
||||
episode_ids,
|
||||
view,
|
||||
)
|
||||
)
|
||||
reconciliations = list(
|
||||
await conn.fetch(
|
||||
"""
|
||||
SELECT revision_id, episode_id, revision_no, supersedes_revision_id,
|
||||
fast_warning_observation_id, deep_observation_id, fast_warning_id,
|
||||
provisional_status, deep_status, disposition, uncertainty,
|
||||
evidence_turn_ids, counterevidence, model_run_id, created_at
|
||||
FROM app.rupture_reconciliation_revision
|
||||
WHERE episode_id = ANY($1::uuid[]) AND $2 = ANY(visible_to)
|
||||
ORDER BY episode_id, revision_no
|
||||
""",
|
||||
episode_ids,
|
||||
view,
|
||||
)
|
||||
)
|
||||
safety = list(
|
||||
await conn.fetch(
|
||||
"""
|
||||
SELECT rs.episode_id, rs.safety_event_id, se.turn_id,
|
||||
se.ko_risk_level, se.escalated, se.created_at
|
||||
FROM app.rupture_safety_reference rs
|
||||
JOIN app.safety_events se ON se.id = rs.safety_event_id
|
||||
WHERE rs.episode_id = ANY($1::uuid[])
|
||||
ORDER BY rs.episode_id, rs.safety_event_id
|
||||
""",
|
||||
episode_ids,
|
||||
)
|
||||
)
|
||||
|
||||
observations_by_episode: dict[UUID, list[dict[str, Any]]] = {}
|
||||
for row in observations:
|
||||
episode_id = UUID(str(_value(row, "episode_id")))
|
||||
observations_by_episode.setdefault(episode_id, []).append(dict(row))
|
||||
reconciliation_by_episode: dict[UUID, list[dict[str, Any]]] = {}
|
||||
for row in reconciliations:
|
||||
episode_id = UUID(str(_value(row, "episode_id")))
|
||||
reconciliation_by_episode.setdefault(episode_id, []).append(dict(row))
|
||||
safety_by_episode: dict[UUID, list[dict[str, Any]]] = {}
|
||||
for row in safety:
|
||||
episode_id = UUID(str(_value(row, "episode_id")))
|
||||
safety_by_episode.setdefault(episode_id, []).append(dict(row))
|
||||
|
||||
result = []
|
||||
for episode in episodes:
|
||||
episode_id = UUID(str(_value(episode, "episode_id")))
|
||||
event_rows = observations_by_episode.get(episode_id, [])
|
||||
revision_rows = reconciliation_by_episode.get(episode_id, [])
|
||||
latest_event = event_rows[-1] if event_rows else None
|
||||
latest_revision = revision_rows[-1] if revision_rows else None
|
||||
latest_human_correction = next(
|
||||
(
|
||||
item
|
||||
for item in reversed(event_rows)
|
||||
if _value(item, "event_kind") == "human.corrected"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if latest_human_correction is not None:
|
||||
current_status = _value(latest_human_correction, "to_state")
|
||||
status_source = "human_correction"
|
||||
elif latest_revision is not None:
|
||||
current_status = _value(latest_revision, "deep_status")
|
||||
status_source = "deep_reconciliation"
|
||||
else:
|
||||
current_status = _value(latest_event or {}, "to_state")
|
||||
status_source = "lifecycle_event"
|
||||
result.append(
|
||||
{
|
||||
**dict(episode),
|
||||
"rupture_type": _value(
|
||||
latest_human_correction or latest_event or {}, "rupture_type"
|
||||
),
|
||||
"current_status": current_status,
|
||||
"status_source": status_source,
|
||||
"observations": event_rows,
|
||||
"reconciliation_revisions": revision_rows,
|
||||
"safety_references": safety_by_episode.get(episode_id, []),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"requested_view": view,
|
||||
"clinical_claim_allowed": False,
|
||||
"episodes": result,
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"RuptureRepairConflictError",
|
||||
"RuptureRepairNotFoundError",
|
||||
"RuptureRepairStateError",
|
||||
"append_evaluator_observation",
|
||||
"append_human_correction",
|
||||
"append_reconciliation_revision",
|
||||
"read_rupture_repairs",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue