Files
cs249r_book/book/tools/audit/chapter_html_verify.py
Vijay Janapa Reddi bd5a05544b Harden LEGO verify: per-ref duplicate-unit check, unrendered-math and crossref scan.
Mutation testing (inject known defect, require pipeline FAIL) found two
escapes in the per-chapter verify:

- The duplicate-unit check received the whole substituted line and
  compared the line's end against unit suffixes — it could effectively
  never fire ('150 ms ms' passed 43/43). Rewritten to check the prose
  immediately after each {python} ref against the unit the closed
  formatter already emitted.
- An unclosed $...$ span renders as literal '$() = 1 - (1 - p)^N.' in
  reader-visible prose and no gate caught it. chapter_html_verify now
  strips client-rendered math/pseudocode containers from visible text
  and flags unrendered math delimiters, '$' before a symbol, and
  unresolved @sec-/@fig- crossrefs. Validated against all 44 archived
  chapter pages: one true positive (nn_architectures), zero false
  positives.

Also create the gitignored reports dir before snapshotting so a fresh
worktree run does not die at the first cp.
2026-06-07 08:04:49 -04:00

528 lines
18 KiB
Python

#!/usr/bin/env python3
"""Build chapter HTML, audit raw output, and track pass/fail in a ledger.
Usage (repo root)::
python3 book/tools/audit/chapter_html_verify.py --list
python3 book/tools/audit/chapter_html_verify.py --vol1 introduction
python3 book/tools/audit/chapter_html_verify.py --vol1 --all
python3 book/tools/audit/chapter_html_verify.py --report
Ledger: book/tools/audit/artifacts/chapter_html_audit.json
Table: book/tools/audit/artifacts/chapter_html_audit.md
"""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
AUDIT_HTML = REPO_ROOT / "book/tools/audit/fmt/audit_html.py"
LEDGER_JSON = REPO_ROOT / "book/tools/audit/artifacts/chapter_html_audit.json"
LEDGER_MD = REPO_ROOT / "book/tools/audit/artifacts/chapter_html_audit.md"
CHAPTERS: dict[str, list[str]] = {
"vol1": [
"introduction/introduction",
"ml_systems/ml_systems",
"ml_workflow/ml_workflow",
"data_engineering/data_engineering",
"nn_computation/nn_computation",
"nn_architectures/nn_architectures",
"frameworks/frameworks",
"training/training",
"data_selection/data_selection",
"model_compression/model_compression",
"hw_acceleration/hw_acceleration",
"benchmarking/benchmarking",
"model_serving/model_serving",
"ml_ops/ml_ops",
"responsible_engr/responsible_engr",
"conclusion/conclusion",
"backmatter/appendix_algorithm",
"backmatter/appendix_assumptions",
"backmatter/appendix_dam",
"backmatter/appendix_data",
"backmatter/appendix_machine",
],
"vol2": [
"introduction/introduction",
"compute_infrastructure/compute_infrastructure",
"network_fabrics/network_fabrics",
"data_storage/data_storage",
"distributed_training/distributed_training",
"collective_communication/collective_communication",
"fault_tolerance/fault_tolerance",
"fleet_orchestration/fleet_orchestration",
"performance_engineering/performance_engineering",
"inference/inference",
"edge_intelligence/edge_intelligence",
"ops_scale/ops_scale",
"security_privacy/security_privacy",
"robust_ai/robust_ai",
"sustainable_ai/sustainable_ai",
"responsible_ai/responsible_ai",
"conclusion/conclusion",
"backmatter/appendix_dam",
"backmatter/appendix_fleet",
"backmatter/appendix_communication",
"backmatter/appendix_reliability",
"backmatter/appendix_inference",
"backmatter/appendix_c3",
"backmatter/appendix_assumptions",
],
}
HTML_ERROR_PATTERNS = [
re.compile(r"NameError:", re.I),
re.compile(r"KeyError:", re.I),
re.compile(r"AttributeError:", re.I),
re.compile(r"ModuleNotFoundError:", re.I),
re.compile(r"Traceback \(most recent call last\)"),
re.compile(r"Error rendering"),
]
HTML_MATH_RENDER_PATTERNS = [
(
"spaced ASCII multiplication",
re.compile(r"\b\d[\d,]*(?:\.\d+)?\s+x\s+(?:\d|\$|USD|[A-Za-z_(])"),
),
(
"literal dollar-times",
re.compile(r"\$\s*\\times\s*\$"),
),
# 2026-06-07: added after mutation testing — an unclosed `$...` in QMD
# renders as a literal dollar + broken math in reader-visible prose
# ("$() = 1 - (1 - p)^N.") and nothing below caught it. These scan
# _visible_text(), which excludes rendered math spans, so legitimate
# `\(...\)` MathJax sources never match.
(
"unrendered math delimiter",
re.compile(r"\\\(|\\\)|\\\[|\\\]"),
),
(
# `)` deliberately excluded: "(bandwidth/$)" and "per 1M tokens ($)"
# are legitimate currency parentheticals.
"unrendered math: $ before symbol",
re.compile(r"\$\s*[(=\\]"),
),
(
"unresolved crossref",
re.compile(r"\?@(?:sec|fig|tbl|eq|lst|algo)-|(?<![\w./])@(?:sec|fig|tbl|eq|lst|algo)-[\w-]+"),
),
]
@dataclass
class ChapterResult:
vol: str
chapter: str
qmd: str
html: str
status: str # pass | fail | pending | skip
build_ok: bool = False
build_seconds: float = 0.0
html_spurious_clean: bool | None = None
html_error_hits: list[str] = field(default_factory=list)
html_math_artifacts_clean: bool | None = None
html_math_artifact_hits: list[str] = field(default_factory=list)
lego_focal_ok: bool | None = None
prose_ok: bool | None = None
registry_ok: bool | None = None
notes: str = ""
updated_at: str = ""
def _chapter_id(vol: str, ch_path: str) -> str:
return f"{vol}/{ch_path.split('/')[-1]}"
def _qmd_path(vol: str, ch_path: str) -> Path:
return REPO_ROOT / "book/quarto/contents" / vol / f"{ch_path}.qmd"
def _prepare_single_chapter_build(vol: str) -> None:
"""Drop stale chapter HTML so post-render xref scan cannot false-fail.
Fast single-chapter binder builds only re-render the target QMD; leftover
HTML from prior full or partial builds remains under ``html-{vol}/`` and
``verify_rendered_xrefs.py`` scans the whole output tree.
"""
build_dir = REPO_ROOT / "book/quarto/_build" / f"html-{vol}"
contents = build_dir / "contents" / vol
if contents.is_dir():
for html in contents.rglob("*.html"):
html.unlink()
search_json = build_dir / "search.json"
if search_json.is_file():
search_json.unlink()
def _build_html(vol: str, ch_path: str) -> tuple[bool, float, str]:
name = ch_path.split("/")[-1]
binder_vol = f"--{vol}"
binder_ch = f"{vol}/{name}"
log = Path(f"/tmp/render_{vol}_{name}.log")
_prepare_single_chapter_build(vol)
t0 = time.monotonic()
proc = subprocess.run(
["./book/binder", "build", "html", binder_vol, binder_ch,
"--skip-hygiene", "--skip-validate"],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
elapsed = time.monotonic() - t0
log.write_text((proc.stdout or "") + (proc.stderr or ""), encoding="utf-8")
return proc.returncode == 0, elapsed, str(log)
def _live_html(vol: str, ch_path: str) -> Path:
build_dir = REPO_ROOT / "book/quarto/_build" / f"html-{vol}" / "contents" / vol
return build_dir / f"{ch_path}.html"
def _archive_html(vol: str, ch_path: str, live: Path) -> Path:
name = ch_path.split("/")[-1]
archive_dir = REPO_ROOT / "book/quarto/_build/html-audit" / vol
archive_dir.mkdir(parents=True, exist_ok=True)
dest = archive_dir / f"{name}.html"
dest.write_bytes(live.read_bytes())
return dest
def _audit_spurious(html: Path) -> tuple[bool, list[str]]:
proc = subprocess.run(
[sys.executable, str(AUDIT_HTML), str(html)],
capture_output=True,
text=True,
)
if proc.returncode == 0 and "CLEAN" in proc.stdout:
return True, []
issues = [ln for ln in (proc.stdout or proc.stderr).splitlines() if ln.strip()]
return False, issues[:10]
def _scan_html_errors(html: Path) -> list[str]:
try:
from bs4 import BeautifulSoup
except ImportError:
return []
soup = BeautifulSoup(html.read_text(encoding="utf-8", errors="replace"), "html.parser")
content = soup.find("main") or soup.body
if not content:
return []
for tag in content(["script", "style", "pre", "code"]):
tag.decompose()
text = content.get_text(separator=" ")
hits: list[str] = []
for pat in HTML_ERROR_PATTERNS:
if pat.search(text):
hits.append(pat.pattern)
return hits
def _visible_text(html: Path) -> str:
try:
from bs4 import BeautifulSoup
except ImportError:
return ""
soup = BeautifulSoup(html.read_text(encoding="utf-8", errors="replace"), "html.parser")
content = soup.find("main") or soup.body
if not content:
return ""
for tag in content(["script", "style", "annotation", "mjx-assistive-mml", "pre", "code"]):
tag.decompose()
# Rendered math spans legitimately contain raw `\(...\)` source (MathJax
# processes client-side), and pseudocode blocks contain raw algorithmic
# LaTeX (pseudocode.js renders client-side too); exclude both so the
# unrendered-math patterns only fire on LaTeX that leaked OUTSIDE a
# client-rendered container. (2026-06-07)
for tag in content.find_all(class_=re.compile(r"\bmath\b|pseudocode")):
tag.decompose()
return re.sub(r"\s+", " ", content.get_text(separator=" "))
def _scan_math_render_artifacts(html: Path) -> list[str]:
text = _visible_text(html)
if not text:
return []
hits: list[str] = []
for label, pat in HTML_MATH_RENDER_PATTERNS:
for match in pat.finditer(text):
start = max(0, match.start() - 50)
end = min(len(text), match.end() + 50)
context = text[start:end].strip()
hits.append(f"{label}: {context}")
if len(hits) >= 10:
return hits
return hits
def _lego_focal(qmd: Path) -> tuple[bool, str]:
proc = subprocess.run(
[sys.executable, str(REPO_ROOT / "book/tools/audit/lego_focal_verify.py"), str(qmd)],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
out = (proc.stdout or "") + (proc.stderr or "")
if proc.returncode == 0 and not out.strip():
return True, "no inline Python refs"
ok = proc.returncode == 0 and "issues=0" in out
return ok, out.strip().splitlines()[-1] if out.strip() else "no output"
def _prose_exec(qmd: Path, timeout_s: int = 120) -> tuple[bool, str]:
env = {**os.environ, "PYTHONPATH": str(REPO_ROOT / "mlsysim"), "MPLBACKEND": "Agg"}
try:
proc = subprocess.run(
[sys.executable, "-W", "ignore::UserWarning",
str(REPO_ROOT / "book/tools/audit/fmt/audit_prose.py"), str(qmd)],
cwd=REPO_ROOT,
env=env,
capture_output=True,
text=True,
timeout=timeout_s,
)
except subprocess.TimeoutExpired:
return False, f"prose exec timed out after {timeout_s}s"
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or "prose exec failed").strip()
return False, err[:300]
return True, "ok"
def _registry_scan(qmd: Path) -> tuple[bool, str]:
proc = subprocess.run(
[sys.executable, str(REPO_ROOT / "book/tools/audit/book_check_registry_sources.py"), str(qmd)],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
return proc.returncode == 0, (proc.stdout or proc.stderr or "").strip()[-200:]
def verify_chapter(vol: str, ch_path: str, skip_build: bool = False) -> ChapterResult:
qmd = _qmd_path(vol, ch_path)
name = ch_path.split("/")[-1]
res = ChapterResult(
vol=vol,
chapter=name,
qmd=str(qmd.relative_to(REPO_ROOT)),
html="",
status="pending",
updated_at=datetime.now(timezone.utc).isoformat(),
)
if not qmd.is_file():
res.status = "skip"
res.notes = "QMD missing"
return res
reg_ok, reg_msg = _registry_scan(qmd)
res.registry_ok = reg_ok
if not reg_ok:
res.notes += f"registry: {reg_msg}; "
lego_ok, lego_msg = _lego_focal(qmd)
res.lego_focal_ok = lego_ok
if not lego_ok:
res.notes += f"lego: {lego_msg}; "
prose_ok, prose_msg = _prose_exec(qmd)
res.prose_ok = prose_ok
if not prose_ok:
res.notes += f"prose: {prose_msg}; "
if skip_build:
archive = REPO_ROOT / "book/quarto/_build/html-audit" / vol / f"{name}.html"
if not archive.is_file():
res.status = "pending"
res.notes += "no archived HTML (--skip-build)"
return res
html = archive
res.build_ok = True
res.build_seconds = 0.0
else:
build_ok, secs, log = _build_html(vol, ch_path)
res.build_ok = build_ok
res.build_seconds = round(secs, 1)
if not build_ok:
res.status = "fail"
res.notes += f"build failed ({log}); "
return res
live = _live_html(vol, ch_path)
if not live.is_file():
res.status = "fail"
res.notes += f"HTML missing at {live}; "
return res
html = _archive_html(vol, ch_path, live)
res.html = str(html.relative_to(REPO_ROOT))
res.html_error_hits = _scan_html_errors(html)
res.html_math_artifact_hits = _scan_math_render_artifacts(html)
res.html_math_artifacts_clean = not res.html_math_artifact_hits
clean, spurious = _audit_spurious(html)
res.html_spurious_clean = clean
if res.html_error_hits:
res.notes += f"html errors: {res.html_error_hits}; "
if res.html_math_artifact_hits:
res.notes += f"math artifacts: {res.html_math_artifact_hits[:2]}; "
if not clean:
res.notes += f"spurious .0: {spurious[:2]}; "
passed = (
res.build_ok
and not res.html_error_hits
and res.html_math_artifacts_clean
and res.html_spurious_clean
and res.lego_focal_ok
and res.prose_ok
and res.registry_ok
)
res.status = "pass" if passed else "fail"
if passed:
res.notes = "OK — build + HTML clean + LEGO/registry consistent"
return res
def load_ledger() -> dict:
if LEDGER_JSON.is_file():
return json.loads(LEDGER_JSON.read_text(encoding="utf-8"))
return {"updated_at": "", "chapters": {}}
def save_ledger(ledger: dict) -> None:
LEDGER_JSON.parent.mkdir(parents=True, exist_ok=True)
ledger["updated_at"] = datetime.now(timezone.utc).isoformat()
LEDGER_JSON.write_text(json.dumps(ledger, indent=2), encoding="utf-8")
_write_markdown_table(ledger)
def _write_markdown_table(ledger: dict) -> None:
rows = []
for key in sorted(ledger.get("chapters", {}).keys()):
c = ledger["chapters"][key]
rows.append(c)
def yn(v):
if v is True:
return ""
if v is False:
return ""
return ""
lines = [
"# Chapter HTML audit ledger",
"",
f"Updated: {ledger.get('updated_at', '')}",
"",
"Registry migration chapter verification: build HTML → raw HTML scan → LEGO/prose/registry checks.",
"",
"| Vol | Chapter | Status | Build | HTML | Math | Spurious | LEGO | Prose | Registry | s | Notes |",
"|-----|---------|--------|-------|------|------|----------|------|-------|----------|---|-------|",
]
pass_n = fail_n = pending_n = 0
for c in rows:
st = c.get("status", "pending")
if st == "pass":
pass_n += 1
elif st == "fail":
fail_n += 1
else:
pending_n += 1
notes = (c.get("notes") or "").replace("|", "\\|")[:80]
lines.append(
f"| {c['vol']} | {c['chapter']} | **{st}** | {yn(c.get('build_ok'))} | "
f"{yn(not c.get('html_error_hits')) if c.get('html') else ''} | "
f"{yn(c.get('html_math_artifacts_clean'))} | "
f"{yn(c.get('html_spurious_clean'))} | {yn(c.get('lego_focal_ok'))} | "
f"{yn(c.get('prose_ok'))} | {yn(c.get('registry_ok'))} | "
f"{c.get('build_seconds', '')} | {notes} |"
)
lines.extend([
"",
f"**Summary:** {pass_n} pass / {fail_n} fail / {pending_n} pending / {len(rows)} total",
"",
"Re-run one chapter: `python3 book/tools/audit/chapter_html_verify.py --vol1 training`",
"",
])
LEDGER_MD.write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vol1", nargs="*", metavar="CHAPTER", help="Vol1 chapter(s) by short name")
parser.add_argument("--vol2", nargs="*", metavar="CHAPTER", help="Vol2 chapter(s) by short name")
parser.add_argument("--all", action="store_true", help="All chapters in selected volume(s)")
parser.add_argument("--list", action="store_true", help="List chapters")
parser.add_argument("--report", action="store_true", help="Regenerate markdown table only")
parser.add_argument("--skip-build", action="store_true", help="Audit archived HTML only")
args = parser.parse_args()
if args.list:
for vol, paths in CHAPTERS.items():
print(f"\n{vol}:")
for p in paths:
print(f" {p.split('/')[-1]}")
return 0
if args.report:
save_ledger(load_ledger())
print(f"Wrote {LEDGER_MD}")
return 0
targets: list[tuple[str, str]] = []
if args.all:
if args.vol1 is not None or (not args.vol1 and not args.vol2):
targets.extend(("vol1", p) for p in CHAPTERS["vol1"])
if args.vol2 is not None or (not args.vol1 and not args.vol2):
targets.extend(("vol2", p) for p in CHAPTERS["vol2"])
else:
for vol_flag, vol in [(args.vol1, "vol1"), (args.vol2, "vol2")]:
if vol_flag is None:
continue
names = vol_flag if vol_flag else []
if not names:
parser.error(f"Provide chapter names or --all for {vol}")
path_map = {p.split("/")[-1]: p for p in CHAPTERS[vol]}
for name in names:
if name not in path_map:
parser.error(f"Unknown {vol} chapter: {name}")
targets.append((vol, path_map[name]))
if not targets:
parser.print_help()
return 2
ledger = load_ledger()
ledger.setdefault("chapters", {})
fail = 0
for vol, ch_path in targets:
cid = _chapter_id(vol, ch_path)
print(f"\n=== {cid} ===")
result = verify_chapter(vol, ch_path, skip_build=args.skip_build)
ledger["chapters"][cid] = asdict(result)
save_ledger(ledger)
print(f" {result.status.upper()} ({result.build_seconds}s) — {result.notes[:120]}")
if result.status == "fail":
fail += 1
print(f"\nLedger: {LEDGER_JSON}")
print(f"Table: {LEDGER_MD}")
return 1 if fail else 0
if __name__ == "__main__":
raise SystemExit(main())