from __future__ import annotations import argparse import json 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" DEFAULT_OUTPUT = DOCS_DIR / "unit-sprite-next-steps.md" SPEC_BY_STEM = { "unit-shu-spearman": "unit-sprite-batch2-shu-spearman-spec.md", "unit-shu-archer": "unit-sprite-batch2-shu-archer-spec.md", "unit-shu-cavalry": "unit-sprite-batch2-shu-cavalry-spec.md", "unit-shu-strategist": "unit-sprite-batch2-shu-strategist-spec.md", "unit-shu-officer": "unit-sprite-batch2-shu-officer-spec.md", } 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 row_by_stem(audit: dict) -> dict[str, dict]: return {row["stem"]: row for row in audit.get("rows", [])} def command_block(commands: list[str]) -> list[str]: return ["```powershell", *commands, "```"] def write_report(output: Path) -> None: manifest = load_json(MANIFEST_PATH) audit = load_json(AUDIT_PATH) rows = row_by_stem(audit) approved = manifest.get("approvedBaseline", []) pending = manifest.get("pendingApproval", []) candidates = manifest.get("currentApprovalCandidate", {}) next_after = manifest.get("nextAfterApproval", []) stage_counts = audit.get("stage_counts", {}) lines = [ "# Unit Sprite Next Steps", "", "This file is generated by `scripts/write-unit-sprite-next-steps.py`.", "", "## Current Gate", "", f"- Approved baseline units: {len(approved)}", f"- Pending approval units: {len(pending)}", f"- Queued rework units: {stage_counts.get('queued-rework', 0)}", f"- Manifest: `{MANIFEST_PATH.relative_to(ROOT).as_posix()}`", f"- Audit: `{AUDIT_PATH.relative_to(ROOT).as_posix()}`", "- Batch inventory boards include old queued assets; use them for inventory/regression checks, not approval.", "", ] if pending: lines.insert(-2, "- Current approval board: `docs/unit-sprite-current-approval-board.png`") else: lines.insert(-2, "- Current approval board: not generated until the next pending sample.") if pending: lines.extend( [ "## Approval Blocker", "", "Do not expand to the next unit, commit, push, or deploy while these units remain pending:", "", ] ) 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 bbox `{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')}`" ) else: lines.append(f"- `{stem}`: missing from audit") lines.append("") promote_commands = [] if pending: unit = pending[0] version = candidates.get(unit, {}).get("version") if isinstance(candidates, dict) else None command = f"python scripts\\promote-unit-sprite-approval.py --unit {unit}" if version: command += f" --expected-version {version}" promote_commands.append(command) lines.extend( [ "## Next Unit Queue", "", ] ) if next_after: for index, stem in enumerate(next_after, start=1): row = rows.get(stem) status = "missing from audit" if row: status = f"{row['review_stage']}, base `{row['base']['status']}`, actions `{row['actions']['status']}`" spec = SPEC_BY_STEM.get(stem) spec_note = f", spec `{spec}`" if spec else "" lines.append(f"{index}. `{stem}` - {status}{spec_note}") else: lines.append("No queued next unit is defined in the manifest.") lines.extend( [ "", "## Commands Before Approval", "", *command_block( [ "python scripts\\audit-unit-sprite-sheets.py", "python scripts\\render-unit-sprite-review-boards.py", "python scripts\\render-current-sprite-approval-board.py", "python scripts\\check-unit-sprite-art-quality.py", "python scripts\\check-unit-sprite-approval-gate.py --mode sample", "python scripts\\write-unit-sprite-next-steps.py", "python scripts\\check-unit-sprite-workflow-consistency.py", ] ), "", "## Commands After Approval", "", "After user approval, promote the approved unit in `unit-sprite-rework-manifest.json`, regenerate outputs, and only then use the release gate.", "", *command_block( [ *promote_commands, "python scripts\\audit-unit-sprite-sheets.py", "python scripts\\render-unit-sprite-review-boards.py", "python scripts\\check-unit-sprite-approval-gate.py --mode release", "pnpm build", ] ), "", "## First Task After Current Approval", "", ] ) if next_after: first = next_after[0] spec = SPEC_BY_STEM.get(first, "") lines.append(f"- Start with `{first}`.") if spec: lines.append(f"- Use spec: `{spec}`.") lines.append("- Generate source art, assemble sheets, produce contact/GIF/before-after/scale/battle screenshots, then rerun the gates.") else: lines.append("- No first task is defined.") output.write_text("\n".join(lines) + "\n", encoding="utf-8") def main() -> None: parser = argparse.ArgumentParser(description="Write the next-step report for sprite rework.") parser.add_argument("--out", type=Path, default=DEFAULT_OUTPUT) args = parser.parse_args() write_report(args.out) print(f"Wrote {args.out}") if __name__ == "__main__": main()