from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] MANIFEST_PATH = ROOT / "docs" / "unit-sprite-rework-manifest.json" AUDIT_PATH = ROOT / "docs" / "unit-sprite-rework-audit.json" def load_json(path: Path) -> dict[str, Any]: if not path.exists(): raise FileNotFoundError(f"missing required file: {path.relative_to(ROOT)}") return json.loads(path.read_text(encoding="utf-8")) def write_manifest(manifest: dict[str, Any]) -> None: MANIFEST_PATH.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def audit_row(audit: dict[str, Any], unit: str) -> dict[str, Any] | None: return next((row for row in audit.get("rows", []) if row.get("stem") == unit), None) def require_ok_sample(unit: str, audit: dict[str, Any]) -> None: row = audit_row(audit, unit) if row is None: raise ValueError(f"{unit}: missing from audit") if row.get("review_stage") != "pending-approval": raise ValueError(f"{unit}: expected pending-approval stage, got {row.get('review_stage')}") if row.get("base", {}).get("status") != "ok" or row.get("actions", {}).get("status") != "ok": raise ValueError(f"{unit}: base/actions audit must both be ok before promotion") def promote(manifest: dict[str, Any], unit: str, expected_version: str | None) -> dict[str, Any]: pending = list(manifest.get("pendingApproval", [])) approved = list(manifest.get("approvedBaseline", [])) candidates = manifest.get("currentApprovalCandidate", {}) candidate = candidates.get(unit, {}) if unit not in pending: raise ValueError(f"{unit}: not listed in pendingApproval") if unit in approved: raise ValueError(f"{unit}: already listed in approvedBaseline") if expected_version and candidate.get("version") != expected_version: raise ValueError( f"{unit}: expected candidate version {expected_version}, got {candidate.get('version')}" ) manifest["pendingApproval"] = [item for item in pending if item != unit] manifest["approvedBaseline"] = sorted([*approved, unit]) if isinstance(candidates, dict): candidates.pop(unit, None) manifest["currentApprovalCandidate"] = candidates return manifest def main() -> int: parser = argparse.ArgumentParser( description="Promote a user-approved pending unit sprite sample into approvedBaseline." ) parser.add_argument("--unit", required=True, help="Unit texture key, for example unit-shu-infantry.") parser.add_argument("--expected-version", help="Optional candidate version guard, for example v3.") parser.add_argument( "--dry-run", action="store_true", help="Validate and print the resulting manifest status without writing the file.", ) args = parser.parse_args() try: manifest = load_json(MANIFEST_PATH) audit = load_json(AUDIT_PATH) require_ok_sample(args.unit, audit) next_manifest = promote(manifest, args.unit, args.expected_version) except Exception as exc: print(f"Unit sprite approval promotion: FAILED\n- {exc}") return 1 approved_count = len(next_manifest.get("approvedBaseline", [])) pending_count = len(next_manifest.get("pendingApproval", [])) if args.dry_run: print("Unit sprite approval promotion: DRY RUN OK") else: write_manifest(next_manifest) print("Unit sprite approval promotion: OK") print(f"- promoted: {args.unit}") print(f"- approved baseline: {approved_count}") print(f"- pending approval: {pending_count}") return 0 if __name__ == "__main__": sys.exit(main())