Files
heros_web/scripts/check-unit-sprite-approval-gate.py

142 lines
4.8 KiB
Python

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())