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" / "rule_lifecycle_policy.json" def load_json(path: Path) -> dict[str, Any]: payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError("policy payload must be object") return payload def main() -> int: parser = argparse.ArgumentParser(description="Validate rule_lifecycle_policy.json schema and action consistency.") parser.add_argument("--json", default=str(DEFAULT_PATH)) args = parser.parse_args() path = Path(args.json) if not path.is_absolute(): path = ROOT / path payload = load_json(path) errors: list[str] = [] if not payload.get("as_of"): errors.append("missing as_of") if not str(payload.get("schema_version") or "").startswith("2026-05-22-rule-lifecycle-v1"): errors.append("schema_version mismatch") summary = payload.get("summary") if not isinstance(summary, dict): errors.append("summary must be object") summary = {} actions_count = summary.get("actions_count") if not isinstance(actions_count, dict): errors.append("summary.actions_count must be object") actions_count = {} rows = payload.get("rule_lifecycle_rows") if not isinstance(rows, list): errors.append("rule_lifecycle_rows must be list") rows = [] allowed_actions = {"KEEP", "WATCH", "DEMOTE", "RETIRE"} allowed_states = {"ACTIVE", "SHADOW", "DEPRECATED", "RETIRED"} counted: dict[str, int] = {k: 0 for k in allowed_actions} for idx, row in enumerate(rows): if not isinstance(row, dict): errors.append(f"row[{idx}] must be object") continue action = str(row.get("policy_action") or "") state = str(row.get("rule_state") or "") if action not in allowed_actions: errors.append(f"row[{idx}].policy_action invalid: {action}") else: counted[action] += 1 if state not in allowed_states: errors.append(f"row[{idx}].rule_state invalid: {state}") for key in ("rule_key", "policy_reason"): if not str(row.get(key) or "").strip(): errors.append(f"row[{idx}].{key} missing") samples = row.get("samples_t1") if not isinstance(samples, int) or samples < 0: errors.append(f"row[{idx}].samples_t1 invalid") mismatch = row.get("mismatch_rate_t1_pct") if mismatch is not None and (not isinstance(mismatch, (int, float)) or mismatch < 0 or mismatch > 100): errors.append(f"row[{idx}].mismatch_rate_t1_pct invalid") for action, cnt in counted.items(): if int(actions_count.get(action, 0)) != cnt: errors.append(f"actions_count mismatch for {action}: summary={actions_count.get(action)} rows={cnt}") if errors: print("RULE_LIFECYCLE_POLICY_INVALID") print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2)) return 1 print("RULE_LIFECYCLE_POLICY_OK") print(json.dumps({"rows": len(rows), "actions_count": actions_count}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())