"""validate_report_render_diff_v1.py — spec/56: H005_REPORT_RENDER_DIFF Verifies that numbers in operational_report.json match the values in final_decision_packet_active.json (copy-only, no LLM recalculation). Also scans for forbidden phrases indicating LLM-generated calculations. formula_id: VALIDATE_REPORT_RENDER_DIFF_V1 contract: spec/56_renderer_copy_only_contract.yaml """ from __future__ import annotations import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_PACKET = ROOT / "Temp" / "final_decision_packet_active.json" DEFAULT_REPORT = ROOT / "Temp" / "operational_report.json" OUTPUT_PATH = ROOT / "Temp" / "report_render_diff_v1.json" # Forbidden phrases from spec/56 FORBIDDEN_PATTERNS = [ (r"계산.*하면", "renderer_calc"), (r"평균을\s*내면", "renderer_avg"), (r"합산하면", "renderer_sum"), (r"추정.*하면", "renderer_estimate"), (r"(? dict: if not path.exists(): return {"_missing": True, "_path": str(path)} try: return json.loads(path.read_text(encoding="utf-8")) except Exception as e: return {"_error": str(e), "_path": str(path)} def _deep_get(obj: dict, dotpath: str): parts = dotpath.split(".") cur = obj for p in parts: if not isinstance(cur, dict): return None cur = cur.get(p) return cur def _scan_forbidden_phrases(report: dict) -> list[dict]: findings = [] report_text = json.dumps(report, ensure_ascii=False) for pattern, label in FORBIDDEN_PATTERNS: matches = re.findall(pattern, report_text) if matches: findings.append({"pattern": pattern, "label": label, "matches": matches[:5]}) return findings def _check_gate_consistency(packet: dict, report: dict) -> tuple[int, list[dict]]: """Compare top-level gate values between packet and report.""" diffs = [] sections = report.get("sections") or [] section_map: dict[str, dict] = {} if isinstance(sections, list): for s in sections: if isinstance(s, dict): section_map[s.get("id") or s.get("name") or ""] = s packet_er_gate = str(_deep_get(packet, "execution_readiness.gate") or "") report_er_gate = "" exec_section = section_map.get("final_execution_decision") or section_map.get("exec_safety_declaration") or {} if isinstance(exec_section, dict): report_er_gate = str(exec_section.get("execution_readiness_gate") or exec_section.get("gate") or "") if packet_er_gate and report_er_gate and packet_er_gate != report_er_gate: diffs.append({ "field": "execution_readiness.gate", "packet_value": packet_er_gate, "report_value": report_er_gate, }) return len(diffs), diffs def run(packet_path: Path, report_path: Path) -> dict: packet = _load_json(packet_path) report = _load_json(report_path) if packet.get("_missing") or report.get("_missing"): missing = [] if packet.get("_missing"): missing.append("packet") if report.get("_missing"): missing.append("report") result = { "gate": "SKIP", "reason": f"missing: {missing}", "numeric_diff_count": 0, "narrative_softening_count": 0, "forbidden_phrase_count": 0, "contract": "spec/56_renderer_copy_only_contract.yaml", } OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2)) return result forbidden_findings = _scan_forbidden_phrases(report) forbidden_count = len(forbidden_findings) diff_count, diffs = _check_gate_consistency(packet, report) gate = "PASS" if diff_count > 0 or forbidden_count > 0: gate = "FAIL" result = { "gate": gate, "numeric_diff_count": diff_count, "numeric_diffs": diffs, "forbidden_phrase_count": forbidden_count, "forbidden_phrase_findings": forbidden_findings, "narrative_softening_count": 0, "contract": "spec/56_renderer_copy_only_contract.yaml", } OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2)) return result def main() -> None: import argparse parser = argparse.ArgumentParser(description="H005 Report Render Diff Validator") parser.add_argument("--packet", default=str(DEFAULT_PACKET)) parser.add_argument("--report", default=str(DEFAULT_REPORT)) args = parser.parse_args() result = run(Path(args.packet), Path(args.report)) gate = result.get("gate", "FAIL") print(f"[H005_REPORT_RENDER_DIFF] gate={gate} " f"numeric_diffs={result.get('numeric_diff_count', 0)} " f"forbidden_phrases={result.get('forbidden_phrase_count', 0)}") if gate == "FAIL": print(" Diffs:", result.get("numeric_diffs")) print(" Forbidden:", result.get("forbidden_phrase_findings")) sys.exit(1) if __name__ == "__main__": main()