Files
troupe-standup/wizard-work/sphinx/verify_mm_dossier.py
T

127 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Independent verifier for the multimodal arXiv dossier.
Checks, in order:
A. every file listed in MANIFEST.json exists and its sha256 matches the manifest
B. paper.html's sha256 matches the html_sha256 recorded in that paper's dossier.json
C. paper.txt really is derived from paper.html (substring agreement + size bounds)
D. tables.json and figures.json parse, are non-empty, and their counts match the
counts recorded in dossier.json
E. RE-FETCH each paper.html over HTTP; the sha256 must equal the manifest hash
(i.e. the archive reproduces what arXiv serves today)
F. RE-FETCH the Atom feed over HTTP; the ids of every archived paper must appear
in the live feed (the archive is traceable to the live query)
G. REPORT.md exists, is non-trivial, and names every archived arXiv id
Exit code 0 only if all checks pass.
"""
import hashlib
import json
import os
import re
import sys
import time
import urllib.error
import urllib.request
UA = {"User-Agent": "sphinx-archive-verify/1.0 (mailto:sphinx@agents.invalid)"}
D = sys.argv[1] if len(sys.argv) > 1 else "/root/.hermes/wizard-outputs/sphinx/mm-vla-dossier-2026-10-22"
fails = []
notes = []
def check(name, ok, detail=""):
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name, (" — " + detail) if detail else ""))
if not ok:
fails.append(name)
return ok
def sha(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for c in iter(lambda: f.read(65536), b""):
h.update(c)
return h.hexdigest()
def http(url, timeout=60):
req = urllib.request.Request(url, headers=UA)
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, r.read()
manifest = json.load(open(os.path.join(D, "MANIFEST.json")))
papers = manifest["papers"]
print("A. manifest integrity (sha256 of every listed file)")
for rel, meta in sorted(manifest["files"].items()):
p = os.path.join(D, rel)
ok = os.path.exists(p) and sha(p) == meta["sha256"]
check("A %s" % rel, ok, "%d bytes" % meta["bytes"] if os.path.exists(p) else "MISSING")
print("B. paper.html sha256 == dossier.html_sha256")
for p in papers:
pj = os.path.join(D, p["arxiv_id"], "paper.html")
dj = json.load(open(os.path.join(D, p["arxiv_id"], "dossier.json")))
check("B %s" % p["arxiv_id"], os.path.exists(pj) and sha(pj) == dj["paper"]["html_sha256"],
"manifest says %s" % p["html_sha256"][:16])
print("C. paper.txt is a real text layer of paper.html")
for p in papers:
h = open(os.path.join(D, p["arxiv_id"], "paper.html"), encoding="utf-8", errors="replace").read()
t = open(os.path.join(D, p["arxiv_id"], "paper.txt"), encoding="utf-8").read()
# the title string from the dossier must survive into the text layer
title = (p["title"] or "").strip()
probe = " ".join(title.split())[:40]
ok = (len(t) > 500 and len(t) < len(h) and probe in " ".join(t.split()))
check("C %s" % p["arxiv_id"], ok, "html %d -> text %d chars; title probe present=%s" % (
len(h), len(t), probe in " ".join(t.split())))
print("D. tables.json / figures.json counts agree with dossier.json")
for p in papers:
base = os.path.join(D, p["arxiv_id"])
d = json.load(open(os.path.join(base, "dossier.json")))["archive"]
tabs = json.load(open(os.path.join(base, "tables.json")))
figs = json.load(open(os.path.join(base, "figures.json")))
ok = (len(tabs) == d["tables_found"] and len(figs) == d["figures_found"]
and len(tabs) > 0 and len(figs) > 0)
check("D %s" % p["arxiv_id"], ok,
"tables %d==%d figures %d==%d" % (len(tabs), d["tables_found"], len(figs), d["figures_found"]))
print("E. live re-fetch of paper.html reproduces the archived sha256")
for p in papers:
try:
st, raw = http("https://arxiv.org/html/%s" % p["arxiv_id"])
got = hashlib.sha256(raw).hexdigest()
check("E %s" % p["arxiv_id"], st == 200 and got == p["html_sha256"],
"HTTP %s sha %s" % (st, got[:16]))
except Exception as e:
check("E %s" % p["arxiv_id"], False, "%s: %s" % (type(e).__name__, e))
time.sleep(1.0)
print("F. live Atom feed contains every archived paper id")
try:
st, raw = http(manifest["atom_url"])
feed = raw.decode("utf-8", "replace")
ids_in_feed = set(re.findall(r"<id>http://arxiv\.org/abs/([^<]+)</id>", feed))
missing = [p["arxiv_id"] for p in papers if p["arxiv_id"] not in ids_in_feed]
check("F atom ids", st == 200 and not missing,
"HTTP %s; feed ids %d; missing %s" % (st, len(ids_in_feed), missing or "none"))
except Exception as e:
check("F atom ids", False, "%s: %s" % (type(e).__name__, e))
print("G. REPORT.md exists and names every archived paper")
rp = os.path.join(D, "REPORT.md")
rep = open(rp, encoding="utf-8").read() if os.path.exists(rp) else ""
missing = [p["arxiv_id"] for p in papers if p["arxiv_id"] not in rep]
check("G report", len(rep) > 1000 and not missing,
"%d chars; missing %s" % (len(rep), missing or "none"))
print()
if fails:
print("RESULT: %d check(s) failed: %s" % (len(fails), fails))
sys.exit(1)
print("RESULT: all checks passed (%d papers, %d manifest files)" % (len(papers), len(manifest["files"])))