from __future__ import annotations import argparse import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DOCS_DIR = ROOT / "docs" MANIFEST_PATH = DOCS_DIR / "unit-sprite-rework-manifest.json" AUDIT_PATH = DOCS_DIR / "unit-sprite-rework-audit.json" def load_json(path: Path) -> dict: if not path.exists(): raise FileNotFoundError(f"missing required file: {path.relative_to(ROOT)}") return json.loads(path.read_text(encoding="utf-8")) def sorted_unique(values: list[str]) -> list[str]: return sorted(dict.fromkeys(values)) def row_by_stem(audit: dict) -> dict[str, dict]: return {row["stem"]: row for row in audit.get("rows", [])} def validate_manifest(manifest: dict, audit: dict) -> list[str]: issues: list[str] = [] rows = row_by_stem(audit) approved = sorted_unique(manifest.get("approvedBaseline", [])) pending = sorted_unique(manifest.get("pendingApproval", [])) next_after = sorted_unique(manifest.get("nextAfterApproval", [])) overlap = sorted(set(approved) & set(pending)) if overlap: issues.append(f"units cannot be both approved and pending: {', '.join(overlap)}") for stem in approved + pending + next_after: if stem not in rows: issues.append(f"manifest references unknown unit sheet: {stem}") for stem in approved: row = rows.get(stem) if not row: continue if row["base"]["status"] != "ok" or row["actions"]["status"] != "ok": issues.append(f"approved unit has technical issue: {stem}") if row["review_stage"] != "approved-baseline": issues.append(f"approved unit has wrong review stage in audit: {stem} -> {row['review_stage']}") for stem in pending: row = rows.get(stem) if not row: continue if row["base"]["status"] != "ok" or row["actions"]["status"] != "ok": issues.append(f"pending sample has technical issue: {stem}") if row["review_stage"] != "pending-approval": issues.append(f"pending sample has wrong review stage in audit: {stem} -> {row['review_stage']}") return issues def release_blockers(manifest: dict, audit: dict) -> list[str]: blockers: list[str] = [] pending = sorted_unique(manifest.get("pendingApproval", [])) if pending and manifest.get("approvalPolicy", {}).get("holdCommitPushDeployUntilBatchApproved", True): blockers.append(f"pending approval units block commit/push/deploy: {', '.join(pending)}") stage_counts = audit.get("stage_counts", {}) if stage_counts.get("pending-approval", 0) != len(pending): blockers.append( "audit pending-approval count does not match manifest " f"({stage_counts.get('pending-approval', 0)} != {len(pending)})" ) return blockers def sample_summary(manifest: dict, audit: dict) -> list[str]: rows = row_by_stem(audit) pending = sorted_unique(manifest.get("pendingApproval", [])) lines = [ "Unit sprite sample gate: OK", f"approved baseline: {len(manifest.get('approvedBaseline', []))}", f"pending approval: {len(pending)}", f"queued rework: {audit.get('stage_counts', {}).get('queued-rework', 0)}", ] for stem in pending: row = rows.get(stem) if row: base = row["base"] lines.append( f"{stem}: {row['review_stage']}, base={base['status']}, " f"battle={base.get('battle_min_width_px')}..{base.get('battle_max_width_px')} x " f"{base.get('battle_min_height_px')}..{base.get('battle_max_height_px')}" ) return lines def main() -> int: parser = argparse.ArgumentParser(description="Check sprite rework approval gates.") parser.add_argument( "--mode", choices=("sample", "release"), default="sample", help="sample allows pending approval; release fails while pending approval remains.", ) args = parser.parse_args() try: manifest = load_json(MANIFEST_PATH) audit = load_json(AUDIT_PATH) except Exception as exc: print(f"Unit sprite approval gate: FAILED\n- {exc}") return 1 issues = validate_manifest(manifest, audit) if issues: print("Unit sprite approval gate: FAILED") for issue in issues: print(f"- {issue}") return 1 if args.mode == "release": blockers = release_blockers(manifest, audit) if blockers: print("Unit sprite release gate: BLOCKED") for blocker in blockers: print(f"- {blocker}") return 2 print("Unit sprite release gate: OK") return 0 print("\n".join(sample_summary(manifest, audit))) return 0 if __name__ == "__main__": sys.exit(main())