"""validate_completion_criteria_v1.py — COMPLETION_CRITERIA_VALIDATOR_V1 spec/30_completion_criteria_contract.yaml의 16개 기준을 build_completion_gap_v1.py 산출물(Temp/completion_gap_v1.json)로부터 프로그램적으로 검증한다. 기본 모드: 기준 파일 존재·schema·계산 정확성 검증 (FAIL 기준도 허용). --require-pass N: N개 이상 PASS 필요. --strict: 모든 기준 PASS 필요 (이상적 목표 — 현재 달성 불가). 종료코드: 0=검증 통과, 1=검증 실패. """ from __future__ import annotations import argparse import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_GAP = ROOT / "Temp" / "completion_gap_v1.json" SPEC30 = ROOT / "spec" / "30_completion_criteria_contract.yaml" def _load(path: Path) -> dict: try: return json.loads(path.read_text(encoding="utf-8")) except Exception as e: print(f"FAIL: cannot load {path}: {e}") sys.exit(1) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--gap", default=str(DEFAULT_GAP)) ap.add_argument("--require-pass", type=int, default=0, help="Required minimum PASS count (0=no requirement)") ap.add_argument("--strict", action="store_true", help="All 16 criteria must PASS (ideal goal)") args = ap.parse_args() gap_path = Path(args.gap) if Path(args.gap).is_absolute() else ROOT / args.gap if not gap_path.exists(): print(f"FAIL: {gap_path} not found - run build-completion-gap-v1 first") return 1 d = _load(gap_path) failures: list[str] = [] # 1) schema 검증 required = ["formula_id", "total_criteria", "passed_count", "failed_count", "pass_rate_pct", "criteria", "priority_roadmap"] for f in required: if f not in d: failures.append(f"missing field: {f}") if "workflow_disciplines" not in d: failures.append("missing field: workflow_disciplines") if failures: for f in failures: print("FAIL:", f) return 1 total = d["total_criteria"] passed = d["passed_count"] pass_rate = d["pass_rate_pct"] criteria = d["criteria"] # 2) 16개 기준 완전성 확인 expected_criteria_count = 16 if total != expected_criteria_count: failures.append( f"total_criteria={total} != {expected_criteria_count} " f"— spec/30에 {expected_criteria_count}개 기준 정의됨" ) # 3) 계산 정확성 검증 actual_passed = sum(1 for c in criteria if c.get("status") == "PASS") actual_failed = sum(1 for c in criteria if c.get("status") == "FAIL") if actual_passed != passed: failures.append(f"passed_count mismatch: declared={passed} actual={actual_passed}") expected_rate = round(actual_passed / total * 100, 1) if total else 0 if abs(expected_rate - pass_rate) > 0.2: failures.append(f"pass_rate_pct mismatch: declared={pass_rate} expected={expected_rate}") # 4) 각 기준에 필수 필드 존재 확인 req_crit_fields = ["id", "target", "current", "status", "fix", "effort"] for c in criteria: for f in req_crit_fields: if f not in c: failures.append(f"criterion {c.get('id','?')} missing field: {f}") # 5) decision_source 필드 확인 (엔진 결정론 기준) reproducibility = next( (c for c in criteria if c.get("id") == "decision_reproducibility_score"), {} ) if reproducibility.get("status") != "PASS": failures.append("CRITICAL: decision_reproducibility_score != PASS — 결정론 미달") llm_field = next( (c for c in criteria if c.get("id") == "llm_generated_decision_field_count"), {} ) if str(llm_field.get("current")) != "0" and llm_field.get("current") != 0: failures.append("CRITICAL: llm_generated_decision_field_count != 0 — LLM 판단 개입") workflow = d.get("workflow_disciplines") if isinstance(d.get("workflow_disciplines"), dict) else {} required_order = workflow.get("required_preimplementation_order") if isinstance(workflow.get("required_preimplementation_order"), list) else [] expected_order = [ "로드맵/현황 확인", "WBS 작성", "목표 설정", "성공판단 데이터 정의", "구현", "사후 검증", "증빙 기록", ] if required_order != expected_order: failures.append(f"workflow_disciplines.required_preimplementation_order mismatch: {required_order}") for key in ("completion_gate_rule", "small_change_rule", "scope_change_rule", "evidence_rule"): if not str(workflow.get(key) or "").strip(): failures.append(f"workflow_disciplines missing or empty: {key}") if failures: for f in failures: print("FAIL:", f) print(f"COMPLETION_CRITERIA_VALIDATOR_V1: FAIL ({len(failures)} issue(s))") return 1 # 6) --require-pass 검사 if args.require_pass > 0 and passed < args.require_pass: print( f"REQUIRE_PASS_FAIL: {passed}/{total} < required {args.require_pass} " f"({pass_rate}%)" ) return 1 # 7) --strict 검사 if args.strict and passed < total: fail_ids = [c["id"] for c in criteria if c.get("status") == "FAIL"] print(f"STRICT_FAIL: {total-passed}/{total} criteria still FAIL: {fail_ids}") return 1 print( f"COMPLETION_CRITERIA_VALIDATOR_V1: OK | " f"{passed}/{total} PASS ({pass_rate}%) | " f"deterministic=PASS | llm_fields=0" ) return 0 if __name__ == "__main__": sys.exit(main())