#!/usr/bin/env python3 """Build one no-secret status report for the final Devpost closeout.""" from __future__ import annotations import argparse import json from datetime import datetime, timezone from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] SCHEMA = "proofframe.final_closeout_status.v1" DEFAULT_JSON = ROOT / "docs" / "assets" / "final-closeout-status.json" DEFAULT_MD = ROOT / "docs" / "assets" / "final-closeout-status.md" REPORTS = { "final_control": ("docs/assets/final-submission-control.json", "proofframe.final_submission_control.v1"), "credential_handoff": ("docs/assets/live-credential-handoff.json", "proofframe.live_credential_handoff.v1"), "b2_evidence": ("docs/assets/b2-live-proof-evidence.json", "proofframe.b2_live_proof.v1"), "final_evidence": ("docs/assets/final-live-proof-evidence.json", "proofframe.final_live_proof.v1"), "public_video": ("docs/assets/public-video-check.json", "proofframe.public_video_check.v1"), "video_publish_kit": ("docs/assets/final-video-publish-kit.json", "proofframe.final_video_publish_kit.v1"), "devpost_checklist": ("docs/assets/devpost-submission-checklist.json", "proofframe.devpost_submission_checklist.v1"), "devpost_preview": ("docs/assets/devpost-submission-preview.json", "proofframe.devpost_submission_preview.v1"), "devpost_receipt": ("docs/assets/devpost-submission-receipt.json", "proofframe.devpost_submission_receipt.v1"), "secret_scan": ("docs/assets/secret-scan-report.json", "proofframe.secret_scan.v1"), "submission_audit": ("docs/assets/submission-audit-report.json", "proofframe.submission_audit.v1"), "submission_bundle": ("docs/assets/submission-bundle-manifest.json", "proofframe.submission_bundle.v1"), "public_space_sync": ("docs/assets/public-space-sync-report.json", "proofframe.public_space_sync.v1"), } REQUIRED_TASKS = ("T020", "T021", "T041", "T041A", "T042") def utc_now() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") def load_json(path: Path) -> dict[str, Any] | None: try: data = json.loads(path.read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return None return data if isinstance(data, dict) else None def relative(path: Path, root: Path) -> str: try: return str(path.resolve().relative_to(root.resolve())) except ValueError: return str(path) def task_statuses(root: Path) -> dict[str, str]: tasks = load_json(root / "tasks.json") or {} return { str(task.get("id", "")).upper(): str(task.get("status", "missing")) for task in tasks.get("tasks", []) } def report_status(root: Path, report_id: str, relative_path: str, schema: str) -> dict[str, Any]: path = root / relative_path report = load_json(path) if report is None: return { "id": report_id, "present": False, "schema_ok": False, "path": relative_path, "schema": None, "ok": None, "mode": None, "safe_to_submit": None, } return { "id": report_id, "present": True, "schema_ok": report.get("schema") == schema, "path": relative(path, root), "schema": report.get("schema"), "ok": report.get("ok"), "mode": report.get("mode"), "storage_backend": report.get("storage_backend"), "generation_backend": report.get("generation_backend"), "asset_storage_backend": report.get("asset_storage_backend"), "manifest_storage_backend": report.get("manifest_storage_backend"), "asset_storage_key": report.get("asset_storage_key"), "manifest_key": report.get("manifest_key"), "asset_sha256": report.get("asset_sha256"), "manifest_sha256": report.get("manifest_sha256"), "safe_to_submit": report.get("safe_to_submit"), "safe_to_share": report.get("safe_to_share"), "control_health_ok": report.get("control_health_ok"), "submission_gate_ok": (report.get("submission_gate") or {}).get("ok"), "submission_gate_mode": (report.get("submission_gate") or {}).get("mode"), "closeout_gate_status": (report.get("closeout_gate") or {}).get("status"), "ready_for_live_proof": report.get("ready_for_live_proof"), "final_video_ready": report.get("final_video_ready"), "final_form_ready": report.get("final_form_ready"), "public_video_ready": report.get("public_video_ready"), "project_url": report.get("project_url"), "missing_artifacts": report.get("missing_artifacts"), "missing_ids": report.get("missing_ids"), "findings_count": len(report.get("findings") or []), } def gate_item(gate_id: str, label: str, ok: bool, detail: str, evidence: str) -> dict[str, Any]: return {"id": gate_id, "label": label, "ok": ok, "detail": detail, "evidence": evidence} def evidence_ok(report: dict[str, Any], *, expected_mode: str | None = None) -> bool: if not (report["present"] and report["schema_ok"] and report["ok"] is True): return False if expected_mode is not None and report.get("mode") != expected_mode: return False return True def b2_live_evidence_ok(report: dict[str, Any]) -> bool: if evidence_ok(report): return True return bool( report["present"] and report.get("ok") is True and report.get("storage_backend") == "b2" and report.get("generation_backend") == "mock" and report.get("asset_storage_backend") == "b2" and report.get("manifest_storage_backend") == "b2" and report.get("asset_storage_key") and report.get("manifest_key") and report.get("asset_sha256") and report.get("manifest_sha256") ) def final_live_evidence_ok(report: dict[str, Any]) -> bool: if evidence_ok(report): return True return bool( report["present"] and report.get("ok") is True and report.get("storage_backend") == "b2" and report.get("generation_backend") == "genblaze" and report.get("asset_storage_backend") == "b2" and report.get("manifest_storage_backend") == "b2" and report.get("asset_storage_key") and report.get("manifest_key") and report.get("asset_sha256") and report.get("manifest_sha256") ) def credential_handoff_ready(report: dict[str, Any]) -> bool: if not (report["present"] and report["schema_ok"]): return False if report.get("ready_for_live_proof") is True: return True return bool(report.get("ok") is True and report.get("mode") == "live_env_ready") def credential_handoff_detail(report: dict[str, Any]) -> str: missing = [str(item) for item in report.get("missing_ids", []) if item] suffix = f"; missing ids: {', '.join(missing)}" if missing else "" return f"Credential handoff mode is {report.get('mode')}{suffix}." def submission_bundle_inputs_ready(report: dict[str, Any]) -> bool: return bool( report["present"] and report["schema_ok"] and report.get("safe_to_share") is True and report.get("missing_artifacts") == [] and report.get("submission_gate_ok") is True ) def first_failed(gates: list[dict[str, Any]]) -> dict[str, Any] | None: return next((gate for gate in gates if not gate["ok"]), None) def build_report(root: Path = ROOT) -> dict[str, Any]: root = root.resolve() statuses = task_statuses(root) reports = { report_id: report_status(root, report_id, relative_path, schema) for report_id, (relative_path, schema) in REPORTS.items() } final_control = reports["final_control"] credential_handoff = reports["credential_handoff"] b2_evidence = reports["b2_evidence"] final_evidence = reports["final_evidence"] public_video = reports["public_video"] video_publish_kit = reports["video_publish_kit"] devpost_checklist = reports["devpost_checklist"] devpost_receipt = reports["devpost_receipt"] secret_scan = reports["secret_scan"] submission_audit = reports["submission_audit"] submission_bundle = reports["submission_bundle"] public_space_sync = reports["public_space_sync"] report_inventory_ok = all( report["present"] and report["schema_ok"] for report_id, report in reports.items() if report_id not in {"b2_evidence", "final_evidence"} ) expected_pre_live_missing_evidence = not b2_evidence["present"] and not final_evidence["present"] gates = [ gate_item( "report_inventory", "Required closeout reports are present and schema-valid", report_inventory_ok, "All standing control reports are present; live evidence files may be absent before credentials." if report_inventory_ok else "One or more standing reports are missing or schema-invalid.", "docs/assets/*.json", ), gate_item( "ci_and_public_demo", "Public demo evidence is synced", bool(public_space_sync["present"] and public_space_sync["schema_ok"] and public_space_sync["ok"] is True), f"Public Space sync mode is {public_space_sync.get('mode')}; ok is {public_space_sync.get('ok')}.", public_space_sync["path"], ), gate_item( "credential_handoff", "Live credential handoff is ready", credential_handoff_ready(credential_handoff), credential_handoff_detail(credential_handoff), credential_handoff["path"], ), gate_item( "b2_live_proof", "Backblaze B2 live proof is captured", bool(b2_live_evidence_ok(b2_evidence) and statuses.get("T020") == "done"), f"T020 is {statuses.get('T020', 'missing')}; evidence present is {b2_evidence['present']}.", b2_evidence["path"], ), gate_item( "genblaze_live_proof", "Genblaze live proof is captured", bool(final_live_evidence_ok(final_evidence) and statuses.get("T021") == "done"), f"T021 is {statuses.get('T021', 'missing')}; final evidence present is {final_evidence['present']}.", final_evidence["path"], ), gate_item( "public_video", "Final public video URL is verified", bool(public_video.get("safe_to_submit") is True and video_publish_kit.get("final_video_ready") is True), ( f"Public video mode is {public_video.get('mode')}; " f"video kit final_video_ready is {video_publish_kit.get('final_video_ready')}." ), public_video["path"], ), gate_item( "devpost_ready", "Devpost checklist is final-ready", bool(devpost_checklist.get("safe_to_submit") is True), f"Devpost checklist mode is {devpost_checklist.get('mode')}.", devpost_checklist["path"], ), gate_item( "final_secret_scan", "Final secret scan is clear after live artifacts", bool(secret_scan.get("ok") is True and secret_scan.get("mode") == "clear" and statuses.get("T041A") == "done"), f"T041A is {statuses.get('T041A', 'missing')}; secret scan mode is {secret_scan.get('mode')}.", secret_scan["path"], ), gate_item( "final_submission_audit", "Final submission audit is complete", bool(submission_audit.get("ok") is True and statuses.get("T041") == "done"), f"T041 is {statuses.get('T041', 'missing')}; audit mode is {submission_audit.get('mode')}.", submission_audit["path"], ), gate_item( "devpost_receipt", "Devpost submitted receipt is captured", bool(devpost_receipt.get("ok") is True and statuses.get("T042") == "done"), f"T042 is {statuses.get('T042', 'missing')}; receipt mode is {devpost_receipt.get('mode')}.", devpost_receipt["path"], ), gate_item( "final_bundle", "Final submission bundle inputs are ready", submission_bundle_inputs_ready(submission_bundle), ( f"Bundle safe_to_share is {submission_bundle.get('safe_to_share')}; " f"submission_gate_ok is {submission_bundle.get('submission_gate_ok')}; " f"missing_artifacts is {submission_bundle.get('missing_artifacts')}." ), submission_bundle["path"], ), gate_item( "final_control", "Final control gate is green", bool(final_control.get("safe_to_submit") is True), f"Final control mode is {final_control.get('mode')}; safe_to_submit is {final_control.get('safe_to_submit')}.", final_control["path"], ), ] missing_required_tasks = [task_id for task_id in REQUIRED_TASKS if task_id not in statuses] unexpected_findings: list[dict[str, str]] = [] if missing_required_tasks: unexpected_findings.append( { "id": "missing_required_tasks", "detail": "Missing task ids: " + ", ".join(missing_required_tasks), } ) if not report_inventory_ok: missing_reports = [ report_id for report_id, report in reports.items() if report_id not in {"b2_evidence", "final_evidence"} and not (report["present"] and report["schema_ok"]) ] unexpected_findings.append( { "id": "report_inventory", "detail": "Missing or schema-invalid reports: " + ", ".join(missing_reports), } ) blocker = first_failed(gates) safe_to_submit = all(gate["ok"] for gate in gates) if safe_to_submit: mode = "final_closeout_ready" phase = "submit_receipt_captured" next_command = "python scripts/submission_bundle.py --strict-final" next_detail = "All closeout gates are green; build the strict final bundle and preserve the Devpost receipt." elif unexpected_findings: mode = "closeout_needs_repair" phase = "repair_reports" next_command = "python scripts/final_submission_control.py" next_detail = "Repair missing or schema-invalid control reports before continuing." elif expected_pre_live_missing_evidence and blocker and blocker["id"] == "credential_handoff": mode = "waiting_for_credentials" phase = "credential_entry" next_command = "python scripts/final_env_wizard.py --output .env.final.local --missing-only --force" next_detail = "Enter live credentials locally; do not paste secrets into chat, docs, screenshots, or git." elif blocker: mode = "closeout_blocked" phase = blocker["id"] next_command = next_command_for(blocker["id"]) next_detail = blocker["detail"] else: mode = "closeout_blocked" phase = "unknown" next_command = "python scripts/final_submission_control.py" next_detail = "Closeout state could not be classified." closeout_health_ok = report_inventory_ok and not unexpected_findings return { "schema": SCHEMA, "created_at": utc_now(), "ok": closeout_health_ok, "mode": mode, "phase": phase, "safe_to_submit": safe_to_submit, "closeout_health_ok": closeout_health_ok, "task_statuses": {task_id: statuses.get(task_id, "missing") for task_id in REQUIRED_TASKS}, "reports": reports, "gates": gates, "unexpected_findings": unexpected_findings, "next_command": next_command, "next_detail": next_detail, "secret_policy": ( "This closeout report stores only task statuses, report metadata, public URLs, and artifact paths; " "it never stores Backblaze keys, Genblaze provider keys, Devpost cookies, browser sessions, or signed URLs." ), } def next_command_for(gate_id: str) -> str: commands = { "report_inventory": "python scripts/final_submission_control.py", "ci_and_public_demo": "python scripts/public_space_sync.py", "credential_handoff": "python scripts/live_env_handoff.py --env-file .env.final.local --strict", "b2_live_proof": "python scripts/run_b2_live_proof.py --env-file .env.final.local --evidence-out docs/assets/b2-live-proof-evidence.json", "genblaze_live_proof": ( "python scripts/run_final_live_proof.py --env-file .env.final.local " "--genblaze-provider local --genblaze-image-model local-svg-v1 " "--evidence-out docs/assets/final-live-proof-evidence.json" ), "public_video": 'python scripts/public_video_check.py --video-url "$PROOFFRAME_PUBLIC_VIDEO_URL" --verify-url --strict-final', "devpost_ready": "python scripts/devpost_submission_checklist.py --strict-final", "final_secret_scan": "python scripts/secret_scan.py", "final_submission_audit": "python scripts/submission_audit.py --strict-final", "devpost_receipt": 'python scripts/devpost_submission_receipt.py --project-url "$PROOFFRAME_DEVPOST_PROJECT_URL" --submitted-at "$PROOFFRAME_DEVPOST_SUBMITTED_AT" --confirmation-note "Devpost accepted/submitted the ProofFrame project."', "final_bundle": "python scripts/submission_bundle.py --strict-final", "final_control": "python scripts/final_submission_control.py --strict-final", } return commands.get(gate_id, "python scripts/final_submission_control.py") def render_markdown(report: dict[str, Any]) -> str: lines = [ "# ProofFrame Final Closeout Status", "", f"Mode: `{report['mode']}`", f"Phase: `{report['phase']}`", f"Closeout health OK: `{str(report['closeout_health_ok']).lower()}`", f"Safe to submit: `{str(report['safe_to_submit']).lower()}`", f"Next command: `{report['next_command']}`", "", report["secret_policy"], "", "## Gates", "", "| Status | Gate | Detail | Evidence |", "| --- | --- | --- | --- |", ] for gate in report["gates"]: status = "OK" if gate["ok"] else "BLOCKED" lines.append(f"| {status} | `{gate['id']}` | {gate['detail']} | `{gate['evidence']}` |") lines.extend(["", "## Task Statuses", ""]) for task_id, status in report["task_statuses"].items(): lines.append(f"- `{task_id}`: `{status}`") lines.extend(["", "## Unexpected Findings", ""]) if report["unexpected_findings"]: lines.extend(f"- `{item['id']}`: {item['detail']}" for item in report["unexpected_findings"]) else: lines.append("- None") lines.extend(["", "## Next Detail", "", report["next_detail"], ""]) return "\n".join(lines) 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 the ProofFrame final closeout status 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="Exit nonzero unless safe_to_submit is true.") return parser def main() -> None: args = build_parser().parse_args() report = build_report(root=args.root) write_outputs(report, args.json_out, args.markdown_out) print( json.dumps( { "ok": report["ok"], "mode": report["mode"], "phase": report["phase"], "safe_to_submit": report["safe_to_submit"], "json": str(args.json_out), "markdown": str(args.markdown_out), "next_command": report["next_command"], "unexpected_findings": len(report["unexpected_findings"]), }, indent=2, ) ) if args.strict_final and not report["safe_to_submit"]: raise SystemExit(2) if not report["ok"]: raise SystemExit(1) if __name__ == "__main__": main()