"""Submission readiness gate for the browser demo and final audit flow.""" from __future__ import annotations import json import os from pathlib import Path from typing import Any REQUIRED_FINAL_TASKS = ["T020", "T021", "T040", "T041", "T041A", "T042"] REQUIRED_FINAL_REPORTS = [ { "id": "secret_scan", "task_id": "T041A", "path": "docs/assets/secret-scan-report.json", "schema": "proofframe.secret_scan.v1", "action": "Run the final secret scan and capture a clean report.", }, { "id": "submission_audit", "task_id": "T041", "path": "docs/assets/submission-audit-report.json", "schema": "proofframe.submission_audit.v1", "action": "Run the final submission audit and capture a passing report.", }, { "id": "devpost_submission_receipt", "task_id": "T042", "path": "docs/assets/devpost-submission-receipt.json", "schema": "proofframe.devpost_submission_receipt.v1", "action": "Capture the public-safe Devpost submission receipt after submitting.", }, ] FINAL_EVIDENCE_FIELDS: dict[str, Any] = { "ok": True, "storage_backend": "b2", "generation_backend": "genblaze", "asset_storage_backend": "b2", } FINAL_ALLOWED_ASSET_PROVIDERS = { "genblaze/gmicloud-image", "genblaze/openai-image", "genblaze/local-image", } FINAL_EVIDENCE_REQUIRED_VALUES = [ "asset_sha256", "manifest_sha256", "asset_storage_key", "manifest_key", ] def project_root() -> Path: explicit_root = os.environ.get("PROOFFRAME_PROJECT_ROOT", "").strip() candidates = [] if explicit_root: candidates.append(Path(explicit_root).expanduser()) candidates.extend([Path.cwd(), Path(__file__).resolve().parents[2]]) for candidate in candidates: resolved = candidate.resolve() if (resolved / "tasks.json").exists() or (resolved / "apps" / "web").exists(): return resolved return Path.cwd().resolve() 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 display_path(path: Path, root: Path | None = None) -> str: if root is not None: try: return str(path.resolve().relative_to(root.resolve())) except ValueError: pass return str(path) def task_lookup(tasks_data: dict[str, Any] | None) -> dict[str, dict[str, Any]]: if not tasks_data: return {} return { str(task.get("id", "")).upper(): task for task in tasks_data.get("tasks", []) if task.get("id") } def build_task_gates(tasks_data: dict[str, Any] | None) -> list[dict[str, Any]]: tasks = task_lookup(tasks_data) gates: list[dict[str, Any]] = [] for task_id in REQUIRED_FINAL_TASKS: task = tasks.get(task_id) status = str(task.get("status", "missing") if task else "missing") gates.append( { "id": task_id, "title": str(task.get("title", task_id) if task else task_id), "status": status, "ok": status == "done", "notes": str(task.get("notes", "") if task else ""), } ) return gates def build_evidence_gate(evidence_path: Path, root: Path | None = None) -> dict[str, Any]: evidence = load_json(evidence_path) findings: list[dict[str, str]] = [] if evidence is None: return { "ok": False, "status": "missing", "path": display_path(evidence_path, root), "findings": [ { "field": "final-live-proof-evidence", "detail": "Final B2 and Genblaze evidence file is missing or invalid JSON.", } ], } for key, expected in FINAL_EVIDENCE_FIELDS.items(): actual = evidence.get(key) if actual != expected: findings.append({"field": key, "detail": f"Expected {expected!r}, got {actual!r}."}) asset_provider = evidence.get("asset_provider") if asset_provider not in FINAL_ALLOWED_ASSET_PROVIDERS: findings.append( { "field": "asset_provider", "detail": ( "Expected one of " f"{sorted(FINAL_ALLOWED_ASSET_PROVIDERS)!r}, got {asset_provider!r}." ), } ) for key in FINAL_EVIDENCE_REQUIRED_VALUES: if not evidence.get(key): findings.append({"field": key, "detail": "Required evidence value is missing."}) return { "ok": not findings, "status": "verified" if not findings else "incomplete", "path": display_path(evidence_path, root), "findings": findings, } def build_packet_gate(root: Path) -> dict[str, Any]: packet_path = root / "docs" / "assets" / "devpost-submission-packet.json" packet = load_json(packet_path) if packet is None: return { "ok": False, "status": "missing", "path": display_path(packet_path, root), "mode": None, } mode = packet.get("mode") final_packet_ready = mode == "post_live_verified" if final_packet_ready: status = "post_live_packet_ready" elif mode == "pre_live_safe": status = "pre_live_packet_pending" else: status = "invalid_packet_mode" return { "ok": final_packet_ready, "status": status, "path": display_path(packet_path, root), "mode": mode, "claim_warning": packet.get("claim_warning"), } def build_report_gate(root: Path) -> dict[str, Any]: reports: list[dict[str, Any]] = [] findings: list[dict[str, str]] = [] for spec in REQUIRED_FINAL_REPORTS: path = root / spec["path"] report = load_json(path) record: dict[str, Any] = { "id": spec["id"], "task_id": spec["task_id"], "path": display_path(path, root), "required_schema": spec["schema"], "schema": None, "schema_ok": False, "ok": False, "status": "missing", "mode": None, "findings": [], } if report is None: detail = "Required final report is missing or invalid JSON." finding = {"report": spec["id"], "field": "file", "detail": detail} record["findings"].append(finding) findings.append(finding) reports.append(record) continue schema = report.get("schema") schema_ok = schema == spec["schema"] ok = report.get("ok") is True report_findings: list[dict[str, str]] = [] if not schema_ok: report_findings.append( { "report": spec["id"], "field": "schema", "detail": f"Expected {spec['schema']!r}, got {schema!r}.", } ) if not ok: report_findings.append( { "report": spec["id"], "field": "ok", "detail": "Final report must explicitly set ok=true.", } ) record.update( { "schema": schema, "schema_ok": schema_ok, "ok": schema_ok and ok, "status": "verified" if schema_ok and ok else "incomplete", "mode": report.get("mode"), "findings": report_findings, } ) reports.append(record) findings.extend(report_findings) if all(report["ok"] for report in reports): status = "verified" elif any(report["status"] == "missing" for report in reports): status = "missing" else: status = "incomplete" return { "ok": not findings, "status": status, "reports": reports, "findings": findings, } def next_actions( task_gates: list[dict[str, Any]], evidence_gate: dict[str, Any], packet_gate: dict[str, Any], report_gate: dict[str, Any], ) -> list[str]: actions: list[str] = [] for gate in task_gates: if gate["ok"]: continue if gate["id"] == "T020": actions.append("Run a live Backblaze B2 asset and manifest proof.") elif gate["id"] == "T021": actions.append("Run a live Genblaze generation proof and capture provider metadata.") elif gate["id"] == "T040": actions.append("Complete Devpost registration after user-confirmed account details.") elif gate["id"] == "T041A": actions.append("Run the final secret scan before recording or submitting.") elif gate["id"] == "T041": actions.append("Run the final submission audit after live proofs are captured.") elif gate["id"] == "T042": actions.append("Submit the Devpost project after every preceding gate is done.") if not evidence_gate["ok"] and "Capture final live proof evidence JSON." not in actions: actions.append("Capture final live proof evidence JSON.") if not packet_gate["ok"]: actions.append("Regenerate the Devpost packet in post-live mode with the public video URL.") for report in report_gate["reports"]: if report["ok"]: continue action = next( spec["action"] for spec in REQUIRED_FINAL_REPORTS if spec["id"] == report["id"] ) if action not in actions: actions.append(action) return actions[:5] def build_submission_gate(root: Path | None = None) -> dict[str, Any]: root = root or project_root() tasks_path = root / "tasks.json" final_evidence_path = root / "docs" / "assets" / "final-live-proof-evidence.json" tasks_data = load_json(tasks_path) task_gates = build_task_gates(tasks_data) evidence_gate = build_evidence_gate(final_evidence_path, root) packet_gate = build_packet_gate(root) report_gate = build_report_gate(root) required_done = sum(1 for gate in task_gates if gate["ok"]) blocked = sum(1 for gate in task_gates if gate["status"] == "blocked") doing = sum(1 for gate in task_gates if gate["status"] == "doing") todo = sum(1 for gate in task_gates if gate["status"] == "todo") missing = sum(1 for gate in task_gates if gate["status"] == "missing") ready = ( required_done == len(task_gates) and evidence_gate["ok"] and packet_gate["ok"] and report_gate["ok"] ) return { "ok": ready, "mode": "final_ready" if ready else "pre_live_safe", "tasks_path": display_path(tasks_path, root), "summary": { "required": len(task_gates), "done": required_done, "blocked": blocked, "doing": doing, "todo": todo, "missing": missing, }, "task_gates": task_gates, "evidence_gate": evidence_gate, "packet_gate": packet_gate, "report_gate": report_gate, "next_actions": next_actions(task_gates, evidence_gate, packet_gate, report_gate), }