#!/usr/bin/env python3 """Build a fail-closed demo recording readiness report.""" from __future__ import annotations import argparse import importlib.util import json from pathlib import Path from typing import Any from proofframe.submission_gate import build_submission_gate ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "docs" / "assets" / "demo-readiness-report.json" DEFAULT_MD = ROOT / "docs" / "assets" / "demo-readiness-report.md" CLAIM_LINT_PATH = ROOT / "scripts" / "claim_lint.py" CLAIM_LINT_SPEC = importlib.util.spec_from_file_location("claim_lint", CLAIM_LINT_PATH) claim_lint = importlib.util.module_from_spec(CLAIM_LINT_SPEC) assert CLAIM_LINT_SPEC.loader is not None CLAIM_LINT_SPEC.loader.exec_module(claim_lint) REQUIRED_MOCK_RECORDING_FILES = [ "README.md", "docs/demo_script.md", "docs/public_claim_freeze.md", "docs/assets/devpost-submission-packet.json", "docs/assets/devpost-submission-packet.md", "docs/assets/devpost-form-kit.json", "docs/assets/devpost-form-kit.md", "docs/assets/submission-bundle-manifest.json", "docs/assets/submission-bundle-manifest.md", "docs/assets/live-credential-handoff.json", "docs/assets/live-credential-handoff.md", "docs/assets/demo-storyboard.json", "docs/assets/demo-storyboard.md", "docs/assets/public-video-check.json", "docs/assets/public-video-check.md", "docs/assets/proofframe-local-ui-smoke.png", "docs/assets/proofframe-review-console-smoke.png", "docs/assets/proofframe-hf-public-smoke.png", ".env.final.example", "scripts/claim_lint.py", "scripts/devpost_form_kit.py", "scripts/demo_storyboard.py", "scripts/public_video_check.py", "scripts/live_env_handoff.py", "scripts/run_b2_live_proof.py", "scripts/secret_scan.py", ] def load_json(path: Path) -> dict[str, Any] | None: try: return json.loads(path.read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return None def task_statuses(root: Path) -> dict[str, str]: tasks = load_json(root / "tasks.json") if not tasks: return {} return { str(task.get("id", "")).upper(): str(task.get("status", "missing")) for task in tasks.get("tasks", []) } def file_record(root: Path, relative_path: str) -> dict[str, Any]: path = root / relative_path return { "path": relative_path, "present": path.exists(), "bytes": path.stat().st_size if path.exists() else 0, } def missing_files(records: list[dict[str, Any]]) -> list[str]: return [record["path"] for record in records if not record["present"]] def build_next_actions( *, missing: list[str], claim_ok: bool, statuses: dict[str, str], evidence_ok: bool, ) -> list[str]: actions: list[str] = [] if missing: actions.append("Restore missing demo recording files before recording.") if not claim_ok: actions.append("Run python scripts/claim_lint.py and fix unsafe public claims.") if statuses.get("T020") != "done": actions.append("Capture the live Backblaze B2 asset and manifest proof.") if statuses.get("T021") != "done": actions.append("Capture the live Genblaze generation proof.") if not evidence_ok: actions.append("Write docs/assets/final-live-proof-evidence.json with sanitized live proof.") if statuses.get("T041A") != "done": actions.append("Run the final secret scan before recording the final sponsor-backed video.") return actions[:6] def build_report(root: Path = ROOT) -> dict[str, Any]: root = root.resolve() files = [file_record(root, path) for path in REQUIRED_MOCK_RECORDING_FILES] missing = missing_files(files) packet = load_json(root / "docs" / "assets" / "devpost-submission-packet.json") or {} gate = build_submission_gate(root) statuses = task_statuses(root) claim_report = claim_lint.build_claim_report(root) mock_recording_ready = ( not missing and claim_report["ok"] and packet.get("mode") in {"pre_live_safe", "post_live_verified"} and str(packet.get("demo_url", "")).endswith("/?judge=1") and statuses.get("T040") == "done" ) final_recording_ready = ( mock_recording_ready and gate["evidence_gate"]["ok"] and statuses.get("T020") == "done" and statuses.get("T021") == "done" and statuses.get("T041A") == "done" ) return { "schema": "proofframe.demo_readiness.v1", "mode": "final_ready" if final_recording_ready else "pre_live_mock_ready", "mock_recording_ready": mock_recording_ready, "final_recording_ready": final_recording_ready, "public_demo_url": packet.get("demo_url"), "devpost_packet_mode": packet.get("mode"), "task_statuses": {task: statuses.get(task, "missing") for task in ["T020", "T021", "T040", "T041", "T041A", "T042"]}, "submission_gate": { "ok": gate["ok"], "mode": gate["mode"], "summary": gate["summary"], "evidence_status": gate["evidence_gate"]["status"], "packet_status": gate["packet_gate"]["status"], }, "claim_lint": { "ok": claim_report["ok"], "mode": claim_report["mode"], "findings": claim_report["findings"], }, "files": files, "missing_files": missing, "next_actions": build_next_actions( missing=missing, claim_ok=claim_report["ok"], statuses=statuses, evidence_ok=gate["evidence_gate"]["ok"], ), } def render_markdown(report: dict[str, Any]) -> str: lines = [ "# ProofFrame Demo Readiness", "", f"Mode: `{report['mode']}`", f"Mock recording ready: `{str(report['mock_recording_ready']).lower()}`", f"Final recording ready: `{str(report['final_recording_ready']).lower()}`", f"Public demo: {report['public_demo_url']}", f"Devpost packet mode: `{report['devpost_packet_mode']}`", "", "## Current Gate", "", f"- Submission gate: `{report['submission_gate']['mode']}`", f"- Live evidence: `{report['submission_gate']['evidence_status']}`", f"- Claim lint: `{str(report['claim_lint']['ok']).lower()}`", "", "## Task Status", "", ] lines.extend(f"- {task}: `{status}`" for task, status in report["task_statuses"].items()) lines.extend(["", "## Required Recording Files", ""]) for item in report["files"]: marker = "OK" if item["present"] else "MISSING" lines.append(f"- {marker} `{item['path']}`") lines.extend(["", "## Next Actions", ""]) if report["next_actions"]: lines.extend(f"- {action}" for action in report["next_actions"]) else: lines.append("- Ready to record the final sponsor-backed demo.") return "\n".join(lines) + "\n" def write_outputs(report: dict[str, Any], json_path: Path, markdown_path: Path) -> None: json_path.parent.mkdir(parents=True, exist_ok=True) markdown_path.parent.mkdir(parents=True, exist_ok=True) json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") markdown_path.write_text(render_markdown(report), encoding="utf-8") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Build a ProofFrame demo recording readiness report.") parser.add_argument("--root", type=Path, default=ROOT) parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON) parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD) parser.add_argument( "--strict-final", action="store_true", help="Fail unless final B2/Genblaze recording evidence is ready.", ) return parser def main() -> None: args = build_parser().parse_args() report = build_report(args.root) write_outputs(report, args.json_out, args.markdown_out) print( json.dumps( { "ok": report["final_recording_ready"] if args.strict_final else report["mock_recording_ready"], "mode": report["mode"], "json": str(args.json_out), "markdown": str(args.markdown_out), "next_actions": report["next_actions"], }, indent=2, ) ) raise SystemExit( 0 if (report["final_recording_ready"] if args.strict_final else report["mock_recording_ready"]) else 2 ) if __name__ == "__main__": main()