27730704ae
Snapshot Admin Web Validation / validate-snapshot-admin-smoke (push) Has been cancelled
Snapshot Admin Web Validation / validate-snapshot-admin-full (push) Has been cancelled
Quant Engine CI/CD Pipeline / validate-core (pull_request) Has been cancelled
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been cancelled
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Has been cancelled
152 lines
5.6 KiB
Python
152 lines
5.6 KiB
Python
"""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())
|