"""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", ]