File size: 26,394 Bytes
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9224d5
 
 
 
 
 
 
 
 
 
 
 
 
4e3dd84
 
 
 
 
 
a7a715a
 
 
 
 
 
e9224d5
 
 
 
 
 
 
 
 
 
 
 
2608b33
 
 
 
 
 
e9224d5
f74bce1
 
 
 
7098eeb
 
 
 
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a7f491e
 
 
6a909b2
 
 
f74bce1
 
 
36ebc46
 
 
7700633
 
 
0eb210c
 
 
7700633
 
 
2608b33
 
 
50a1c46
 
 
f74bce1
 
 
 
 
52b556c
 
 
 
5b93402
 
 
7700633
 
 
f74bce1
 
 
 
 
 
 
 
 
 
25cdc14
 
 
f74bce1
 
 
1a3dda6
 
 
7700633
 
 
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f1532d
 
 
6b7b99b
 
 
f74bce1
 
 
 
 
f54fafa
 
 
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9224d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9224d5
 
f74bce1
 
 
 
 
 
 
 
 
 
 
 
e9224d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f74bce1
 
 
 
 
e9224d5
f74bce1
 
 
 
 
 
 
e9224d5
f74bce1
 
 
 
 
 
 
 
 
 
e9224d5
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9224d5
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f54fafa
 
 
 
 
 
 
 
 
 
 
 
 
f74bce1
 
 
 
 
e9224d5
f74bce1
 
 
 
 
 
 
 
 
 
 
 
 
f54fafa
e9224d5
 
f74bce1
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
#!/usr/bin/env python3
"""Build a safe, judge-facing evidence bundle manifest for Devpost submission."""

from __future__ import annotations

import argparse
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from zipfile import ZIP_DEFLATED, ZipFile

from proofframe.submission_gate import build_submission_gate


ROOT = Path(__file__).resolve().parents[1]
DEFAULT_JSON = ROOT / "docs" / "assets" / "submission-bundle-manifest.json"
DEFAULT_MD = ROOT / "docs" / "assets" / "submission-bundle-manifest.md"
CLOSEOUT_REPORTS = [
    {
        "id": "secret_scan_after_receipt",
        "path": "docs/assets/secret-scan-report.json",
        "schema": "proofframe.secret_scan.v1",
        "ready_fields": {"ok": True, "mode": "clear"},
    },
    {
        "id": "final_submission_control_after_receipt",
        "path": "docs/assets/final-submission-control.json",
        "schema": "proofframe.final_submission_control.v1",
        "ready_fields": {"safe_to_submit": True, "mode": "final_submit_ready"},
    },
    {
        "id": "final_closeout_status_after_receipt",
        "path": "docs/assets/final-closeout-status.json",
        "schema": "proofframe.final_closeout_status.v1",
        "ready_fields": {"ok": True, "safe_to_submit": True, "mode": "final_closeout_ready"},
    },
    {
        "id": "final_video_publish_kit_after_receipt",
        "path": "docs/assets/final-video-publish-kit.json",
        "schema": "proofframe.final_video_publish_kit.v1",
        "ready_fields": {"ok": True, "safe_to_submit": True, "mode": "public_video_ready"},
    },
    {
        "id": "final_launch_plan_after_receipt",
        "path": "docs/assets/final-launch-plan.json",
        "schema": "proofframe.final_launch_plan.v1",
        "ready_fields": {"ok": True, "mode": "submitted"},
    },
    {
        "id": "devpost_preview_after_receipt",
        "path": "docs/assets/devpost-submission-preview.json",
        "schema": "proofframe.devpost_submission_preview.v1",
        "ready_fields": {"safe_to_submit": True, "mode": "final_preview_ready"},
    },
    {
        "id": "judge_decision_brief_after_receipt",
        "path": "docs/assets/judge-decision-brief.json",
        "schema": "proofframe.judge_decision_brief.v1",
        "ready_fields": {"ok": True, "safe_to_submit": True, "mode": "final_decision_ready"},
    },
]

ARTIFACTS = [
    ("repo_readme", "README.md", "Core public project overview"),
    ("dockerfile", "Dockerfile", "Docker deployment image contract"),
    ("dockerignore", ".dockerignore", "Docker build context secret exclusion policy"),
    ("docker_smoke_script", "scripts/docker_smoke.py", "Docker build and API smoke verifier"),
    ("docker_smoke_json", "docs/assets/docker-smoke-report.json", "Machine-readable Docker smoke report"),
    ("docker_smoke_md", "docs/assets/docker-smoke-report.md", "Human-readable Docker smoke report"),
    ("prd", "docs/prd.md", "Product requirements"),
    ("technical_spec", "docs/spec.md", "Implementation specification"),
    ("deployment_runbook", "docs/deployment.md", "Public demo deployment runbook"),
    ("devpost_draft", "docs/devpost_draft.md", "Copy-ready Devpost fields"),
    ("devpost_packet_json", "docs/assets/devpost-submission-packet.json", "Machine-readable Devpost copy"),
    ("devpost_packet_md", "docs/assets/devpost-submission-packet.md", "Human-readable Devpost copy"),
    ("devpost_event_snapshot_script", "scripts/devpost_event_snapshot.py", "Official Devpost event snapshot checker"),
    ("devpost_event_snapshot_json", "docs/assets/devpost-event-snapshot.json", "Machine-readable Devpost event snapshot"),
    ("devpost_event_snapshot_md", "docs/assets/devpost-event-snapshot.md", "Human-readable Devpost event snapshot"),
    ("agent_handoff_script", "scripts/agent_handoff_check.py", "Agent Bus and Codex handoff path checker"),
    ("agent_handoff_json", "docs/assets/agent-handoff-report.json", "Machine-readable Agent handoff report"),
    ("agent_handoff_md", "docs/assets/agent-handoff-report.md", "Human-readable Agent handoff report"),
    ("public_space_sync_script", "scripts/public_space_sync.py", "Public Hugging Face Space sync verifier"),
    ("public_space_sync_json", "docs/assets/public-space-sync-report.json", "Machine-readable public Space sync report"),
    ("public_space_sync_md", "docs/assets/public-space-sync-report.md", "Human-readable public Space sync report"),
    ("public_space_upload_script", "scripts/public_space_upload.py", "No-secret public Hugging Face Space upload helper"),
    ("public_space_upload_json", "docs/assets/public-space-upload-report.json", "Machine-readable public Space upload report"),
    ("public_space_upload_md", "docs/assets/public-space-upload-report.md", "Human-readable public Space upload report"),
    ("public_demo_screenshot_script", "scripts/public_demo_screenshot.py", "Public judge-mode screenshot verifier"),
    ("public_demo_screenshot_json", "docs/assets/public-demo-screenshot-report.json", "Machine-readable public screenshot report"),
    ("public_demo_screenshot_md", "docs/assets/public-demo-screenshot-report.md", "Human-readable public screenshot report"),
    ("devpost_form_kit_script", "scripts/devpost_form_kit.py", "Field-by-field Devpost form kit"),
    ("devpost_form_kit_json", "docs/assets/devpost-form-kit.json", "Machine-readable Devpost form kit"),
    ("devpost_form_kit_md", "docs/assets/devpost-form-kit.md", "Human-readable Devpost form kit"),
    ("devpost_preview_script", "scripts/devpost_submission_preview.py", "One-page claim-safe Devpost preview"),
    ("devpost_preview_json", "docs/assets/devpost-submission-preview.json", "Machine-readable Devpost preview"),
    ("devpost_preview_md", "docs/assets/devpost-submission-preview.md", "Human-readable Devpost preview"),
    ("devpost_submission_checklist_script", "scripts/devpost_submission_checklist.py", "No-secret final Devpost web submission checklist"),
    ("devpost_submission_checklist_json", "docs/assets/devpost-submission-checklist.json", "Machine-readable final Devpost submission checklist"),
    ("devpost_submission_checklist_md", "docs/assets/devpost-submission-checklist.md", "Human-readable final Devpost submission checklist"),
    ("judge_brief_script", "scripts/judge_brief.py", "Public-safe 30-second judge brief"),
    ("judge_brief_json", "docs/assets/judge-brief.json", "Machine-readable judge brief"),
    ("judge_brief_md", "docs/assets/judge-brief.md", "Human-readable judge brief"),
    ("judge_crosswalk_script", "scripts/judge_crosswalk.py", "Official criteria to evidence crosswalk"),
    ("judge_crosswalk_json", "docs/assets/judge-crosswalk.json", "Machine-readable judge crosswalk"),
    ("judge_crosswalk_md", "docs/assets/judge-crosswalk.md", "Human-readable judge crosswalk"),
    ("judge_decision_brief_script", "scripts/judge_decision_brief.py", "Public-safe judge decision brief builder"),
    ("judge_decision_brief_json", "docs/assets/judge-decision-brief.json", "Machine-readable judge decision brief"),
    ("judge_decision_brief_md", "docs/assets/judge-decision-brief.md", "Human-readable judge decision brief"),
    ("judge_evidence_index_script", "scripts/judge_evidence_index.py", "Public-safe judge evidence index builder"),
    ("judge_evidence_index_json", "docs/assets/judge-evidence-index.json", "Machine-readable judge evidence index"),
    ("judge_evidence_index_md", "docs/assets/judge-evidence-index.md", "Human-readable judge evidence index"),
    ("frontend_ui", "apps/web/index.html", "Browser proof ledger UI"),
    ("demo_script", "docs/demo_script.md", "Video shot list and narration"),
    ("demo_storyboard_script", "scripts/demo_storyboard.py", "Machine-checked demo video storyboard"),
    ("demo_storyboard_json", "docs/assets/demo-storyboard.json", "Machine-readable demo storyboard"),
    ("demo_storyboard_md", "docs/assets/demo-storyboard.md", "Human-readable demo storyboard"),
    ("demo_video_draft_script", "scripts/demo_video_draft.py", "Mock demo video draft builder"),
    ("demo_video_draft_json", "docs/assets/demo-video-draft.json", "Machine-readable demo video draft report"),
    ("demo_video_draft_md", "docs/assets/demo-video-draft.md", "Human-readable demo video draft report"),
    ("demo_video_draft_mp4", "docs/assets/proofframe-demo-draft.mp4", "Public-safe mock demo video draft"),
    ("final_video_publish_kit_script", "scripts/final_video_publish_kit.py", "Final public video upload copy and gate kit"),
    ("final_video_publish_kit_json", "docs/assets/final-video-publish-kit.json", "Machine-readable final video publish kit"),
    ("final_video_publish_kit_md", "docs/assets/final-video-publish-kit.md", "Human-readable final video publish kit"),
    ("public_video_check_script", "scripts/public_video_check.py", "Public demo video URL verifier"),
    ("public_video_check_json", "docs/assets/public-video-check.json", "Machine-readable public video check"),
    ("public_video_check_md", "docs/assets/public-video-check.md", "Human-readable public video check"),
    ("sponsor_fit_matrix", "docs/sponsor_fit_matrix.md", "Sponsor judging matrix and claim boundary"),
    ("sponsor_fit_audit_script", "scripts/sponsor_fit_audit.py", "Sponsor-fit clarity audit"),
    ("sponsor_fit_audit_json", "docs/assets/sponsor-fit-audit.json", "Machine-readable sponsor-fit audit"),
    ("sponsor_fit_audit_md", "docs/assets/sponsor-fit-audit.md", "Human-readable sponsor-fit audit"),
    ("award_readiness_script", "scripts/award_readiness.py", "Judge-facing award readiness scorecard"),
    ("award_readiness_json", "docs/assets/award-readiness-report.json", "Machine-readable award readiness report"),
    ("award_readiness_md", "docs/assets/award-readiness-report.md", "Human-readable award readiness report"),
    ("final_submission_control_script", "scripts/final_submission_control.py", "Final submission control tower"),
    ("final_submission_control_json", "docs/assets/final-submission-control.json", "Machine-readable final control report"),
    ("final_submission_control_md", "docs/assets/final-submission-control.md", "Human-readable final control report"),
    ("final_closeout_status_script", "scripts/final_closeout_status.py", "No-secret final closeout status report"),
    ("final_closeout_status_json", "docs/assets/final-closeout-status.json", "Machine-readable final closeout status"),
    ("final_closeout_status_md", "docs/assets/final-closeout-status.md", "Human-readable final closeout status"),
    ("final_operator_brief_script", "scripts/final_operator_brief.py", "No-secret final operator brief"),
    ("final_operator_brief_json", "docs/assets/final-operator-brief.json", "Machine-readable final operator brief"),
    ("final_operator_brief_md", "docs/assets/final-operator-brief.md", "Human-readable final operator brief"),
    ("final_launch_plan_script", "scripts/final_launch_plan.py", "No-secret final launch phase plan"),
    ("final_launch_plan_json", "docs/assets/final-launch-plan.json", "Machine-readable final launch plan"),
    ("final_launch_plan_md", "docs/assets/final-launch-plan.md", "Human-readable final launch plan"),
    ("final_rehearsal_script", "scripts/final_rehearsal.py", "No-secret final submission rehearsal checklist"),
    ("final_rehearsal_json", "docs/assets/final-rehearsal-checklist.json", "Machine-readable final rehearsal checklist"),
    ("final_rehearsal_md", "docs/assets/final-rehearsal-checklist.md", "Human-readable final rehearsal checklist"),
    ("submission_audit_script", "scripts/submission_audit.py", "Pre-submit audit gate"),
    ("submission_audit_json", "docs/assets/submission-audit-report.json", "Machine-readable submission audit report"),
    ("submission_audit_md", "docs/assets/submission-audit-report.md", "Human-readable submission audit report"),
    ("devpost_receipt_script", "scripts/devpost_submission_receipt.py", "Public-safe Devpost submission receipt generator"),
    ("devpost_receipt_json", "docs/assets/devpost-submission-receipt.json", "Machine-readable Devpost submission receipt"),
    ("devpost_receipt_md", "docs/assets/devpost-submission-receipt.md", "Human-readable Devpost submission receipt"),
    ("evidence_package", "docs/evidence_package.md", "Evidence package plan"),
    ("public_claim_freeze", "docs/public_claim_freeze.md", "Verified-claim boundary"),
    ("verification_runbook", "docs/verification.md", "Local, CI, Docker, and live proof runbook"),
    ("final_env_template", ".env.final.example", "Redacted final B2 plus Genblaze env template"),
    ("secret_scan_script", "scripts/secret_scan.py", "Final secret scanner"),
    ("secret_scan_json", "docs/assets/secret-scan-report.json", "Machine-readable secret scan report"),
    ("secret_scan_md", "docs/assets/secret-scan-report.md", "Human-readable secret scan report"),
    ("b2_live_setup_json", "docs/assets/b2-live-setup.json", "Non-secret B2 bucket setup record"),
    ("b2_live_setup_md", "docs/assets/b2-live-setup.md", "Human-readable B2 bucket setup record"),
    ("b2_key_scope_checklist_script", "scripts/b2_key_scope_checklist.py", "No-secret Backblaze B2 application key scope checklist"),
    ("b2_key_scope_checklist_json", "docs/assets/b2-key-scope-checklist.json", "Machine-readable B2 key scope checklist"),
    ("b2_key_scope_checklist_md", "docs/assets/b2-key-scope-checklist.md", "Human-readable B2 key scope checklist"),
    ("genblaze_contract_script", "scripts/genblaze_contract_check.py", "No-secret Genblaze/B2 SDK contract checker"),
    ("genblaze_contract_json", "docs/assets/genblaze-contract-report.json", "Machine-readable Genblaze SDK contract report"),
    ("genblaze_contract_md", "docs/assets/genblaze-contract-report.md", "Human-readable Genblaze SDK contract report"),
    ("b2_live_runner", "scripts/run_b2_live_proof.py", "One-command B2 storage proof runner"),
    ("live_env_handoff_script", "scripts/live_env_handoff.py", "Redacted live credential handoff gate"),
    ("live_env_handoff_json", "docs/assets/live-credential-handoff.json", "Machine-readable live credential handoff"),
    ("live_env_handoff_md", "docs/assets/live-credential-handoff.md", "Human-readable live credential handoff"),
    ("final_env_wizard", "scripts/final_env_wizard.py", "Local final credential env wizard"),
    ("post_credential_live_proof_script", "scripts/post_credential_live_proof.py", "Post-credential live proof sequence runner"),
    ("post_credential_live_proof_json", "docs/assets/post-credential-live-proof-plan.json", "Machine-readable post-credential live proof plan"),
    ("post_credential_live_proof_md", "docs/assets/post-credential-live-proof-plan.md", "Human-readable post-credential live proof plan"),
    ("public_claim_lint", "scripts/claim_lint.py", "Fail-closed public claim lint"),
    ("demo_readiness_script", "scripts/demo_readiness.py", "Demo recording readiness gate"),
    ("demo_readiness_json", "docs/assets/demo-readiness-report.json", "Machine-readable demo readiness report"),
    ("demo_readiness_md", "docs/assets/demo-readiness-report.md", "Human-readable demo readiness report"),
    ("recording_assets_script", "scripts/recording_assets.py", "Public-safe recording asset gate"),
    ("recording_assets_json", "docs/assets/recording-assets.json", "Machine-readable recording asset report"),
    ("recording_assets_md", "docs/assets/recording-assets.md", "Human-readable recording asset report"),
    ("final_live_runner", "scripts/run_final_live_proof.py", "One-command final B2 plus Genblaze proof runner"),
    ("submission_plan", "docs/submission.md", "Registration and submission plan"),
    ("task_ledger", "tasks.json", "Machine-readable task board"),
    ("local_ui_smoke", "docs/assets/proofframe-local-ui-smoke.png", "Local UI screenshot"),
    ("review_console_smoke", "docs/assets/proofframe-review-console-smoke.png", "Review console screenshot"),
    ("public_hf_smoke", "docs/assets/proofframe-hf-public-smoke.png", "Public mock demo screenshot"),
]


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 parse_utc(value: Any) -> datetime | None:
    if not isinstance(value, str) or not value.strip():
        return None
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return None
    if parsed.tzinfo is None:
        return None
    return parsed.astimezone(timezone.utc)


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def artifact_record(root: Path, artifact_id: str, relative_path: str, label: str) -> dict[str, Any]:
    path = root / relative_path
    record: dict[str, Any] = {
        "id": artifact_id,
        "path": relative_path,
        "label": label,
        "present": path.exists(),
    }
    if path.exists():
        record.update({"bytes": path.stat().st_size, "sha256": sha256_file(path)})
    return record


def load_packet_summary(root: Path) -> dict[str, Any]:
    packet_path = root / "docs" / "assets" / "devpost-submission-packet.json"
    packet = load_json(packet_path)
    if packet is None:
        return {"present": False, "mode": None, "claim_warning": "Devpost packet missing."}
    return {
        "present": True,
        "mode": packet.get("mode"),
        "project_name": packet.get("project_name"),
        "tagline": packet.get("tagline"),
        "repository_url": packet.get("repository_url"),
        "demo_url": packet.get("demo_url"),
        "claim_warning": packet.get("claim_warning"),
    }


def closeout_report_record(root: Path, spec: dict[str, Any], receipt_created_at: datetime | None) -> dict[str, Any]:
    data = load_json(root / spec["path"])
    record: dict[str, Any] = {
        "id": spec["id"],
        "path": spec["path"],
        "present": data is not None,
        "schema": data.get("schema") if data else None,
        "schema_ok": bool(data and data.get("schema") == spec["schema"]),
        "created_at": data.get("created_at") if data else None,
        "ready": False,
        "fresh_after_receipt": False,
        "findings": [],
    }
    if data is None:
        record["findings"].append("Report is missing or invalid JSON.")
        return record
    for key, expected in spec["ready_fields"].items():
        actual = data.get(key)
        if actual != expected:
            record["findings"].append(f"Expected {key}={expected!r}, got {actual!r}.")
    report_created_at = parse_utc(data.get("created_at"))
    if report_created_at is None:
        record["findings"].append("created_at is missing or invalid.")
    elif receipt_created_at is None:
        record["findings"].append("Receipt created_at is missing or invalid.")
    else:
        record["fresh_after_receipt"] = report_created_at >= receipt_created_at
        if not record["fresh_after_receipt"]:
            record["findings"].append("Report was generated before the Devpost receipt.")
    record["ready"] = bool(record["schema_ok"] and not record["findings"])
    return record


def build_closeout_gate(root: Path) -> dict[str, Any]:
    receipt_path = "docs/assets/devpost-submission-receipt.json"
    receipt = load_json(root / receipt_path)
    receipt_created_at = parse_utc(receipt.get("created_at") if receipt else None)
    receipt_record = {
        "path": receipt_path,
        "present": receipt is not None,
        "schema": receipt.get("schema") if receipt else None,
        "schema_ok": bool(receipt and receipt.get("schema") == "proofframe.devpost_submission_receipt.v1"),
        "ok": receipt.get("ok") if receipt else None,
        "mode": receipt.get("mode") if receipt else None,
        "created_at": receipt.get("created_at") if receipt else None,
    }
    reports = [closeout_report_record(root, spec, receipt_created_at) for spec in CLOSEOUT_REPORTS]
    receipt_ok = bool(
        receipt_record["present"]
        and receipt_record["schema_ok"]
        and receipt_record["ok"] is True
        and receipt_record["mode"] == "submitted"
        and receipt_created_at is not None
    )
    return {
        "ok": bool(receipt_ok and all(report["ready"] for report in reports)),
        "status": "verified" if receipt_ok and all(report["ready"] for report in reports) else "incomplete",
        "receipt": receipt_record,
        "reports": reports,
    }


def build_manifest(root: Path = ROOT) -> dict[str, Any]:
    root = root.resolve()
    artifacts = [artifact_record(root, *artifact) for artifact in ARTIFACTS]
    missing = [artifact for artifact in artifacts if not artifact["present"]]
    gate = build_submission_gate(root)
    closeout_gate = build_closeout_gate(root)
    return {
        "schema": "proofframe.submission_bundle.v1",
        "created_at": utc_now(),
        "project": "ProofFrame",
        "repository_url": "https://github.com/adjcjh777/backblaze-proofframe",
        "public_demo_url": "https://adjcjh-backblaze-proofframe.hf.space/?judge=1",
        "safe_to_share": not missing,
        "safe_to_submit": bool(gate["ok"] and closeout_gate["ok"]),
        "devpost_packet": load_packet_summary(root),
        "submission_gate": {
            "ok": gate["ok"],
            "mode": gate["mode"],
            "summary": gate["summary"],
            "packet_gate": gate["packet_gate"],
            "evidence_gate": gate["evidence_gate"],
            "report_gate": gate["report_gate"],
            "next_actions": gate["next_actions"],
        },
        "closeout_gate": closeout_gate,
        "artifacts": artifacts,
        "missing_artifacts": [artifact["path"] for artifact in missing],
    }


def render_markdown(manifest: dict[str, Any]) -> str:
    gate = manifest["submission_gate"]
    packet = manifest["devpost_packet"]
    lines = [
        "# ProofFrame Submission Bundle",
        "",
        f"Created: `{manifest['created_at']}`",
        f"Repository: {manifest['repository_url']}",
        f"Public mock demo: {manifest['public_demo_url']}",
        f"Devpost packet mode: `{packet.get('mode')}`",
        f"Final gate mode: `{gate['mode']}`",
        f"Final gate ready: `{str(gate['ok']).lower()}`",
        "",
        "## Current Gate",
        "",
        (
            f"- Tasks: {gate['summary']['done']} done / {gate['summary']['required']} required; "
            f"{gate['summary']['doing']} doing, {gate['summary']['blocked']} blocked, "
            f"{gate['summary']['todo']} todo."
        ),
        f"- Live evidence: `{gate['evidence_gate']['status']}`",
        f"- Devpost copy packet: `{gate['packet_gate']['status']}`",
        f"- Final reports: `{gate['report_gate']['status']}`",
        f"- Post-Devpost closeout: `{manifest['closeout_gate']['status']}`",
        "",
        "## Next Actions",
        "",
    ]
    if gate["next_actions"]:
        lines.extend(f"- {action}" for action in gate["next_actions"])
    else:
        lines.append("- Ready for final Devpost submit.")

    lines.extend(["", "## Artifacts", ""])
    for artifact in manifest["artifacts"]:
        if artifact["present"]:
            lines.append(
                f"- `{artifact['path']}` ({artifact['bytes']} bytes, sha256 `{artifact['sha256'][:16]}...`) - {artifact['label']}"
            )
        else:
            lines.append(f"- MISSING `{artifact['path']}` - {artifact['label']}")
    lines.append("")
    lines.append("No credentials, browser cookies, signed URLs, or raw provider keys are included.")
    return "\n".join(lines) + "\n"


def write_outputs(
    manifest: dict[str, Any],
    *,
    json_path: Path = DEFAULT_JSON,
    markdown_path: Path = DEFAULT_MD,
    zip_path: Path | None = None,
    root: Path = ROOT,
) -> 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(manifest, indent=2) + "\n", encoding="utf-8")
    markdown_path.write_text(render_markdown(manifest), encoding="utf-8")
    if zip_path:
        zip_path.parent.mkdir(parents=True, exist_ok=True)
        with ZipFile(zip_path, "w", ZIP_DEFLATED) as archive:
            archive.writestr("submission-bundle-manifest.json", json.dumps(manifest, indent=2) + "\n")
            archive.writestr("submission-bundle-manifest.md", render_markdown(manifest))
            for artifact in manifest["artifacts"]:
                if artifact["present"]:
                    archive.write(root / artifact["path"], artifact["path"])


def cli_summary(manifest: dict[str, Any], args: argparse.Namespace) -> dict[str, Any]:
    return {
        "ok": manifest["safe_to_submit"],
        "mode": manifest["submission_gate"]["mode"],
        "safe_to_share": manifest["safe_to_share"],
        "safe_to_submit": manifest["safe_to_submit"],
        "json": str(args.json_out),
        "markdown": str(args.markdown_out),
        "zip": str(args.zip_out) if args.zip_out else None,
        "missing_artifacts": manifest["missing_artifacts"],
    }


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Build a safe ProofFrame submission bundle manifest.")
    parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON)
    parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD)
    parser.add_argument("--zip-out", type=Path, help="Optionally write a zip with public artifacts.")
    parser.add_argument("--strict-final", action="store_true", help="Fail unless the final submission gate is ready.")
    return parser


def main() -> None:
    args = build_parser().parse_args()
    manifest = build_manifest(ROOT)
    write_outputs(
        manifest,
        json_path=args.json_out,
        markdown_path=args.markdown_out,
        zip_path=args.zip_out,
        root=ROOT,
    )
    print(json.dumps(cli_summary(manifest, args), indent=2))
    if args.strict_final and not manifest["safe_to_submit"]:
        raise SystemExit(2)


if __name__ == "__main__":
    main()