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" / "execution_readiness_matrix_v1.json" REQUIRED_AXES = { "data_integrity", "routing_serving", "serving_output_lock", "decision_governance", "final_judgment_lock", "fundamental_basis", "horizon_policy", "smart_money_liquidity", "cash_recovery_execution", "execution_availability", "performance_readiness", "report_consistency", } 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 EXECUTION_READINESS_MATRIX_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] = [] if str(payload.get("formula_id") or "") != "EXECUTION_READINESS_MATRIX_V1": errors.append("formula_id mismatch") gate = str(payload.get("gate") or "") if gate not in {"PASS_100", "WATCH_PENDING_SAMPLE", "BLOCK_EXECUTION"}: errors.append(f"gate={gate}") axes = payload.get("axes") if not isinstance(axes, list) or not axes: errors.append("axes must be non-empty list") axes = [] seen: set[str] = set() scores: list[float] = [] block_count = 0 for idx, row in enumerate(axes): if not isinstance(row, dict): errors.append(f"axes[{idx}] must be object") continue axis = str(row.get("axis") or "") seen.add(axis) score = row.get("score_0_100") if not isinstance(score, (int, float)): errors.append(f"axes[{idx}].score_0_100 must be numeric") continue score_f = float(score) if score_f < 0 or score_f > 100: errors.append(f"axes[{idx}].score_0_100 out of range") scores.append(score_f) row_gate = str(row.get("gate") or "") if row_gate not in {"PASS_100", "WATCH", "BLOCK"}: errors.append(f"axes[{idx}].gate={row_gate}") if row_gate == "BLOCK": block_count += 1 if row_gate != "PASS_100" and not str(row.get("blocking_reason") or "").strip(): errors.append(f"axes[{idx}].blocking_reason missing") if not str(row.get("source_json") or "").strip(): errors.append(f"axes[{idx}].source_json missing") if not str(row.get("formula_id") or "").strip(): errors.append(f"axes[{idx}].formula_id missing") missing_axes = sorted(REQUIRED_AXES - seen) if missing_axes: errors.append(f"missing_axes={missing_axes}") if scores: expected_min = round(min(scores), 2) actual_min = payload.get("min_axis_score") if not isinstance(actual_min, (int, float)) or round(float(actual_min), 2) != expected_min: errors.append(f"min_axis_score mismatch expected={expected_min} actual={actual_min}") expected_avg = round(sum(scores) / len(scores), 2) actual_avg = payload.get("average_axis_score") if not isinstance(actual_avg, (int, float)) or round(float(actual_avg), 2) != expected_avg: errors.append(f"average_axis_score mismatch expected={expected_avg} actual={actual_avg}") actual_blocks = payload.get("hard_block_count") if not isinstance(actual_blocks, int) or actual_blocks != block_count: errors.append(f"hard_block_count mismatch expected={block_count} actual={actual_blocks}") if gate == "PASS_100": if block_count != 0: errors.append("PASS_100 cannot have BLOCK axes") if scores and min(scores) < 100.0: errors.append("PASS_100 requires all axes score_0_100=100") if gate == "BLOCK_EXECUTION" and block_count == 0: errors.append("BLOCK_EXECUTION requires at least one BLOCK axis") if errors: print("EXECUTION_READINESS_MATRIX_V1_FAIL") for err in errors: print(f" {err}") return 1 print("EXECUTION_READINESS_MATRIX_V1_OK") print(f" gate: {gate}") print(f" min_axis_score: {float(payload.get('min_axis_score')):.2f}") print(f" hard_block_count: {block_count}") return 0 if __name__ == "__main__": raise SystemExit(main())