vignette/apps/api/app/services/continuous_improvement.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

175 lines
6.2 KiB
Python

"""G8 콘텐츠 승격, 모델 교체 gate, 운영 오류 회귀 DAG 코어."""
from __future__ import annotations
from typing import Iterable
from ..contracts.continuous_improvement import (
AgenticReleaseManifest,
ApprovedCatalogEntry,
ContentBenchmarkQualification,
ContentSourceArtifact,
GeneratedContentDraft,
IncidentRegressionDag,
IndependentRedTeamReview,
ModelCalibrationSnapshot,
ModelChangeGateResult,
OperationalIncident,
RegressionBacklogNode,
)
REQUIRED_REVIEW_DIMENSIONS = {
"safety",
"identity",
"answer_leakage",
"cultural_bias",
"difficulty",
"pii",
"grounding",
}
def promote_content_to_catalog(
*,
draft: GeneratedContentDraft,
sources: Iterable[ContentSourceArtifact],
reviews: Iterable[IndependentRedTeamReview],
benchmark: ContentBenchmarkQualification,
) -> ApprovedCatalogEntry:
source_by_id = {item.source_id: item for item in sources}
if set(draft.source_refs) - set(source_by_id):
raise ValueError("content draft references an unknown source")
if any(source_by_id[item].usage_status != "approved" for item in draft.source_refs):
raise ValueError("content promotion requires approved source usage")
if draft.visible_answer_overlap_tokens:
raise ValueError("content promotion blocked by visible answer leakage")
if draft.pii_findings:
raise ValueError("content promotion blocked by PII findings")
if draft.unsupported_clinical_claims:
raise ValueError("content promotion blocked by unsupported clinical claims")
review_items = tuple(reviews)
if len(review_items) < 2:
raise ValueError("content promotion requires two independent red-team reviews")
if len({item.reviewer_agent_id for item in review_items}) != len(review_items):
raise ValueError("red-team reviewers must be independent")
if any(item.draft_id != draft.draft_id for item in review_items):
raise ValueError("red-team review references another draft")
if any(
item.reviewed_payload_sha256 != draft.payload_sha256 for item in review_items
):
raise ValueError("red-team review payload hash differs from draft")
covered = {dimension for item in review_items for dimension in item.dimensions}
if not REQUIRED_REVIEW_DIMENSIONS.issubset(covered):
raise ValueError("red-team reviews do not cover every required dimension")
unresolved = [
finding
for item in review_items
for finding in item.findings
if finding.state == "open"
or (
finding.state == "accepted_risk" and finding.severity in {"blocker", "high"}
)
]
if unresolved:
raise ValueError("content promotion blocked by unresolved red-team findings")
if benchmark.draft_id != draft.draft_id or not benchmark.qualified:
raise ValueError("content promotion requires a qualified benchmark")
return ApprovedCatalogEntry(
catalog_entry_id=draft.draft_id.replace("oas-g8-draft-", "oas-g8-catalog-"),
draft_id=draft.draft_id,
payload_sha256=draft.payload_sha256,
source_refs=draft.source_refs,
review_ids=tuple(item.review_id for item in review_items),
benchmark_id=benchmark.benchmark_id,
)
def decide_model_change(
*,
baseline: ModelCalibrationSnapshot,
candidate: ModelCalibrationSnapshot,
) -> ModelChangeGateResult:
reasons: list[str] = []
if candidate.critical_miss_count > baseline.critical_miss_count:
reasons.append("critical_miss_regression")
if candidate.leakage_count or candidate.pii_count:
reasons.append("candidate_privacy_or_leakage_failure")
if candidate.task_accuracy < baseline.task_accuracy - 0.02:
reasons.append("task_accuracy_regression")
if candidate.calibration_error > baseline.calibration_error + 0.02:
reasons.append("calibration_error_regression")
if candidate.subgroup_max_gap > baseline.subgroup_max_gap + 0.05:
reasons.append("subgroup_gap_regression")
if candidate.leakage_count or candidate.pii_count or candidate.critical_miss_count:
decision = "rollback"
elif reasons:
decision = "quarantine"
else:
decision = "promote"
reasons.append("candidate_passed_all_calibration_gates")
return ModelChangeGateResult(
baseline_snapshot_id=baseline.snapshot_id,
candidate_snapshot_id=candidate.snapshot_id,
decision=decision,
reasons=tuple(reasons),
rollback_target_snapshot_id=(
baseline.snapshot_id if decision == "rollback" else None
),
)
def build_incident_regression_dag(
incident: OperationalIncident,
) -> IncidentRegressionDag:
prefix = incident.incident_id.replace("oas-g8-incident-", "")
reproduction_id = f"oas-g8-node-{prefix}-reproduction"
implementation_id = f"oas-g8-node-{prefix}-implementation"
e2e_id = f"oas-g8-node-{prefix}-e2e"
runtime_id = f"oas-g8-node-{prefix}-runtime"
return IncidentRegressionDag(
incident_id=incident.incident_id,
nodes=(
RegressionBacklogNode(
node_id=reproduction_id,
node_type="reproduction_test",
depends_on=(),
evidence_ref=incident.evidence_refs[0],
status="pending",
),
RegressionBacklogNode(
node_id=implementation_id,
node_type="implementation",
depends_on=(reproduction_id,),
status="pending",
),
RegressionBacklogNode(
node_id=e2e_id,
node_type="e2e",
depends_on=(implementation_id,),
status="pending",
),
RegressionBacklogNode(
node_id=runtime_id,
node_type="runtime_proof",
depends_on=(e2e_id,),
status="pending",
),
),
)
def release_allowed(manifest: AgenticReleaseManifest) -> bool:
return manifest.releasable
__all__ = [
"REQUIRED_REVIEW_DIMENSIONS",
"build_incident_regression_dag",
"decide_model_change",
"promote_content_to_catalog",
"release_allowed",
]