#!/usr/bin/env python3 """Validate the generated PhantomWiki 100Q static bundle.""" from __future__ import annotations import base64 import gzip import hashlib import json from pathlib import Path ROOT = Path(__file__).resolve().parents[1] EXPECTED_SHA256 = ( "69ac438794500cb64239009c3cb3a4d70f68b2073d29f592e246db1edd84cd6c" ) def read_json(path: Path): return json.loads(path.read_text(encoding="utf-8")) def qid_sha256(qids: set[str]) -> str: return hashlib.sha256("\n".join(sorted(qids)).encode()).hexdigest() def require_qids(label: str, actual: set[str], expected: set[str]) -> None: if actual != expected: raise ValueError( f"{label}: qid mismatch; missing={sorted(expected - actual)[:5]} " f"extra={sorted(actual - expected)[:5]}" ) def main() -> None: manifest = read_json(ROOT / "data/manifest.json") qids = set(read_json(ROOT / "data/qids.json")) if len(qids) != 100: raise ValueError(f"expected 100 qids, got {len(qids)}") if qid_sha256(qids) != EXPECTED_SHA256: raise ValueError("fixed QID SHA-256 mismatch") if manifest["qid_sha256"] != EXPECTED_SHA256: raise ValueError("manifest QID SHA-256 mismatch") eval_rows = read_json(ROOT / manifest["files"]["eval"]) require_qids("eval", {str(row["id"]) for row in eval_rows}, qids) structures = read_json(ROOT / manifest["files"]["structures"]) require_qids( "structures", {str(row["qid"]) for row in structures["rows"]}, qids, ) for row in structures["rows"]: path = ROOT / "data/structures" / row["path"] if not path.is_file(): raise FileNotFoundError(path) payload = json.loads( gzip.decompress(base64.b64decode(path.read_text(encoding="ascii"))) ) if str(payload["qid"]) != str(row["qid"]): raise ValueError(f"{path}: qid mismatch") run_slots = [] for run in manifest["runs"]: run_slots.append(run["slot"]) index = read_json(ROOT / run["index"]) run_qids = {str(row["qid"]) for row in index["records"]} require_qids(f"run {run['slot']}", run_qids, qids) for qid in qids: result_path = ROOT / run["results"] / f"{qid}.json" trajectory_path = ROOT / run["trajectories"] / f"{qid}.json" if not result_path.is_file(): raise FileNotFoundError(result_path) if not trajectory_path.is_file(): raise FileNotFoundError(trajectory_path) if read_json(result_path)["qid"] != qid: raise ValueError(f"{result_path}: qid mismatch") if read_json(trajectory_path)["qid"] != qid: raise ValueError(f"{trajectory_path}: qid mismatch") compare = read_json(ROOT / manifest["files"]["compare"]) if compare["slots"] != run_slots: raise ValueError("compare slot order does not match run manifest") require_qids( "compare", {str(row["qid"]) for row in compare["records"]}, qids, ) for qid in qids: record = read_json(ROOT / "data/compare/records" / f"{qid}.json") if set(record["runs"]) != set(run_slots): raise ValueError(f"compare {qid}: run set mismatch") forbidden = [] if (ROOT / "sets.json").exists(): forbidden.append("sets.json") forbidden.extend(path.name for path in ROOT.glob("corpus_size*_seed*.json")) forbidden.extend(path.name for path in ROOT.glob("eval_size*_seed*.json")) if forbidden: raise ValueError(f"legacy multi-universe files present: {sorted(set(forbidden))}") print( f"validated 100 qids, {len(run_slots)} runs, " f"{len(structures['rows'])} structure records" ) if __name__ == "__main__": main()