vignette/apps/api/app/services/supervision_research_version_evaluator.py
Yun Chan 16e791e044 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 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

388 lines
14 KiB
Python

"""Repo-approved G6 synthetic evaluator version comparison producer.
The repository benchmark owns gold labels and the baseline/candidate outputs.
Runtime code may bind those immutable observations to approved synthetic
measurement anchors, but it must never invent a candidate or read transcript
content.
"""
from __future__ import annotations
import hashlib
import json
from collections import defaultdict
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Any
from uuid import UUID, uuid5
import asyncpg
from ..contracts.supervision_research import (
EvaluationVersionBatch,
LedgerEvidencePointer,
SupervisionResearchBenchmarkPack,
VersionedEvaluationObservation,
)
from . import supervision_research_store
from .supervision_research import (
compare_evaluation_versions,
load_supervision_research_benchmark,
)
BENCHMARK_PATH = (
Path(__file__).resolve().parents[1]
/ "data"
/ "supervision_research_benchmark_g6.v1.json"
)
_EVALUATOR_NAMESPACE = UUID("0ee4f677-979d-4635-9201-3aefc89ec71c")
_FORBIDDEN_SOURCE_KEYS = {"raw_transcript", "transcript", "utterance_text"}
@dataclass(frozen=True, slots=True)
class ValidatedRepositoryBenchmark:
pack: SupervisionResearchBenchmarkPack
content_sha256: str
@dataclass(frozen=True, slots=True)
class RepositoryComparisonInput:
benchmark: ValidatedRepositoryBenchmark
cohort_id: str
baseline: EvaluationVersionBatch
candidate: EvaluationVersionBatch
pointers_by_event_id: Mapping[str, LedgerEvidencePointer]
learner_ids_by_event_id: Mapping[str, UUID]
def _canonical_json(payload: Any) -> str:
return json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
def _forbidden_keys(payload: Any) -> set[str]:
if isinstance(payload, Mapping):
found = _FORBIDDEN_SOURCE_KEYS & {str(key) for key in payload}
for value in payload.values():
found.update(_forbidden_keys(value))
return found
if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes)):
found: set[str] = set()
for value in payload:
found.update(_forbidden_keys(value))
return found
return set()
@lru_cache(maxsize=4)
def _load_validated_repository_benchmark(
resolved_path: str,
) -> ValidatedRepositoryBenchmark:
path = Path(resolved_path)
raw = json.loads(path.read_text(encoding="utf-8"))
forbidden = _forbidden_keys(raw)
if forbidden:
raise ValueError(
"G6 repository benchmark contains forbidden source text fields: "
+ ",".join(sorted(forbidden))
)
pack = load_supervision_research_benchmark(path)
if pack.data_classification != "synthetic_educational":
raise ValueError("G6 repository benchmark must remain synthetic educational")
if pack.clinical_claim_allowed:
raise ValueError("G6 repository benchmark cannot allow clinical claims")
baseline_by_key = {
(item.case_ref, item.competency_id): item
for item in pack.baseline_batch.observations
}
candidate_by_key = {
(item.case_ref, item.competency_id): item
for item in pack.candidate_batch.observations
}
if set(baseline_by_key) != set(candidate_by_key):
raise ValueError("G6 candidate must use the repository baseline gold case set")
for key, baseline_item in baseline_by_key.items():
candidate_item = candidate_by_key[key]
if (
baseline_item.gold_label != candidate_item.gold_label
or baseline_item.synthetic_subgroup != candidate_item.synthetic_subgroup
):
raise ValueError(
"G6 candidate cannot replace repository gold labels or subgroups"
)
provenance = (
pack.baseline_batch.model,
pack.baseline_batch.prompt_version,
pack.baseline_batch.instrument_id,
pack.baseline_batch.instrument_version,
)
candidate_provenance = (
pack.candidate_batch.model,
pack.candidate_batch.prompt_version,
pack.candidate_batch.instrument_id,
pack.candidate_batch.instrument_version,
)
if provenance == candidate_provenance:
raise ValueError("G6 candidate must identify a distinct evaluator version")
source_ids = [
item.evidence_event_id
for batch in (pack.baseline_batch, pack.candidate_batch)
for item in batch.observations
]
if len(source_ids) != len(set(source_ids)):
raise ValueError("G6 repository benchmark evidence ids must be unique")
report = compare_evaluation_versions(pack.baseline_batch, pack.candidate_batch)
if report.status != pack.expected_drift_status:
raise ValueError("G6 repository benchmark expected drift status is stale")
return ValidatedRepositoryBenchmark(
pack=pack,
content_sha256=hashlib.sha256(_canonical_json(raw).encode("utf-8")).hexdigest(),
)
def load_validated_repository_benchmark(
path: str | Path = BENCHMARK_PATH,
) -> ValidatedRepositoryBenchmark:
return _load_validated_repository_benchmark(str(Path(path).resolve()))
def benchmark_anchor_metadata(
benchmark: ValidatedRepositoryBenchmark,
batch: EvaluationVersionBatch,
observation: VersionedEvaluationObservation,
) -> dict[str, Any]:
"""Return the exact safe metadata contract required from a runtime anchor."""
return {
"benchmark_schema_version": benchmark.pack.schema_version,
"benchmark_pack_version": benchmark.pack.version,
"benchmark_content_sha256": benchmark.content_sha256,
"data_classification": benchmark.pack.data_classification,
"clinical_claim_allowed": False,
"source_evidence_event_id": observation.evidence_event_id,
"batch_id": batch.batch_id,
"model": batch.model,
"prompt_version": batch.prompt_version,
"instrument_id": batch.instrument_id,
"instrument_version": batch.instrument_version,
"case_ref": observation.case_ref,
"competency_id": observation.competency_id,
"synthetic_subgroup": observation.synthetic_subgroup,
"gold_label": observation.gold_label,
"predicted_label": observation.predicted_label,
}
def _row_metadata(row: Mapping[str, Any]) -> Mapping[str, Any]:
metadata = row.get("metadata")
if isinstance(metadata, str):
metadata = json.loads(metadata)
if not isinstance(metadata, Mapping):
raise ValueError("G6 benchmark measurement anchor metadata is invalid")
forbidden = _forbidden_keys(metadata)
if forbidden:
raise ValueError("G6 benchmark anchor contains forbidden source text fields")
return metadata
def _bind_batch(
batch: EvaluationVersionBatch,
rows_by_source_id: Mapping[str, Mapping[str, Any]],
) -> EvaluationVersionBatch:
payload = batch.model_dump(mode="json")
for observation in payload["observations"]:
source_id = str(observation["evidence_event_id"])
observation["evidence_event_id"] = str(
rows_by_source_id[source_id]["measurement_id"]
)
return EvaluationVersionBatch.model_validate(payload)
def build_repository_comparison_input(
rows: Sequence[Mapping[str, Any]],
*,
benchmark: ValidatedRepositoryBenchmark | None = None,
) -> RepositoryComparisonInput | None:
benchmark = benchmark or load_validated_repository_benchmark()
expected: dict[
str, tuple[EvaluationVersionBatch, VersionedEvaluationObservation]
] = {}
for batch in (benchmark.pack.baseline_batch, benchmark.pack.candidate_batch):
for observation in batch.observations:
expected[observation.evidence_event_id] = (batch, observation)
by_cohort: dict[str, dict[str, Mapping[str, Any]]] = defaultdict(dict)
for row in rows:
metadata = _row_metadata(row)
source_id = str(metadata.get("source_evidence_event_id") or "")
if source_id not in expected:
raise ValueError("G6 benchmark anchor references an unapproved source id")
cohort_id = str(row.get("cohort_id") or "")
if not cohort_id:
raise ValueError("G6 benchmark anchor cohort is missing")
if source_id in by_cohort[cohort_id]:
raise ValueError("G6 benchmark anchor source id is duplicated")
batch, observation = expected[source_id]
required = benchmark_anchor_metadata(benchmark, batch, observation)
if any(metadata.get(key) != value for key, value in required.items()):
raise ValueError(
"G6 benchmark anchor provenance differs from repository gold"
)
by_cohort[cohort_id][source_id] = row
complete = [
(cohort_id, mapped)
for cohort_id, mapped in by_cohort.items()
if set(mapped) == set(expected)
]
if not complete:
return None
if len(complete) > 1:
raise ValueError(
"G6 repository benchmark has multiple complete runtime cohorts"
)
cohort_id, rows_by_source_id = complete[0]
baseline = _bind_batch(benchmark.pack.baseline_batch, rows_by_source_id)
candidate = _bind_batch(benchmark.pack.candidate_batch, rows_by_source_id)
pointers: dict[str, LedgerEvidencePointer] = {}
learner_ids: dict[str, UUID] = {}
for row in rows_by_source_id.values():
event_id = str(row["measurement_id"])
pointers[event_id] = LedgerEvidencePointer(
ledger="measurement_event",
event_id=event_id,
session_id=str(row["session_id"]),
route_hint=(
f"/research/benchmarks/supervision-research/{benchmark.pack.version}"
),
)
learner_ids[event_id] = UUID(str(row["learner_id"]))
return RepositoryComparisonInput(
benchmark=benchmark,
cohort_id=cohort_id,
baseline=baseline,
candidate=candidate,
pointers_by_event_id=pointers,
learner_ids_by_event_id=learner_ids,
)
async def _load_repository_benchmark_anchors(
conn: asyncpg.Connection,
benchmark: ValidatedRepositoryBenchmark,
) -> Sequence[Mapping[str, Any]]:
return await conn.fetch(
"""
SELECT m.measurement_id, m.session_id, s.learner_id, u.cohort AS cohort_id,
m.metadata
FROM app.measurement_event m
JOIN app.sessions s ON s.id = m.session_id
JOIN app.app_user u ON u.user_id = s.learner_id
WHERE m.status = 'ready'
AND m.source_kind = 'observed_runtime'
AND m.perspective = 'runtime_observation'
AND m.metadata->>'benchmark_schema_version' = $1
AND m.metadata->>'benchmark_pack_version' = $2
AND m.metadata->>'benchmark_content_sha256' = $3
AND m.metadata->>'data_classification' = 'synthetic_educational'
AND m.metadata->>'clinical_claim_allowed' = 'false'
ORDER BY u.cohort, m.measurement_id
""",
benchmark.pack.schema_version,
benchmark.pack.version,
benchmark.content_sha256,
)
def _stable_ids(comparison: RepositoryComparisonInput) -> dict[str, UUID]:
key = f"{comparison.benchmark.content_sha256}:{comparison.cohort_id}"
return {
name: uuid5(_EVALUATOR_NAMESPACE, f"{name}:{key}")
for name in (
"comparison-submission",
"drift-report",
"baseline-submission",
"baseline-batch",
"candidate-submission",
"candidate-batch",
)
}
async def produce_repository_version_comparison(
conn: asyncpg.Connection,
*,
benchmark_path: str | Path = BENCHMARK_PATH,
) -> dict[str, Any]:
benchmark = load_validated_repository_benchmark(benchmark_path)
comparison = build_repository_comparison_input(
await _load_repository_benchmark_anchors(conn, benchmark),
benchmark=benchmark,
)
if comparison is None:
return {
"status": "skipped",
"reason": "repo_approved_synthetic_evidence_incomplete",
"benchmark_schema_version": benchmark.pack.schema_version,
"benchmark_version": benchmark.pack.version,
"benchmark_content_sha256": benchmark.content_sha256,
"data_classification": benchmark.pack.data_classification,
"raw_transcript_included": False,
"clinical_claim_allowed": False,
}
ids = _stable_ids(comparison)
result = await supervision_research_store.append_evaluation_comparison(
conn,
submission_id=ids["comparison-submission"],
drift_report_id=ids["drift-report"],
baseline_submission_id=ids["baseline-submission"],
baseline_batch_record_id=ids["baseline-batch"],
candidate_submission_id=ids["candidate-submission"],
candidate_batch_record_id=ids["candidate-batch"],
cohort_id=comparison.cohort_id,
baseline=comparison.baseline,
candidate=comparison.candidate,
pointers_by_event_id=comparison.pointers_by_event_id,
learner_ids_by_event_id=comparison.learner_ids_by_event_id,
)
return {
**result,
"benchmark_schema_version": benchmark.pack.schema_version,
"benchmark_version": benchmark.pack.version,
"benchmark_content_sha256": benchmark.content_sha256,
"data_classification": benchmark.pack.data_classification,
"baseline_provenance": {
"model": comparison.baseline.model,
"prompt_version": comparison.baseline.prompt_version,
"instrument_id": comparison.baseline.instrument_id,
"instrument_version": comparison.baseline.instrument_version,
},
"candidate_provenance": {
"model": comparison.candidate.model,
"prompt_version": comparison.candidate.prompt_version,
"instrument_id": comparison.candidate.instrument_id,
"instrument_version": comparison.candidate.instrument_version,
},
"raw_transcript_included": False,
"clinical_claim_allowed": False,
}
__all__ = [
"BENCHMARK_PATH",
"RepositoryComparisonInput",
"ValidatedRepositoryBenchmark",
"benchmark_anchor_metadata",
"build_repository_comparison_input",
"load_validated_repository_benchmark",
"produce_repository_version_comparison",
]