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 산출물은 커밋에서 제외했다.
700 lines
25 KiB
Python
700 lines
25 KiB
Python
"""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",
|
|
]
|