4"""Regenerate the tracked HUM chapter map and require byte identity."""
6from __future__
import annotations
14from pathlib
import Path
16REPO_ROOT = Path(__file__).resolve().parents[2]
17ARTEFACT = Path(
"docs/reference/CHAPTER_MAP.md")
18SOURCE_PDF = Path(
"docs/reference/ra8d2-hardware-user-manual.pdf")
19GENERATOR = Path(
"scripts/gen/build_chapter_map.sh")
20CHAPTER_ROW_RE = re.compile(
r"^\|\s+\d+\s+\|")
24class GenerationError(RuntimeError):
25 """The canonical generator could not produce trustworthy bytes."""
28def _run(root: Path, *argv: str) -> subprocess.CompletedProcess[bytes]:
29 """Run fixed repository tooling without a shell."""
30 return subprocess.run(
31 [*argv], cwd=root, capture_output=
True, check=
False
35def _is_tracked(root: Path, path: Path) -> bool:
36 """Return whether Git owns ``path`` in the candidate index."""
37 git = shutil.which(
"git")
40 result = _run(root, git,
"ls-files",
"--error-unmatch",
"--", path.as_posix())
41 return result.returncode == 0
44def _generate(root: Path) -> bytes:
45 """Run the canonical generator into an isolated temporary output."""
46 bash = shutil.which(
"bash")
48 message =
"bash is required"
49 raise GenerationError(message)
50 with tempfile.TemporaryDirectory()
as raw_tmp:
51 output = Path(raw_tmp) / ARTEFACT.name
52 result = _run(root, bash, str(root / GENERATOR),
"--output", str(output))
53 if result.returncode != 0:
54 detail = result.stderr.decode(
"utf-8", errors=
"replace").strip()
55 message = detail
or "chapter-map generator failed"
56 raise GenerationError(message)
57 if not output.is_file():
58 message =
"chapter-map generator wrote no output"
59 raise GenerationError(message)
60 return output.read_bytes()
63def freshness_reason(candidate: bytes |
None, fresh: bytes) -> str |
None:
64 """Return a failure reason for missing/drifted bytes, or ``None``."""
66 return "tracked candidate chapter map is missing"
67 if candidate != fresh:
68 return f
"candidate is {len(candidate)} bytes; regenerate is {len(fresh)} bytes"
72def _chapter_count(rendered: bytes) -> int:
73 """Count structurally rendered chapter-table rows."""
74 text = rendered.decode(
"ascii")
75 return sum(CHAPTER_ROW_RE.match(line)
is not None for line
in text.splitlines())
79 """Prove both verdict directions and the live generator's invariants."""
80 failures: list[str] = []
81 sample = b
"chapter-map\n"
82 if freshness_reason(sample, sample)
is not None:
83 failures.append(
"equal bytes were rejected")
84 if freshness_reason(sample + b
"drift\n", sample)
is None:
85 failures.append(
"drifted bytes were accepted")
86 if freshness_reason(
None, sample)
is None:
87 failures.append(
"missing candidate was accepted")
88 if not _is_tracked(REPO_ROOT, SOURCE_PDF):
89 failures.append(f
"source PDF is not tracked: {SOURCE_PDF}")
90 first = _generate(REPO_ROOT)
91 second = _generate(REPO_ROOT)
93 failures.append(
"two clean regenerations differ")
94 count = _chapter_count(first)
95 if count != EXPECTED_CHAPTERS:
96 failures.append(f
"chapter census drifted: {count} != {EXPECTED_CHAPTERS}")
98 for failure
in failures:
99 print(f
"check_chapter_map_freshness.py: selftest FAIL: {failure}", file=sys.stderr)
101 print(
"check_chapter_map_freshness.py: selftest OK (both directions; 69 chapters)")
106 """Compare the tracked candidate map with a clean regeneration."""
107 if not _is_tracked(REPO_ROOT, ARTEFACT):
108 print(f
"check_chapter_map_freshness.py: FAIL: {ARTEFACT} is not tracked", file=sys.stderr)
111 fresh = _generate(REPO_ROOT)
112 except GenerationError
as exc:
113 print(f
"check_chapter_map_freshness.py: ERROR: {exc}", file=sys.stderr)
115 reason = freshness_reason((REPO_ROOT / ARTEFACT).read_bytes(), fresh)
116 if reason
is not None:
118 f
"check_chapter_map_freshness.py: FAIL: {reason}; run scripts/gen/build_chapter_map.sh",
122 print(f
"Chapter map fresh ({EXPECTED_CHAPTERS} chapters from tracked full PDF)")
127 """Dispatch live check or selftest."""
128 parser = argparse.ArgumentParser(description=__doc__)
129 parser.add_argument(
"--selftest", action=
"store_true")
130 args = parser.parse_args()
131 return selftest()
if args.selftest
else check()
134if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.