backblaze-proofframe / scripts /submission_audit.py
ADJCJH's picture
Sync post-live local Genblaze proof evidence
74ee21a verified
Raw
History Blame Contribute Delete
21.2 kB
#!/usr/bin/env python3
"""Fail-closed audit for ProofFrame's final Devpost submission package."""
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.submission_audit.v1"
DEFAULT_JSON = ROOT / "docs" / "assets" / "submission-audit-report.json"
DEFAULT_MD = ROOT / "docs" / "assets" / "submission-audit-report.md"
REQUIRED_PRE_SUBMIT_TASKS = ["T020", "T021", "T040", "T041A"]
OBSERVER_TASKS = ["T041", "T042"]
EXPECTED_FINAL_CONTROL_BLOCKERS = {"final_submission_audit", "devpost_submitted"}
REQUIRED_PUBLIC_FILES = [
"README.md",
"docs/prd.md",
"docs/spec.md",
"docs/devpost_draft.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/devpost-submission-checklist.json",
"docs/assets/devpost-submission-checklist.md",
"docs/assets/judge-brief.json",
"docs/assets/judge-brief.md",
"docs/assets/judge-crosswalk.json",
"docs/assets/judge-crosswalk.md",
"docs/assets/devpost-event-snapshot.json",
"docs/assets/devpost-event-snapshot.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/demo-readiness-report.json",
"docs/assets/demo-readiness-report.md",
"docs/assets/recording-assets.json",
"docs/assets/recording-assets.md",
"docs/assets/final-submission-control.json",
"docs/assets/final-submission-control.md",
"docs/assets/final-operator-brief.json",
"docs/assets/final-operator-brief.md",
"docs/assets/b2-key-scope-checklist.json",
"docs/assets/b2-key-scope-checklist.md",
"docs/assets/final-rehearsal-checklist.json",
"docs/assets/final-rehearsal-checklist.md",
"docs/assets/docker-smoke-report.json",
"docs/assets/docker-smoke-report.md",
"docs/evidence_package.md",
"docs/demo_script.md",
"docs/public_claim_freeze.md",
"docs/verification.md",
]
REQUIRED_SCREENSHOTS = [
"docs/assets/proofframe-local-ui-smoke.png",
"docs/assets/proofframe-review-console-smoke.png",
"docs/assets/proofframe-hf-public-smoke.png",
]
REQUIRED_REPORT_SCHEMAS = {
"docs/assets/devpost-form-kit.json": "proofframe.devpost_form_kit.v1",
"docs/assets/devpost-submission-checklist.json": "proofframe.devpost_submission_checklist.v1",
"docs/assets/judge-brief.json": "proofframe.judge_brief.v1",
"docs/assets/judge-crosswalk.json": "proofframe.judge_crosswalk.v1",
"docs/assets/devpost-event-snapshot.json": "proofframe.devpost_event_snapshot.v1",
"docs/assets/demo-storyboard.json": "proofframe.demo_storyboard.v1",
"docs/assets/public-video-check.json": "proofframe.public_video_check.v1",
"docs/assets/demo-readiness-report.json": "proofframe.demo_readiness.v1",
"docs/assets/recording-assets.json": "proofframe.recording_assets.v1",
"docs/assets/sponsor-fit-audit.json": "proofframe.sponsor_fit_audit.v1",
"docs/assets/award-readiness-report.json": "proofframe.award_readiness.v1",
"docs/assets/secret-scan-report.json": "proofframe.secret_scan.v1",
"docs/assets/final-submission-control.json": "proofframe.final_submission_control.v1",
"docs/assets/final-operator-brief.json": "proofframe.final_operator_brief.v1",
"docs/assets/b2-key-scope-checklist.json": "proofframe.b2_key_scope_checklist.v1",
"docs/assets/final-rehearsal-checklist.json": "proofframe.final_rehearsal.v1",
"docs/assets/docker-smoke-report.json": "proofframe.docker_smoke.v1",
"docs/assets/devpost-submission-receipt.json": "proofframe.devpost_submission_receipt.v1",
}
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:
return json.loads(path.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
return None
def relative_path(path: Path, root: Path) -> str:
try:
return str(path.resolve().relative_to(root.resolve()))
except ValueError:
return str(path)
def task_map(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", [])}
def task_statuses(tasks_data: dict[str, Any] | None, task_ids: list[str]) -> dict[str, str]:
tasks = task_map(tasks_data)
return {task_id: str(tasks.get(task_id, {}).get("status", "missing")) for task_id in task_ids}
def finding(gate: str, status: str, detail: str, evidence: str) -> dict[str, str]:
return {"gate": gate, "status": status, "detail": detail, "evidence": evidence}
def check_tasks(tasks_data: dict[str, Any] | None) -> list[dict[str, str]]:
tasks = task_map(tasks_data)
findings: list[dict[str, str]] = []
for task_id in REQUIRED_PRE_SUBMIT_TASKS:
task = tasks.get(task_id)
if not task:
findings.append(finding(task_id, "missing", "Task is absent.", "tasks.json"))
elif task.get("status") != "done":
findings.append(
finding(
task_id,
str(task.get("status")),
f"{task.get('title', task_id)} is not done.",
"tasks.json",
)
)
return findings
def check_files(paths: list[Path], root: Path) -> list[dict[str, str]]:
findings: list[dict[str, str]] = []
for path in paths:
if not path.exists():
findings.append(
finding(
relative_path(path, root),
"missing",
"Required public submission artifact is missing.",
relative_path(path, root),
)
)
return findings
def check_report_schemas(root: Path) -> list[dict[str, str]]:
findings: list[dict[str, str]] = []
for relative, expected_schema in REQUIRED_REPORT_SCHEMAS.items():
report = load_json(root / relative)
if report is None:
findings.append(finding(relative, "missing", "Report is missing or invalid JSON.", relative))
continue
actual = report.get("schema")
if actual != expected_schema:
findings.append(
finding(
f"{relative}.schema",
"mismatch",
f"Expected schema {expected_schema!r}, got {actual!r}.",
relative,
)
)
return findings
def check_final_evidence(path: Path, root: Path) -> list[dict[str, str]]:
evidence = load_json(path)
if evidence is None:
return [
finding(
"final-live-proof-evidence",
"missing",
f"{relative_path(path, root)} is missing or invalid JSON.",
relative_path(path, root),
)
]
findings: list[dict[str, str]] = []
expected = {
"ok": True,
"storage_backend": "b2",
"generation_backend": "genblaze",
"asset_storage_backend": "b2",
}
for key, expected_value in expected.items():
actual = evidence.get(key)
if actual != expected_value:
findings.append(
finding(
f"evidence.{key}",
"mismatch",
f"Expected {expected_value!r}, got {actual!r}.",
relative_path(path, root),
)
)
allowed_asset_providers = {
"genblaze/gmicloud-image",
"genblaze/openai-image",
"genblaze/local-image",
}
asset_provider = evidence.get("asset_provider")
if asset_provider not in allowed_asset_providers:
findings.append(
finding(
"evidence.asset_provider",
"mismatch",
f"Expected one of {sorted(allowed_asset_providers)!r}, got {asset_provider!r}.",
relative_path(path, root),
)
)
for key in ("asset_sha256", "manifest_sha256", "asset_storage_key", "manifest_key"):
if not evidence.get(key):
findings.append(
finding(
f"evidence.{key}",
"missing",
"Final evidence is missing this field.",
relative_path(path, root),
)
)
return findings
def usable_video_url(value: Any) -> bool:
text = str(value or "").strip()
return text.startswith(("https://", "http://")) and not text.lower().startswith("tbd")
def check_devpost_packet(root: Path) -> list[dict[str, str]]:
relative = "docs/assets/devpost-submission-packet.json"
packet = load_json(root / relative)
if packet is None:
return [finding("devpost_packet", "missing", "Devpost packet is missing or invalid JSON.", relative)]
findings: list[dict[str, str]] = []
if packet.get("schema") != "proofframe.devpost_packet.v1":
findings.append(
finding(
"devpost_packet.schema",
"mismatch",
f"Expected 'proofframe.devpost_packet.v1', got {packet.get('schema')!r}.",
relative,
)
)
if packet.get("mode") != "post_live_verified":
findings.append(
finding(
"devpost_packet.mode",
"mismatch",
f"Expected 'post_live_verified', got {packet.get('mode')!r}.",
relative,
)
)
if not usable_video_url(packet.get("video_url")):
findings.append(
finding(
"devpost_packet.video_url",
"missing",
"Final packet must include a public http(s) demo video URL, not a placeholder.",
relative,
)
)
checklist = {
str(item.get("task", "")).upper(): str(item.get("status", "missing"))
for item in packet.get("submission_checklist", [])
}
for task_id in REQUIRED_PRE_SUBMIT_TASKS:
if checklist.get(task_id) != "done":
findings.append(
finding(
f"devpost_packet.{task_id}",
checklist.get(task_id, "missing"),
"Devpost packet checklist is not synchronized with required pre-submit task status.",
relative,
)
)
return findings
def check_event_snapshot(root: Path) -> list[dict[str, str]]:
relative = "docs/assets/devpost-event-snapshot.json"
report = load_json(root / relative)
if report is None:
return [finding("devpost_event_snapshot", "missing", "Event snapshot is missing.", relative)]
validation = report.get("validation") or {}
findings: list[dict[str, str]] = []
if not validation.get("ok"):
findings.append(finding("devpost_event_snapshot.validation", "incomplete", "Snapshot validation is not ok.", relative))
if not validation.get("submission_open"):
findings.append(finding("devpost_event_snapshot.submission_open", "closed", "Devpost submission is not marked open.", relative))
return findings
def check_final_reports(root: Path) -> list[dict[str, str]]:
checks = [
(
"docs/assets/devpost-form-kit.json",
"devpost_form_kit.final_form_ready",
"final_form_ready",
True,
"Final Devpost form kit is not ready.",
),
(
"docs/assets/devpost-submission-checklist.json",
"devpost_submission_checklist.safe_to_submit",
"safe_to_submit",
True,
"Final Devpost submission checklist is not ready.",
),
(
"docs/assets/demo-storyboard.json",
"demo_storyboard.public_video_ready",
"public_video_ready",
True,
"Storyboard does not have a public video URL ready.",
),
(
"docs/assets/public-video-check.json",
"public_video_check.safe_to_submit",
"safe_to_submit",
True,
"Public demo video URL is not verified.",
),
(
"docs/assets/demo-readiness-report.json",
"demo_readiness.final_recording_ready",
"final_recording_ready",
True,
"Final recording readiness gate is not ready.",
),
(
"docs/assets/recording-assets.json",
"recording_assets.final_video_ready",
"final_video_ready",
True,
"Recording assets do not verify the final public video.",
),
(
"docs/assets/live-credential-handoff.json",
"live_credential_handoff.ready_for_live_proof",
"ready_for_live_proof",
True,
"Live credential handoff is not ready.",
),
(
"docs/assets/secret-scan-report.json",
"secret_scan.ok",
"ok",
True,
"Secret scan report is not clean.",
),
]
findings: list[dict[str, str]] = []
for relative, gate, key, expected, detail in checks:
report = load_json(root / relative)
if report is None:
findings.append(finding(gate, "missing", "Report is missing or invalid JSON.", relative))
continue
if report.get(key) != expected:
findings.append(finding(gate, "incomplete", detail, relative))
award = load_json(root / "docs/assets/award-readiness-report.json")
if award is None:
findings.append(finding("award_readiness", "missing", "Award readiness report is missing.", "docs/assets/award-readiness-report.json"))
elif int(award.get("score") or 0) < 75:
findings.append(
finding(
"award_readiness.score",
"low",
f"Award readiness score is {award.get('score')}/{award.get('max_score')}.",
"docs/assets/award-readiness-report.json",
)
)
return findings
def check_final_control(root: Path) -> list[dict[str, str]]:
relative = "docs/assets/final-submission-control.json"
report = load_json(root / relative)
if report is None:
return [finding("final_submission_control", "missing", "Final control report is missing.", relative)]
blocker_ids = {str(item.get("id", "")) for item in report.get("blocking_items", [])}
unexpected = sorted(blocker_ids - EXPECTED_FINAL_CONTROL_BLOCKERS)
if unexpected:
return [
finding(
"final_submission_control.blocking_items",
"blocked",
"Unexpected blockers remain before audit sign-off: " + ", ".join(unexpected),
relative,
)
]
return []
def build_audit_report(
tasks_path: Path,
final_evidence_path: Path,
*,
root: Path = ROOT,
) -> dict[str, Any]:
root = root.resolve()
tasks_data = load_json(tasks_path)
required_files = [root / path for path in REQUIRED_PUBLIC_FILES + REQUIRED_SCREENSHOTS]
sections = {
"tasks": check_tasks(tasks_data),
"files": check_files(required_files, root),
"schemas": check_report_schemas(root),
"final_evidence": check_final_evidence(final_evidence_path, root),
"devpost_packet": check_devpost_packet(root),
"event_snapshot": check_event_snapshot(root),
"final_reports": check_final_reports(root),
"final_control": check_final_control(root),
}
findings = [item for values in sections.values() for item in values]
ok = not findings
return {
"schema": SCHEMA,
"created_at": utc_now(),
"mode": "pre_submit_audit_ready" if ok else "pre_submit_audit_blocked",
"ok": ok,
"root": ".",
"required_pre_submit_tasks": REQUIRED_PRE_SUBMIT_TASKS,
"observer_tasks": OBSERVER_TASKS,
"task_statuses": task_statuses(tasks_data, REQUIRED_PRE_SUBMIT_TASKS + OBSERVER_TASKS),
"final_evidence_path": relative_path(final_evidence_path, root),
"section_counts": {name: len(values) for name, values in sections.items()},
"findings": findings,
"next_commands": [
"python scripts/run_b2_live_proof.py --env-file .env.final.local --evidence-out docs/assets/b2-live-proof-evidence.json",
"python scripts/run_final_live_proof.py --env-file .env.final.local --evidence-out docs/assets/final-live-proof-evidence.json",
"python scripts/demo_storyboard.py --strict-final",
'python scripts/public_video_check.py --video-url "$PROOFFRAME_PUBLIC_VIDEO_URL" --verify-url --strict-final',
"python scripts/demo_readiness.py --strict-final",
"python scripts/recording_assets.py --verify-public --strict-final",
"python scripts/secret_scan.py",
'python scripts/devpost_packet.py --post-live --video-url "$PROOFFRAME_PUBLIC_VIDEO_URL"',
"python scripts/devpost_form_kit.py --strict-final",
"python scripts/devpost_submission_checklist.py --strict-final",
"python scripts/final_submission_control.py --strict-final",
"python scripts/submission_audit.py --strict-final",
'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."',
],
"signoff_instruction": (
"When this report is ok, mark T041 done, submit Devpost, generate the public submission receipt, then mark T042 done and rerun final_submission_control.py --strict-final."
),
}
def render_markdown(report: dict[str, Any]) -> str:
lines = [
"# ProofFrame Submission Audit",
"",
f"Mode: `{report['mode']}`",
f"OK: `{str(report['ok']).lower()}`",
f"Created: `{report['created_at']}`",
f"Final evidence: `{report['final_evidence_path']}`",
"",
"## Task Statuses",
"",
]
for task_id, status in report["task_statuses"].items():
lines.append(f"- {task_id}: `{status}`")
lines.extend(["", "## Section Findings", ""])
for section, count in report["section_counts"].items():
status = "OK" if count == 0 else f"{count} finding(s)"
lines.append(f"- {section}: {status}")
lines.extend(["", "## Findings", ""])
if report["findings"]:
for item in report["findings"]:
lines.append(
f"- `{item['gate']}` [{item['status']}]: {item['detail']} Evidence: `{item['evidence']}`"
)
else:
lines.append("- None")
lines.extend(["", "## Next Commands", ""])
lines.extend(f"- `{command}`" for command in report["next_commands"])
lines.extend(["", "## Signoff", "", report["signoff_instruction"]])
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="Audit final Devpost submission readiness.")
parser.add_argument("--tasks", type=Path, default=ROOT / "tasks.json")
parser.add_argument(
"--final-evidence",
type=Path,
default=ROOT / "docs" / "assets" / "final-live-proof-evidence.json",
)
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 the pre-submit audit is fully ready.",
)
return parser
def main() -> None:
args = build_parser().parse_args()
root = args.tasks.resolve().parents[0]
if args.tasks.name != "tasks.json":
root = ROOT
report = build_audit_report(args.tasks, args.final_evidence, root=root)
write_outputs(report, args.json_out, args.markdown_out)
print(
json.dumps(
{
"ok": report["ok"],
"mode": report["mode"],
"json": str(args.json_out),
"markdown": str(args.markdown_out),
"findings": len(report["findings"]),
},
indent=2,
)
)
if args.strict_final and not report["ok"]:
raise SystemExit(2)
if __name__ == "__main__":
main()