fix(F17): a PDF in a folder is not proof the package can be rebuilt
Build sweep run this session: 48 sources, 46 built, 2 failed. Infineon_AI_Engineer's cover letter fails on font expansion (the font is not scalable); cv_template.tex fails on unfilled placeholders, which is expected for the academic legacy path. Overfull boxes: Microsoft ISE 25.33pt, Isovalent 0.88pt. All historical, all left alone. New: resume_builder/helpers/package_manifest.py records, per application package, what a rebuild would ACTUALLY do - sources, class-file hashes, PDFs present, and the reasons a rebuild would not reproduce the submitted document. It is descriptive; nothing in it authorises rewriting a submitted file. What it found across 21 packages: - 17 are DATE-DEPENDENT. The cover letters use oday, so recompiling a historical package silently restamps it with today's date. - 1 has an INCOMPLETE SOURCE DISTRIBUTION: output/NATO_AI_ENGINEER ships no resume.cls. It builds on this machine only because a copy exists elsewhere, and the shipped PDF hid that. Not repaired by copying the newest class in - the repo carries THREE different resume.cls variants (13/7/1 copies) and older macros differ, so that "fix" could silently change a historical document. - 1 has no PDF at all, so nothing is reviewable without a build. Classification: 7 application packages were in no reuse category. Added to historical_revalidate, and the register now records the default explicitly - anything unlisted is treated as revalidate-before-reuse. Being recent is not the same as being verified; none of the seven was re-read for this. Prevention rather than repair: oday removed from the cover-letter template so new packages stop adding to the pile. A test now fails if it comes back, if the manifest goes stale, or if a package is left unclassified. Also: scan_document gained a template mode. A placeholder in a TEMPLATE is what makes it a template; the defect is one surviving into a generated document. 37/37 tests pass; counter-proof still catches all 9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qej5SBvhcbGXHwLqNWiskM
This commit is contained in:
1 parent
c7e8d4e058
commit
eecdbdade6
7 files changed
+797
-24
No files matched your search
@@ -0,0 +1,146 @@
|
||||
#!/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())
|
||||
@@ -218,7 +218,7 @@ def _negated(haystack: str, term: str) -> bool:
|
||||
return bool(hits) and all(_NEGATION.search(haystack[max(0, hit.start() - 70):hit.start()]) for hit in hits)
|
||||
|
||||
|
||||
def scan_document(path: Path, claims: dict) -> list[str]:
|
||||
def scan_document(path: Path, claims: dict, template: bool = False) -> list[str]:
|
||||
"""Deterministic, mechanically checkable properties of a generated document.
|
||||
|
||||
This is NOT a truth check and must never be reported as one. It cannot judge whether a
|
||||
@@ -266,7 +266,9 @@ def scan_document(path: Path, claims: dict) -> list[str]:
|
||||
|
||||
# Scan the comment-stripped RAW text: _MARKUP treats "\item [GENERATE: x]" as a command
|
||||
# with an optional argument and would swallow the placeholder whole.
|
||||
for match in _PLACEHOLDER.finditer(_COMMENT.sub("", raw)):
|
||||
# Templates are EXEMPT: an unfilled placeholder is what makes a template a template. The
|
||||
# defect is a placeholder surviving into a generated document.
|
||||
for match in ([] if template else _PLACEHOLDER.finditer(_COMMENT.sub("", raw))):
|
||||
errors.append(f"{path}: unresolved placeholder left in the document: {match.group(0)!r}")
|
||||
|
||||
identity = claims.get("identity", {})
|
||||
|
||||
Reference in new issue
Block a user