File size: 5,190 Bytes
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
#!/usr/bin/env python3
"""Fail-closed public claim lint for pre-live ProofFrame submissions."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
import re
from typing import Any

from proofframe.submission_gate import build_submission_gate


ROOT = Path(__file__).resolve().parents[1]
ACTIVE_PUBLIC_COPY_FILES = [
    "README.md",
    "docs/submission.md",
    "docs/assets/devpost-submission-packet.md",
    "docs/assets/devpost-submission-packet.json",
    "docs/assets/devpost-form-kit.md",
    "docs/assets/devpost-form-kit.json",
]
PRE_LIVE_ALLOWED_MARKERS = (
    "final gate",
    "final submission gate",
    "submission gate",
    "target",
    "pending",
    "requires",
    "require ",
    "after t020",
    "after t021",
    "after proof",
    "until live",
    "not ",
    "do not say",
    "safe after",
    "verified run",
)
PRE_LIVE_FORBIDDEN_PATTERNS = [
    re.compile(r"(?i)\bstores?\b.*\b(?:in|through|to)\b.*\bbackblaze b2\b"),
    re.compile(r"(?i)\bbackblaze b2-backed\b"),
    re.compile(r"(?i)\bgenerates?\b.*\b(?:with|through)\b.*\bgenblaze\b"),
    re.compile(r"(?i)\bgenblaze-generated\b"),
    re.compile(r"(?i)\buses?\b.*\bb2 storage\b.*\bend[- ]to[- ]end\b"),
    re.compile(r"(?i)\bevery asset\b.*\bb2\b"),
    re.compile(r"(?i)\blive b2 object packet\b"),
]


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 line_is_contextualized(line: str) -> bool:
    lowered = line.lower()
    return any(marker in lowered for marker in PRE_LIVE_ALLOWED_MARKERS)


def scan_public_copy(root: Path) -> list[dict[str, Any]]:
    findings: list[dict[str, Any]] = []
    for relative_path in ACTIVE_PUBLIC_COPY_FILES:
        path = root / relative_path
        if not path.exists():
            findings.append(
                {
                    "path": relative_path,
                    "line": None,
                    "status": "missing",
                    "detail": "Active public copy file is missing.",
                }
            )
            continue
        for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
            if line_is_contextualized(line):
                continue
            for pattern in PRE_LIVE_FORBIDDEN_PATTERNS:
                if pattern.search(line):
                    findings.append(
                        {
                            "path": relative_path,
                            "line": line_number,
                            "status": "unsafe-pre-live-claim",
                            "detail": line.strip(),
                        }
                    )
                    break
    return findings


def proof_ready_from_gate(gate: dict[str, Any]) -> bool:
    task_statuses = {item["id"]: item["ok"] for item in gate.get("task_gates", [])}
    return bool(gate["evidence_gate"]["ok"] and task_statuses.get("T020") and task_statuses.get("T021"))


def check_packet_mode(root: Path, proof_ready: bool) -> list[dict[str, Any]]:
    packet_path = root / "docs" / "assets" / "devpost-submission-packet.json"
    packet = load_json(packet_path)
    if packet is None:
        return [
            {
                "path": str(packet_path.relative_to(root)),
                "line": None,
                "status": "missing",
                "detail": "Devpost packet JSON is missing or invalid.",
            }
        ]
    mode = packet.get("mode")
    if not proof_ready and mode != "pre_live_safe":
        return [
            {
                "path": str(packet_path.relative_to(root)),
                "line": None,
                "status": "unsafe-packet-mode",
                "detail": f"Expected pre_live_safe before final evidence, got {mode!r}.",
            }
        ]
    return []


def build_claim_report(root: Path = ROOT) -> dict[str, Any]:
    root = root.resolve()
    gate = build_submission_gate(root)
    proof_ready = proof_ready_from_gate(gate)
    findings: list[dict[str, Any]] = []
    findings.extend(check_packet_mode(root, proof_ready))
    if not proof_ready:
        findings.extend(scan_public_copy(root))
    return {
        "ok": not findings,
        "mode": "post_live_verified" if proof_ready else "pre_live_safe",
        "final_gate": {
            "ok": gate["ok"],
            "proof_ready": proof_ready,
            "summary": gate["summary"],
            "evidence_status": gate["evidence_gate"]["status"],
            "packet_status": gate["packet_gate"]["status"],
        },
        "scanned_files": ACTIVE_PUBLIC_COPY_FILES,
        "findings": findings,
    }


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Lint ProofFrame public copy for unsafe claims.")
    parser.add_argument("--root", type=Path, default=ROOT)
    return parser


def main() -> None:
    args = build_parser().parse_args()
    report = build_claim_report(args.root)
    print(json.dumps(report, indent=2))
    raise SystemExit(0 if report["ok"] else 2)


if __name__ == "__main__":
    main()