C-001 임상 검토 게이트 보강

This commit is contained in:
Yun Chan 2026-08-28 19:04:17 +09:00
parent 391fb9f4d0
commit 6988280b30
18 changed files with 1896 additions and 67 deletions

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import importlib.util
import json
import unittest
@ -9,33 +10,101 @@ from .paths import repo_path
from .services import guardrail
def _load_clinical_review_checker():
checker_path = repo_path("scripts", "check-clinical-crisis-review.py")
spec = importlib.util.spec_from_file_location("clinical_crisis_review_checker", checker_path)
if spec is None or spec.loader is None:
raise RuntimeError(f"임상 검토 checker를 불러올 수 없다: {checker_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class ClientReplyQualityGateTest(unittest.TestCase):
def test_crisis_protocol_manifest_preserves_external_clinical_approval_gate(self) -> None:
def test_crisis_protocol_manifest_obeys_status_specific_external_review_contract(self) -> None:
manifest = json.loads(
repo_path("data", "clinical", "crisis-protocol-validation.json").read_text(
encoding="utf-8",
),
)
case_set = json.loads(
repo_path("data", "clinical", "p1-crisis-review-cases.json").read_text(
encoding="utf-8",
),
)
self.assertEqual(manifest["technical_status"], "verified")
self.assertEqual(manifest["clinical_status"], "pending_external_review")
status = manifest["clinical_status"]
status_decisions = {
"pending_external_review": None,
"approved": "approved",
"conditional": "conditional",
"rejected": "rejected",
}
self.assertIn(status, status_decisions)
required_approval_fields = {
"reviewer",
"organization",
"reviewed_at",
"decision",
"notes",
"evidence_ref",
"evidence_sha256",
"reviewed_protocol_version",
"reviewed_case_set_sha256",
}
approval = manifest["approval"]
self.assertTrue(required_approval_fields.issubset(approval))
for field in required_approval_fields:
self.assertIsNone(approval[field])
technical_gate_text = " ".join(manifest["technical_gates"])
for field in required_approval_fields:
self.assertIn(f"approval.{field}", technical_gate_text)
authorities = {source["authority"] for source in manifest["sources"]}
self.assertEqual(authorities, {"SAMHSA", "NIMH", "대한민국 보건복지부"})
self.assertEqual(set(approval), required_approval_fields)
self.assertEqual(approval["decision"], status_decisions[status])
assessments = [case["reviewer_assessment"] for case in case_set["cases"]]
if status == "pending_external_review":
self.assertTrue(all(value is None for value in approval.values()))
self.assertTrue(
all(value is None for assessment in assessments for value in assessment.values()),
)
else:
completed_fields = required_approval_fields - {"notes"}
self.assertTrue(all(approval[field] for field in completed_fields))
self.assertTrue(all(assessment["decision"] for assessment in assessments))
self.assertTrue(all(assessment["rationale"] for assessment in assessments))
self.assertTrue(all(assessment["reviewed_at"] for assessment in assessments))
source_ids = {source["source_id"] for source in manifest["sources"]}
self.assertEqual(
source_ids,
{"samhsa_safe_t", "nimh_youth_outpatient_bssa", "mohw_109", "nice_ng225"},
)
self.assertGreaterEqual(len(manifest["review_sequence"]), 6)
self.assertGreaterEqual(len(manifest["technical_gates"]), 5)
self.assertGreaterEqual(len(manifest["technical_gates"]), 6)
self.assertIn("외부 임상 검토와 승인을 대신하지 않는다", manifest["external_review_boundary"])
checker = _load_clinical_review_checker()
self.assertEqual(checker.validate_review_contract(repo_root=repo_path()), [])
def test_synthetic_crisis_review_cases_contain_only_technical_invariants(self) -> None:
case_set = json.loads(
repo_path("data", "clinical", "p1-crisis-review-cases.json").read_text(
encoding="utf-8",
),
)
self.assertEqual(
case_set["content_safety"],
{
"synthetic_only": True,
"method_or_means_detail": "forbidden",
"clinical_answer_included": False,
},
)
self.assertEqual(
{case["case_id"] for case in case_set["cases"]},
{f"P1-CRISIS-{index:03d}" for index in range(1, 7)},
)
for case in case_set["cases"]:
with self.subTest(case_id=case["case_id"]):
self.assertFalse(case["synthetic_scenario"]["method_or_means_detail_present"])
self.assertGreaterEqual(len(case["technical_invariants"]), 1)
self.assertNotIn("clinical_answer", case)
def test_crisis_protocol_technical_gates(self) -> None:
counselor_inquiry = guardrail.classify_crisis(