File size: 11,376 Bytes
7dedc8d
 
 
 
 
 
 
 
 
 
 
60570dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7dedc8d
 
 
 
 
 
74ee21a
 
 
 
 
7dedc8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60570dc
 
 
 
 
 
 
 
 
7dedc8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60570dc
7dedc8d
 
 
 
 
 
60570dc
7dedc8d
 
 
 
 
 
 
 
 
 
 
 
347a4d6
 
 
 
 
 
 
 
 
 
 
7dedc8d
 
 
 
 
 
 
60570dc
7dedc8d
 
 
 
 
 
 
 
 
 
 
60570dc
7dedc8d
 
f74bce1
 
 
 
 
 
 
 
7dedc8d
f74bce1
 
60570dc
f74bce1
7dedc8d
 
 
 
60570dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f74bce1
60570dc
 
7dedc8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f74bce1
 
60570dc
 
 
 
 
 
 
 
 
 
7dedc8d
 
 
 
 
 
 
 
 
 
60570dc
7dedc8d
60570dc
7dedc8d
 
 
 
 
 
60570dc
 
 
 
 
 
7dedc8d
 
 
 
60570dc
7dedc8d
 
 
 
 
 
 
 
 
 
 
60570dc
f74bce1
7dedc8d
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
"""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),
    }