#!/usr/bin/env python3 """Build a reproducibility manifest for every application package (review finding F17). A package folder that contains a PDF looks finished. It is not necessarily reproducible: `output/NATO_AI_ENGINEER/` ships no `resume.cls` at all and only builds on a machine where one happens to be installed elsewhere, and its submitted PDF differs in content from the e2e source's PDF. The existing PDF hid both problems. This records, per package: which sources exist, the hash of the class each one would compile against, whether a PDF is present, and whether the submission-named PDF still matches the e2e one. It does NOT rewrite anything - historical packages are evidence. python resume_builder/helpers/package_manifest.py # write the manifest python resume_builder/helpers/package_manifest.py --check # fail if it is stale """ from __future__ import annotations import argparse import hashlib import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] OUTPUT = ROOT / "output" MANIFEST = ROOT / "resume_builder" / "canonical" / "package_manifest.json" REGISTER = ROOT / "resume_builder" / "canonical" / "historical_outputs.json" TODAY_MACRO = re.compile(r"\\today") # Ship with MiKTeX/TeX Live, so their absence from a package folder is not a missing file. # Only project-local classes (resume.cls, cv.cls) have to travel with the package. CTAN_CLASSES = {"article", "report", "book", "letter", "moderncv", "scrartcl", "scrlttr2", "memoir", "extarticle", "europasscv", "europecv", "altacv"} def digest(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest()[:16] def describe(folder: Path) -> dict: sources = sorted(p.name for p in folder.glob("*.tex")) classes = {p.name: digest(p) for p in folder.glob("*.cls")} pdfs = sorted(p.name for p in folder.glob("*.pdf")) needs = set() uses_today = [] for name in sources: text = (folder / name).read_text(encoding="utf-8", errors="replace") match = re.search(r"\\documentclass(?:\[[^\]]*\])?\{([^}]+)\}", text) if match: needs.add(match.group(1)) if TODAY_MACRO.search(text): uses_today.append(name) problems = [] for required in sorted(needs): # Classes that ship with a TeX distribution are not part of the package's own source # distribution; only project-local classes (resume.cls, cv.cls) have to travel with it. if required in CTAN_CLASSES: continue if f"{required}.cls" not in classes: problems.append( f"source distribution incomplete: requires {required}.cls, which is not in the " f"folder. A build here only succeeds if a copy happens to exist elsewhere on the " f"machine. Do NOT repair by copying the newest class in - the repo carries " f"several class variants and older macros differ.") if uses_today: problems.append( f"rebuild is date-dependent: {', '.join(uses_today)} use \\today, so recompiling a " f"historical package silently restamps it with the current date. New packages must " f"carry the explicit application date instead.") if sources and not pdfs: problems.append("no PDF present: nothing reviewable without a build") return { "sources": sources, "class_files": classes, "requires_class": sorted(needs), "pdfs": pdfs, "uses_today": uses_today, "problems": problems, } def build() -> dict: register = json.loads(REGISTER.read_text(encoding="utf-8")) classified = set() for key in ("unsafe_do_not_reuse", "historical_revalidate", "paused_no_package"): for item in register.get(key, []): classified.add(item["folder"] if isinstance(item, dict) else item) packages = {} for folder in sorted(OUTPUT.iterdir()): if not folder.is_dir() or not any(folder.glob("*.tex")): continue # Only APPLICATION packages. A folder holding a report or analysis document is not a # submission and has nothing to classify for reuse. if not any(folder.glob("*resume*.tex")) and not any(folder.glob("*cover*letter*.tex")): continue entry = describe(folder) entry["reuse_classification"] = ( "recorded" if f"output/{folder.name}" in classified else "UNCLASSIFIED") if f"output/{folder.name}" not in classified: entry["problems"].append( "not classified in historical_outputs.json. Default until someone classifies " "it: treat as historical_revalidate - never reuse a bullet from it without " "re-checking the claim against claims.json first.") packages[folder.name] = entry return { "schema_version": 1, "generated_note": ( "Generated by package_manifest.py (review finding F17). A PDF in a folder is not " "evidence that the package is reproducible. This file records what would actually " "happen on a rebuild. It is descriptive: nothing here authorises rewriting a " "submitted document."), "packages": packages, } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--check", action="store_true", help="fail if the manifest is stale") args = parser.parse_args() manifest = build() text = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n" if args.check: current = MANIFEST.read_text(encoding="utf-8") if MANIFEST.exists() else "" if current != text: print("ERROR: package manifest is stale; re-run package_manifest.py") return 1 print("Package manifest is current.") return 0 MANIFEST.write_text(text, encoding="utf-8") flagged = {name: entry for name, entry in manifest["packages"].items() if entry["problems"]} print(f"{len(manifest['packages'])} packages described, {len(flagged)} with findings\n") for name, entry in flagged.items(): print(f" {name}") for problem in entry["problems"]: print(f" - {problem[:150]}") return 0 if __name__ == "__main__": sys.exit(main())