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,275 @@
"""G6 Supervision & Research OS의 역할 안전 운영·연구 계약."""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
AttentionSignalType = Literal[
"deterioration",
"unresolved_rupture",
"safety_boundary",
"persistent_overconfidence",
"growth_stagnation",
"transfer_failure",
]
SignalSeverity = Literal["high", "moderate", "low"]
SignalState = Literal["active", "monitoring", "resolved", "insufficient_evidence"]
EvidenceLedger = Literal[
"measurement_event",
"outcome_trajectory_revision",
"rupture_observation_event",
"rupture_reconciliation_revision",
"safety_event",
"calibration_assessment",
"transfer_assessment",
"practice_attempt",
]
ManifestDomain = Literal["alliance", "rupture", "transfer", "calibration"]
DriftStatus = Literal["stable", "drift_flagged", "insufficient_evidence"]
class LedgerEvidencePointer(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
ledger: EvidenceLedger
event_id: str = Field(min_length=1, max_length=180)
session_id: str | None = Field(default=None, max_length=180)
route_hint: str = Field(pattern=r"^/[a-zA-Z0-9_{}?&=./-]+$")
class LearnerAttentionSignal(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
signal_id: str = Field(pattern=r"^oas-g6-signal-[a-z0-9-]+$")
learner_ref: str = Field(pattern=r"^learner-[a-z0-9-]+$")
signal_type: AttentionSignalType
severity: SignalSeverity
state: SignalState
uncertainty: float = Field(ge=0.0, le=1.0)
observed_sequence: int = Field(ge=1)
evidence: tuple[LedgerEvidencePointer, ...] = ()
counterevidence: tuple[str, ...] = ()
@model_validator(mode="after")
def require_active_evidence(self) -> "LearnerAttentionSignal":
if self.state == "insufficient_evidence":
if self.evidence or self.uncertainty != 1.0:
raise ValueError(
"insufficient attention signal must remain evidence-free"
)
elif not self.evidence:
raise ValueError("classified attention signal requires ledger evidence")
if self.state == "resolved" and not self.counterevidence:
raise ValueError("resolved attention signal requires resolution evidence")
return self
class AttentionQueueReason(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
signal_id: str
signal_type: AttentionSignalType
severity: SignalSeverity
uncertainty: float = Field(ge=0.0, le=1.0)
evidence: tuple[LedgerEvidencePointer, ...] = Field(min_length=1)
class AttentionQueueItem(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
learner_ref: str
queue_position: int = Field(ge=1)
primary_signal: AttentionSignalType
oldest_active_sequence: int = Field(ge=1)
reasons: tuple[AttentionQueueReason, ...] = Field(min_length=1)
drilldown_routes: tuple[str, ...] = Field(min_length=1)
@model_validator(mode="after")
def enforce_direct_drilldown(self) -> "AttentionQueueItem":
if len(self.drilldown_routes) > 3:
raise ValueError(
"attention item must reach evidence within three drilldowns"
)
if len(set(self.drilldown_routes)) != len(self.drilldown_routes):
raise ValueError("attention drilldown routes must be unique")
return self
class TeacherAiDisagreement(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
disagreement_id: str = Field(pattern=r"^oas-g6-disagreement-[a-z0-9-]+$")
case_ref: str = Field(min_length=1, max_length=180)
competency_id: str = Field(pattern=r"^competency\.[a-z0-9_.-]+$")
ai_label: str = Field(min_length=1, max_length=120)
teacher_label: str = Field(min_length=1, max_length=120)
ai_model: str = Field(min_length=1, max_length=180)
prompt_version: str = Field(min_length=1, max_length=80)
instrument_id: str = Field(min_length=1, max_length=180)
instrument_version: str = Field(min_length=1, max_length=80)
ai_evidence: tuple[LedgerEvidencePointer, ...] = Field(min_length=1)
teacher_correction_evidence: tuple[LedgerEvidencePointer, ...] = Field(min_length=1)
correction_reason_code: str = Field(min_length=1, max_length=120)
@model_validator(mode="after")
def require_actual_disagreement(self) -> "TeacherAiDisagreement":
if self.ai_label == self.teacher_label:
raise ValueError("calibration disagreement requires different labels")
return self
class CalibrationDatasetRow(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
row_id: str = Field(pattern=r"^[a-f0-9]{64}$")
disagreement_id: str
case_ref: str
competency_id: str
ai_label: str
teacher_label: str
ai_model: str
prompt_version: str
instrument_id: str
instrument_version: str
evidence_event_ids: tuple[str, ...] = Field(min_length=2)
correction_reason_code: str
raw_transcript_included: Literal[False] = False
class VersionedEvaluationObservation(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
case_ref: str = Field(min_length=1, max_length=180)
competency_id: str = Field(pattern=r"^competency\.[a-z0-9_.-]+$")
synthetic_subgroup: str = Field(pattern=r"^synthetic-[a-z0-9-]+$")
gold_label: str = Field(min_length=1, max_length=120)
predicted_label: str = Field(min_length=1, max_length=120)
evidence_event_id: str = Field(min_length=1, max_length=180)
class EvaluationVersionBatch(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
batch_id: str = Field(pattern=r"^oas-g6-batch-[a-z0-9-]+$")
model: str = Field(min_length=1, max_length=180)
prompt_version: str = Field(min_length=1, max_length=80)
instrument_id: str = Field(min_length=1, max_length=180)
instrument_version: str = Field(min_length=1, max_length=80)
observations: tuple[VersionedEvaluationObservation, ...] = Field(min_length=1)
@model_validator(mode="after")
def require_unique_case_competency(self) -> "EvaluationVersionBatch":
keys = [(item.case_ref, item.competency_id) for item in self.observations]
if len(set(keys)) != len(keys):
raise ValueError("version batch case/competency keys must be unique")
return self
class SubgroupVersionMetric(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
subgroup: str
matched_count: int = Field(ge=0)
baseline_accuracy: float | None = Field(default=None, ge=0.0, le=1.0)
candidate_accuracy: float | None = Field(default=None, ge=0.0, le=1.0)
accuracy_delta: float | None = Field(default=None, ge=-1.0, le=1.0)
class EvaluationVersionDriftReport(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
baseline_batch_id: str
candidate_batch_id: str
matched_count: int = Field(ge=0)
status: DriftStatus
baseline_accuracy: float | None = Field(default=None, ge=0.0, le=1.0)
candidate_accuracy: float | None = Field(default=None, ge=0.0, le=1.0)
accuracy_delta: float | None = Field(default=None, ge=-1.0, le=1.0)
disagreement_case_refs: tuple[str, ...]
subgroup_metrics: tuple[SubgroupVersionMetric, ...]
alerts: tuple[str, ...]
@model_validator(mode="after")
def preserve_underpowered_report(self) -> "EvaluationVersionDriftReport":
if self.status == "insufficient_evidence" and any(
item is not None
for item in (
self.baseline_accuracy,
self.candidate_accuracy,
self.accuracy_delta,
)
):
raise ValueError("underpowered version comparison must remain scoreless")
return self
class Phase3EvidenceArtifact(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
domain: ManifestDomain
artifact_id: str = Field(min_length=1, max_length=180)
schema_version: str = Field(min_length=1, max_length=120)
content_sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
record_count: int = Field(ge=1)
provenance_uri: str = Field(pattern=r"^(repo|db|audit)://[a-zA-Z0-9_./:-]+$")
clinical_claim_allowed: Literal[False] = False
class Phase3OutcomeEvidenceManifest(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
schema_version: Literal["vignette.phase3-outcome-evidence-manifest.v1"]
artifacts: tuple[Phase3EvidenceArtifact, ...] = Field(min_length=4)
@model_validator(mode="after")
def require_complete_unique_domains(self) -> "Phase3OutcomeEvidenceManifest":
domains = [item.domain for item in self.artifacts]
if set(domains) != {"alliance", "rupture", "transfer", "calibration"}:
raise ValueError(
"Phase 3 outcome manifest requires all four evidence domains"
)
if len(domains) != len(set(domains)):
raise ValueError("Phase 3 outcome manifest domains must be unique")
return self
class SupervisionResearchBenchmarkPack(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, protected_namespaces=())
schema_version: Literal["vignette.supervision-research-benchmark.v1"]
version: Literal["1.0.0"]
data_classification: Literal["synthetic_educational"]
clinical_claim_allowed: Literal[False]
attention_signals: tuple[LearnerAttentionSignal, ...] = Field(min_length=6)
expected_queue_order: tuple[str, ...] = Field(min_length=1)
disagreements: tuple[TeacherAiDisagreement, ...] = Field(min_length=1)
baseline_batch: EvaluationVersionBatch
candidate_batch: EvaluationVersionBatch
expected_drift_status: DriftStatus
phase3_artifacts: tuple[Phase3EvidenceArtifact, ...] = Field(min_length=4)
__all__ = [
"AttentionQueueItem",
"AttentionQueueReason",
"AttentionSignalType",
"CalibrationDatasetRow",
"DriftStatus",
"EvaluationVersionBatch",
"EvaluationVersionDriftReport",
"EvidenceLedger",
"LedgerEvidencePointer",
"LearnerAttentionSignal",
"ManifestDomain",
"Phase3EvidenceArtifact",
"Phase3OutcomeEvidenceManifest",
"SignalSeverity",
"SignalState",
"SubgroupVersionMetric",
"SupervisionResearchBenchmarkPack",
"TeacherAiDisagreement",
"VersionedEvaluationObservation",
]