from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_PATH = ROOT / "Temp" / "pass_100_criteria_v1.json" def _load(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: obj = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return obj if isinstance(obj, dict) else {} def main() -> int: ap = argparse.ArgumentParser(description="Validate PASS_100_CRITERIA_V1.") ap.add_argument("--json", default=str(DEFAULT_PATH)) args = ap.parse_args() path = Path(args.json) if not path.is_absolute(): path = ROOT / path payload = _load(path) errors: list[str] = [] _VALID_IDS = {"PASS_100_CRITERIA_V1", "PASS_100_CRITERIA_V3_ALIAS_V1"} if str(payload.get("formula_id") or "") not in _VALID_IDS: errors.append("formula_id mismatch") gate = str(payload.get("gate") or "") if gate not in {"PASS_100", "BLOCK_EXECUTION"}: errors.append(f"gate={gate}") if not isinstance(payload.get("pass_100_allowed"), bool): errors.append("pass_100_allowed must be bool") criteria = payload.get("criteria") if not isinstance(criteria, list) or not criteria: errors.append("criteria must be non-empty list") criteria = [] passed_count = 0 failed: list[str] = [] for idx, row in enumerate(criteria): if not isinstance(row, dict): errors.append(f"criteria[{idx}] must be object") continue if not str(row.get("criterion_id") or "").strip(): errors.append(f"criteria[{idx}].criterion_id missing") if not isinstance(row.get("passed"), bool): errors.append(f"criteria[{idx}].passed must be bool") continue if row["passed"]: passed_count += 1 else: failed.append(str(row.get("criterion_id") or f"criteria[{idx}]")) if not str(row.get("remediation") or "").strip() or str(row.get("remediation")) == "NONE": errors.append(f"criteria[{idx}].remediation missing") if not str(row.get("source_json") or "").strip(): errors.append(f"criteria[{idx}].source_json missing") if not str(row.get("formula_id") or "").strip(): errors.append(f"criteria[{idx}].formula_id missing") expected_score = round(passed_count / len(criteria) * 100.0, 2) if criteria else 0.0 actual_score = payload.get("score_0_100") if not isinstance(actual_score, (int, float)) or round(float(actual_score), 2) != expected_score: errors.append(f"score_0_100 mismatch expected={expected_score} actual={actual_score}") if payload.get("passed_count") != passed_count: errors.append(f"passed_count mismatch expected={passed_count} actual={payload.get('passed_count')}") if payload.get("failed_count") != len(failed): errors.append(f"failed_count mismatch expected={len(failed)} actual={payload.get('failed_count')}") if payload.get("failed_criteria") != failed: errors.append("failed_criteria mismatch") pass_allowed = bool(payload.get("pass_100_allowed")) if pass_allowed and failed: errors.append("pass_100_allowed cannot be true with failed criteria") if gate == "PASS_100" and failed: errors.append("PASS_100 cannot have failed criteria") if gate == "BLOCK_EXECUTION" and not failed: errors.append("BLOCK_EXECUTION requires failed criteria") if gate == "PASS_100" and not pass_allowed: errors.append("PASS_100 requires pass_100_allowed=true") if gate == "BLOCK_EXECUTION" and pass_allowed: errors.append("BLOCK_EXECUTION requires pass_100_allowed=false") if errors: print("PASS_100_CRITERIA_V1_FAIL") for err in errors: print(f" {err}") return 1 print("PASS_100_CRITERIA_V1_OK") print(f" gate: {gate}") print(f" score_0_100: {float(payload.get('score_0_100')):.2f}") print(f" failed_count: {len(failed)}") return 0 if __name__ == "__main__": raise SystemExit(main())