backblaze-proofframe / scripts /claim_lint.py
junhaocheng
Sync post-credential gate hardening
f54fafa
Raw
History Blame Contribute Delete
5.19 kB
#!/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()