move 7: the book's button — check-book.sh + check-book.py

The only source of 'ALL GREEN' for this repository. Rebuilds the PDF,
then verifies 93 countable claims printed in the book against reality
measured at run time:

- source hygiene: inputs<->files both directions, contiguous ch01..ch14,
  every chapter (and the interlude) ends on its checkpoint, per-chapter
  exercise count == solution count with hand-typed numbering N.1..N.k
- built PDF: >=100 pages, zero unresolved references, any page-count
  claim in prose must equal pdfinfo
- internal congruence: chapter-count words in README/ch01 vs measured N
  ('spent twelve chapters' in ch13 is checked as a positional count, not
  grepped as stale — the spelling-vs-property lesson, applied to the
  checker itself); week-plan heading == max table row; the
  discussion-exercise roster parsed from prose == measured set; the
  SLH-DSA arithmetic recomputed from scratch (digest split 21/7/2, sig
  7856, fixed 254, per-layer max 510 by brute force, worst 3824,
  checksum digit examples) and each value required present in ch13
- cross-repo congruence: 19 leaves derived by property (six-digit
  filenames + index fields — the entries/ glob counts 25); every
  nineteen/19 claim in prose parsed and compared; leaves 13-16 subjects
  + 44 certs; leaves 12/17 = 61; leaves 0-11 = 16; leaf 18 = 11 certs,
  apex cone kernel-3+5 oracles, ht cone f,h,t_l, four kernel-3-only
  plumbing certs, all cones exact; first dual-signed head at size 14;
  final head size == leaf count; ch13 parameter card == the const-generic
  arguments parsed out of the extracted Funs.lean; ch07's 71-digit Q ==
  P25519.lean digit for digit

Fails closed: a missing sibling repo is a FAILURE, not a skip;
BOOK_LOCAL_ONLY=1 skips cross-repo loudly and never prints ALL GREEN.
--selftest mutates copies of the sources seven ways (count drift,
deleted solution, one Q digit, leaf-count drift, arithmetic drift,
stray box after a checkpoint, plan/heading divergence) and requires each
to be caught BY ITS OWN CHECK, plus an unmutated control that must pass.

Full run: ALL GREEN (93 checks). Selftest: 8/8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mrwulf 2026-08-08 14:16:30 +02:00
parent 63a809dd2c
commit 311f60d4d0
4 changed files with 507 additions and 0 deletions

View file

@ -87,6 +87,30 @@ lake build Solutions # compiles all solution files as a check
Chapters 24 need no Mathlib at all — you can start them with any Lean 4
install while the cache downloads.
## The button
Like every repository in this estate, the book has one command that earns
its claims — and it is the only source of the words "ALL GREEN" here:
```bash
./check-book.sh
```
It rebuilds the PDF from the committed sources and then verifies ~90
countable claims printed in the book against reality measured at run
time: chapter and week-plan counts, exercise↔solution pairing per chapter,
every chapter ending on its checkpoint, the recomputed SLH-DSA arithmetic
(digest split, signature size, the 3,824-call worst case), the
transparency log's 19 leaves and per-leaf certificate counts, leaf 18's
axiom cones, the first dual-signed head at size 14, the extracted
SLH-DSA-SHA2-128s parameter card, and chapter 7's 71-digit Q — digit for
digit against `P25519.lean`. Numbers are parsed out of the prose and
compared to measurements, so editing either side alone turns the button
red. Cross-repo checks need the sibling estate repos checked out next to
this one (`BOOK_LOCAL_ONLY=1` skips them, loudly, and never prints ALL
GREEN). `./check-book.sh --selftest` mutates copies of the sources seven
ways and proves each mutation is caught by its own check.
## Building the book
The repo's own recipe (tectonic, user-space, no root — installs itself on

372
check-book.py Normal file
View file

@ -0,0 +1,372 @@
#!/usr/bin/env python3
"""check-book.py — the measured half of the book's button (see check-book.sh).
Every check compares a claim PRINTED IN THE BOOK against a value MEASURED
from the sources, the built PDF, or the sibling repositories at the moment
it runs. No expected value is carried in this file when it can be derived;
where the book's prose states a number, the number is parsed OUT OF THE
PROSE and compared against the measurement so editing either side alone
turns the button red.
Populations are derived by property, never by glob or label (the log's
entries/ directory contains convenience copies; leaves are the files whose
names are six digits AND whose index field matches their position).
Exit 0 only if every applicable check passed. A check that could not
measure (missing sibling repo without BOOK_LOCAL_ONLY=1) is a FAILURE,
not a skip: a gate that read nothing must not look like a clean gate.
"""
import json
import os
import re
import subprocess
import sys
FAILS = []
PASSES = []
def ok(label, detail=""):
PASSES.append(label)
print(f" ok {label}" + (f" [{detail}]" if detail else ""))
def fail(label, detail=""):
FAILS.append(label)
print(f" FAIL {label}" + (f" [{detail}]" if detail else ""))
def check(cond, label, detail=""):
(ok if cond else fail)(label, detail)
def read(path):
with open(path, "r", errors="replace") as fh:
return fh.read()
WORDS = {
"one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
"seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11,
"twelve": 12, "thirteen": 13, "fourteen": 14, "fifteen": 15,
"sixteen": 16, "seventeen": 17, "eighteen": 18, "nineteen": 19,
"twenty": 20, "forty-four": 44, "sixty-one": 61,
}
def main():
book = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else ".")
no_pdf = os.environ.get("SKIP_BUILD") == "1"
local_only = os.environ.get("BOOK_LOCAL_ONLY") == "1"
estate = os.environ.get("ESTATE_ROOT", os.path.dirname(book))
ltl = os.environ.get("LTL_DIR", os.path.join(estate, "lean-transparency-log"))
fips = os.environ.get("FIPS205_DIR", os.path.join(estate, "fips205-slhdsa-verified"))
p25519 = os.environ.get("P25519_FILE", os.path.join(
estate, "dalek-ed25519-verified", "verification", "Proofs", "P25519.lean"))
main_tex = read(os.path.join(book, "main.tex"))
readme = read(os.path.join(book, "README.md"))
chdir = os.path.join(book, "chapters")
# ── Phase 0: source hygiene ────────────────────────────────────────────
print("=== Phase 0: source hygiene ===")
inputs = re.findall(r"\\input\{(chapters/[^}]+)\}", main_tex)
missing = [i for i in inputs if not os.path.exists(os.path.join(book, i + ".tex"))]
check(not missing, "every \\input'd chapter file exists",
",".join(missing) or f"{len(inputs)} inputs")
on_disk = {f"chapters/{f[:-4]}" for f in os.listdir(chdir) if f.endswith(".tex")}
orphans = sorted(on_disk - set(inputs))
check(not orphans, "no orphan .tex under chapters/", ",".join(orphans) or "none")
chapter_files = [i for i in inputs if re.search(r"chapters/ch\d\d-", i)]
nums = [int(re.search(r"ch(\d\d)-", c).group(1)) for c in chapter_files]
n_ch = len(chapter_files)
check(nums == list(range(1, n_ch + 1)) and n_ch > 0,
"chapter files contiguous ch01..chNN in input order", f"N={n_ch}")
for cf in chapter_files + ["chapters/interlude-by-hand"]:
src = read(os.path.join(book, cf + ".tex"))
envs = re.findall(r"\\end\{([a-z]+)\}", src)
check(envs and envs[-1] == "checkpoint",
f"{cf.split('/')[-1]}: last environment is checkpoint",
envs[-1] if envs else "no environments")
for cf in chapter_files:
num = int(re.search(r"ch(\d\d)-", cf).group(1))
src = read(os.path.join(book, cf + ".tex"))
n_ex = len(re.findall(r"\\exercise\{", src))
sols = re.findall(r"\\solhead\{(\d+)\.(\d+)\}", src)
n_sol = len(sols)
label = f"ch{num:02d}: exercises == solutions"
if n_ex == 0 and n_sol == 0:
ok(label, "none (allowed)")
continue
good = (n_ex == n_sol
and all(int(a) == num for a, _ in sols)
and [int(b) for _, b in sols] == list(range(1, n_sol + 1)))
check(good, label, f"{n_ex} exercises, solheads {[a+'.'+b for a, b in sols]}")
isrc = read(os.path.join(book, "chapters/interlude-by-hand.tex"))
i_ex = len(re.findall(r"Exercise I\.\d", isrc))
i_sol = re.findall(r"\\solhead\{I\.(\d+)\}", isrc)
check(i_ex == len(i_sol) and [int(x) for x in i_sol] == list(range(1, len(i_sol) + 1)),
"interlude: exercises == solutions", f"{i_ex} vs {len(i_sol)}")
# ── Phase 1 leftovers: claims about the built PDF ──────────────────────
print("=== Phase 1b: built-PDF claims ===")
if no_pdf:
print(" (skipped: SKIP_BUILD=1 — build phase runs in check-book.sh)")
else:
pdf = os.path.join(book, "main.pdf")
info = subprocess.run(["pdfinfo", pdf], capture_output=True, text=True).stdout
pages = int(re.search(r"Pages:\s+(\d+)", info).group(1))
check(pages >= 100, "PDF built and non-trivial", f"{pages} pages")
txt = subprocess.run(["pdftotext", pdf, "-"],
capture_output=True, text=True).stdout
bad = [l for l in txt.splitlines() if "??" in l]
check(not bad, "no unresolved references ('??') in rendered PDF",
bad[0][:60] if bad else "clean")
page_claims = re.findall(r"(\d{2,4}) pages", readme + main_tex)
if page_claims:
for pc in page_claims:
check(int(pc) == pages, f"page-count claim {pc} == built {pages}")
else:
ok("no page-count claim in prose (nothing to bind)")
# ── Phase 2: internal countable claims ─────────────────────────────────
print("=== Phase 2: internal countable claims ===")
m = re.search(r"(\w+(?:-\w+)?) chapters", readme)
check(m and WORDS.get(m.group(1)) == n_ch,
f"README chapter count == {n_ch}", m.group(0) if m else "no claim found")
ch01 = read(os.path.join(book, "chapters/ch01-why-verify.tex"))
m = re.search(r"the next (\w+)\s*\nchapters|the next (\w+) chapters", ch01)
word = (m.group(1) or m.group(2)) if m else None
check(word is not None and WORDS.get(word) == n_ch - 1,
f"ch01 'the next N chapters' == {n_ch - 1}", word or "claim not found")
# stale-total scan is scoped to the front matter, where whole-book totals
# live; inside a chapter, "N chapters" is a positional count checked next
for stale in ("twelve chapters", "thirteen chapters"):
hits = [f for f in ("main.tex", "README.md")
if stale in read(os.path.join(book, f))]
check(not hits, f"no stale '{stale}' in front matter", ",".join(hits) or "clean")
# any "spent N chapters" phrase inside chapter chNN counts its predecessors
for cf in chapter_files:
num = int(re.search(r"ch(\d\d)-", cf).group(1))
for word in re.findall(r"spent (\w+) chapters",
read(os.path.join(book, cf + ".tex"))):
check(WORDS.get(word) == num - 1,
f"ch{num:02d} 'spent {word} chapters' == its {num - 1} predecessors")
m = re.search(r"A (\w+)-week plan", main_tex)
plan_word = WORDS.get(m.group(1)) if m else None
weeks = [int(x) for x in re.findall(r"^(\d+)\s+&", main_tex, re.M)]
ranges = [int(b) for _, b in re.findall(r"^(\d+)--(\d+)\s+&", main_tex, re.M)]
max_week = max(weeks + ranges) if (weeks or ranges) else 0
check(plan_word == max_week and plan_word is not None,
"week-plan heading == max week row", f"{plan_word} vs {max_week}")
check(f"{m.group(1)}-week" in main_tex.replace("A " + m.group(1), "", 1),
"instructors paragraph agrees with plan heading")
m = re.search(r"Chapters ([\d, ]+ and \d+) carry one", main_tex)
if not m:
fail("discussion-exercise roster claim parseable", "pattern not found")
else:
claimed = set(int(x) for x in re.findall(r"\d+", m.group(1)))
measured = set()
for cf in chapter_files:
num = int(re.search(r"ch(\d\d)-", cf).group(1))
src = read(os.path.join(book, cf + ".tex"))
if re.search(r"\\exercise\{\(Discussion\)", src):
measured.add(num)
check(claimed == measured, "discussion-exercise roster == measured",
f"claimed {sorted(claimed)}, measured {sorted(measured)}")
ch13 = read(os.path.join(book, "chapters/ch13-second-summit.tex"))
n, h, d, hp, a, k, w, ln2, m_dig = 16, 63, 7, 9, 12, 14, 16, 3, 30
length = 2 * n + ln2
split = ((k * a + 7) // 8, (h - h // d + 7) // 8, (h + 8 * d - 1) // (8 * d))
check(sum(split) == m_dig and split == (21, 7, 2), "digest split recomputes",
str(split))
sig_bytes = n * (1 + k * (1 + a) + d * (length + hp))
check(sig_bytes == 7856, "signature size recomputes", str(sig_bytes))
fixed = k * a + d * hp + d + 1 + 1 + k # H paths + T's + Hmsg + FORS leaves
best = 0
for csum in range(0, 32 * (w - 1) + 1):
sh = csum << 4
digs = [(sh >> 12) & 0xF, (sh >> 8) & 0xF, (sh >> 4) & 0xF]
best = max(best, csum + sum((w - 1) - x for x in digs))
worst = fixed + d * best
check(fixed == 254 and best == 510 and worst == 3824,
"oracle pricing recomputes (fixed/per-layer-max/worst)",
f"{fixed}/{best}/{worst}")
def see_saw(csum):
sh = csum << 4
return ((sh >> 12) & 0xF, (sh >> 8) & 0xF, (sh >> 4) & 0xF)
check(see_saw(480) == (1, 14, 0) and see_saw(479) == (1, 13, 15)
and see_saw(256) == (1, 0, 0), "checksum worked examples recompute")
for token, why in [("7{,}856", "signature size"), ("254", "fixed oracle calls"),
("510", "per-layer max"), ("3{,}824", "worst total"),
("(1, 14, 0)", "csum 480 digits"),
("(1, 13, 15)", "csum 479 digits"),
("57{,}344", "FORS forest"), ("231", "H count"),
("$21 + 7 + 2 = 30$", "digest split")]:
check(token in ch13, f"ch13 prints {why}", token)
check(len(str(2**255 - 19)) == 77, "'77-digit prime' recomputes")
# ── Phase 3: cross-repo congruence ─────────────────────────────────────
print("=== Phase 3: cross-repo congruence ===")
if local_only:
print(" (SKIPPED: BOOK_LOCAL_ONLY=1 — cross-repo claims NOT verified)")
else:
# the log: leaves by property, not by glob
entdir = os.path.join(ltl, "entries")
if not os.path.isdir(entdir):
fail("lean-transparency-log present", entdir)
else:
leaf_files = sorted(f for f in os.listdir(entdir)
if re.fullmatch(r"\d{6}\.json", f))
leaves = [json.load(open(os.path.join(entdir, f))) for f in leaf_files]
idx_ok = all(lf["index"] == i for i, lf in enumerate(leaves))
n_leaves = len(leaves)
check(idx_ok, "leaf indexes contiguous and match filenames",
f"{n_leaves} leaves")
for where, src, pat in [
("title page '19 entries'", main_tex, r"(\d+) entries and counting"),
("preface 'nineteen pieces'", main_tex, r"lists (\w+)\s*\npieces|lists (\w+) pieces"),
("ch01 tryit '19 entries'", ch01, r"(\d+) entries, each one"),
("ch14 'nineteen leaves'",
read(os.path.join(book, "chapters/ch14-attestation-protocol.tex")),
r"log's (\w+) leaves")]:
mm = re.search(pat, src)
val = None
if mm:
g = next(g for g in mm.groups() if g)
val = int(g) if g.isdigit() else WORDS.get(g)
check(val == n_leaves, f"{where} == measured {n_leaves}", str(val))
def certs(i):
return leaves[i]["leaf"]["attestation"]["certificates"]
def comp(i):
return leaves[i]["leaf"]["attestation"]["subject"]["component"]
ed = {"dalek-ed25519-verified", "anza-ed25519-verified",
"risc0-ed25519-verified", "betrusted-ed25519-verified"}
check({comp(i) for i in (13, 14, 15, 16)} == ed,
"leaves 13-16 subjects are the four ed25519 forks")
check(all(len(certs(i)) == 44 for i in (13, 14, 15, 16)),
"'forty-four certificates each' == measured",
str([len(certs(i)) for i in (13, 14, 15, 16)]))
check(27 + 13 + 4 == 44, "27 main + 13 scalar + 4 apex == 44")
check(comp(17) == "ltl-accumulator-verified"
and len(certs(17)) == 61 and len(certs(12)) == 61,
"'sixty-one' accumulator certificates == measured (leaves 12, 17)")
check(all(len(certs(i)) == 16 for i in range(0, 12)),
"'sixteen' early-leaf certificates == measured (leaves 0-11)")
s18 = leaves[18]["leaf"]["attestation"]
check(s18["subject"]["component"] == "fips205-slhdsa-verified"
and s18["subject"]["kind"] == "slh_dsa",
"leaf 18 subject is fips205-slhdsa-verified / slh_dsa")
check(len(certs(18)) == 11 and "eleven certificates" in ch13,
"'eleven certificates' == measured", str(len(certs(18))))
by_name = {c["name"]: c for c in certs(18)}
for want in ("fips205.chain_free_loop_eq", "fips205.xmss_loop_eq",
"fips205.ht_loop_eq", "fips205.wots_csum_loop_eq",
"fips205.slh_verify_128s_accepts_iff"):
check(want in by_name, f"leaf 18 carries {want}")
kernel3 = {"propext", "Classical.choice", "Quot.sound"}
apex = set(by_name["fips205.slh_verify_128s_accepts_iff"]["observed_axioms"])
oracles = {f"verify_mono.oracle.{x}"
for x in ("f", "h", "h_msg", "t_l", "t_len")}
check(apex == kernel3 | oracles, "apex cone == kernel-3 + five oracles")
ht = set(by_name["fips205.ht_loop_eq"]["observed_axioms"])
check(ht == kernel3 | {"verify_mono.oracle.f", "verify_mono.oracle.h",
"verify_mono.oracle.t_l"},
"ht cone is f,h,t_l (the book's table row, by property not name)")
plumb = sum(1 for c in certs(18) if set(c["observed_axioms"]) == kernel3)
check(plumb == 4, "four kernel-3-only plumbing certificates", str(plumb))
check(all(c["status"] == "proven" and c["axiom_status"] == "clean"
and set(c["observed_axioms"]) == set(c["expected_axioms"])
for c in certs(18)), "leaf 18: all proven, clean, cones exact")
heads = [json.loads(l) for l in
open(os.path.join(ltl, "sth-history.jsonl")) if l.strip()]
dual = [hd["tree_size"] for hd in heads
if hd["signatures"].get("slh_dsa", {}).get("status") == "signed"]
check(dual and min(dual) == 14,
"'since tree 14' dual-signed heads == measured",
f"first dual head size {min(dual) if dual else None}")
check(heads[-1]["tree_size"] == n_leaves,
"final head size == leaf count")
# fips205 parameter card
funs = os.path.join(fips, "verification", "gen", "SlhVerify", "Funs.lean")
if not os.path.exists(funs):
fail("fips205-slhdsa-verified present", funs)
else:
fsrc = read(funs)
mm = re.search(
r"def verify_mono\.slh_verify_128s.*?types\.SlhDsaSig "
r"(\d+)#usize (\d+)#usize (\d+)#usize (\d+)#usize (\d+)#usize "
r"(\d+)#usize.*?slh_verify_internal_free (\d+)#usize (\d+)#usize",
fsrc, re.S)
if not mm:
fail("fips205 entry-point parameters parseable")
else:
A, D, HP, K, LEN, N = (int(mm.group(i)) for i in range(1, 7))
H, M = int(mm.group(7)), int(mm.group(8))
W = int(re.search(r"def W : Std\.U32 := (\d+)#u32", fsrc).group(1))
repo = {"a": A, "d": D, "h'": HP, "k": K, "len": LEN,
"n": N, "h": H, "m": M, "w": W}
card = {"a": a, "d": d, "h'": hp, "k": k, "len": length,
"n": n, "h": h, "m": m_dig, "w": w}
check(repo == card, "ch13 parameter card == extracted entry point",
str(repo))
for sym, val in card.items():
tok = f"{sym} = {val}"
check(tok in ch13 or f"= {val}" in ch13,
f"ch13 prints {sym} = {val}")
# the 71-digit Q, digit for digit
if not os.path.exists(p25519):
fail("P25519.lean present for Q comparison", p25519)
else:
psrc = read(p25519)
mq = re.search(r"theorem prime_(\d{60,})", psrc)
ch07 = read(os.path.join(book, "chapters/ch07-primality-certificates.tex"))
mb = re.search(r"Q = ([0-9\\a-z{} ]+?),\s*\n?\\\]", ch07, re.S)
q_repo = mq.group(1) if mq else None
q_book = re.sub(r"\D", "", mb.group(1)) if mb else None
check(q_repo is not None and q_book == q_repo,
"ch07's printed Q == repository's Q, digit for digit",
f"book {len(q_book or '')} digits, repo {len(q_repo or '')} digits")
check(q_repo is not None and len(q_repo) == 71 and "71-digit" in ch07,
"'71-digit' claim recomputes")
# ── verdict ────────────────────────────────────────────────────────────
print()
total = len(PASSES) + len(FAILS)
if FAILS:
print(f"RED: {len(FAILS)} of {total} claims diverge from measured reality:")
for f in FAILS:
print(f" - {f}")
sys.exit(1)
if local_only:
print(f"LOCAL CHECKS GREEN ({total} checks) — cross-repo claims NOT "
f"verified (BOOK_LOCAL_ONLY=1). This is not ALL GREEN.")
sys.exit(0)
print(f"ALL CLAIM CHECKS GREEN ({total} checks)")
sys.exit(0)
if __name__ == "__main__":
main()

111
check-book.sh Executable file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env bash
# check-book.sh — THE button for verifying-crypto-with-lean.
#
# This script is the only source of the words "ALL GREEN" for this
# repository. It rebuilds the book from the committed sources and then
# verifies that every countable claim printed in the book matches reality
# measured at run time: chapter counts, the week plan, exercise/solution
# pairing, the recomputed SLH-DSA arithmetic, the transparency log's leaf
# and certificate counts, the first dual-signed head, the extracted
# parameter card, and the 71-digit Q — digit for digit.
#
# Phases
# 0 source hygiene (check-book.py)
# 1 build (tectonic via build.sh; fails on TeX errors)
# 1b built-PDF claims (pages, unresolved refs, page-count claims)
# 2 internal congruence (counts and arithmetic inside the book)
# 3 cross-repo congruence (log + fips205 + P25519 siblings)
#
# Environment
# BOOK_LOCAL_ONLY=1 skip phase 3; verdict is downgraded, never ALL GREEN
# ESTATE_ROOT parent dir of the sibling repos (default: ../)
# LTL_DIR / FIPS205_DIR / P25519_FILE override individual siblings
#
# Modes
# ./check-book.sh full run
# ./check-book.sh --selftest adversarial self-test: mutates copies of the
# sources and asserts the button turns RED
set -euo pipefail
cd "$(dirname "$0")"
HERE="$(pwd)"
selftest() {
echo "=== SELFTEST: the button must go red for the right reasons ==="
command -v python3 >/dev/null || { echo "python3 required"; exit 1; }
# the mutated copy keeps the REAL sibling repos: only the book is mutated,
# so a red verdict proves the mutation was caught, not that a repo was lost
local ESTATE; ESTATE="${ESTATE_ROOT:-$(dirname "$HERE")}"
local tmp out pass=0 fail=0
run_copy() {
tmp="$(mktemp -d)"; mkdir -p "$tmp/chapters"
cp main.tex README.md "$tmp/"; cp chapters/*.tex "$tmp/chapters/"
}
run_mutated() { # $1 description, $2 mutation cmd, $3 expected FAIL substring
run_copy
( cd "$tmp" && eval "$2" )
out="$(SKIP_BUILD=1 ESTATE_ROOT="$ESTATE" python3 "$HERE/check-book.py" "$tmp" 2>&1)" \
&& { echo " FAIL mutation NOT caught: $1"; fail=$((fail+1)); rm -rf "$tmp"; return; }
if echo "$out" | grep -q "FAIL.*$3"; then
echo " ok caught for the right reason: $1"; pass=$((pass+1))
else
echo " FAIL red, but not on the expected check ('$3'): $1"; fail=$((fail+1))
echo "$out" | grep " FAIL" | head -3
fi
rm -rf "$tmp"
}
# control: the unmutated copy must pass (proves the harness can go green)
run_copy
if SKIP_BUILD=1 ESTATE_ROOT="$ESTATE" python3 "$HERE/check-book.py" "$tmp" >/dev/null 2>&1; then
echo " ok control: unmutated copy passes"; pass=$((pass+1))
else
echo " FAIL control: unmutated copy should pass but is red"; fail=$((fail+1))
SKIP_BUILD=1 ESTATE_ROOT="$ESTATE" python3 "$HERE/check-book.py" "$tmp" | grep FAIL || true
fi
rm -rf "$tmp"
run_mutated "chapter-count claim drifts (fourteen -> thirteen)" \
"sed -i 's/fourteen chapters/thirteen chapters/' README.md" \
"README chapter count"
run_mutated "a solution deleted (ch13 solhead 13.6 dropped)" \
"sed -i 's/\\\\solhead{13.6}/% gone/' chapters/ch13-second-summit.tex" \
"ch13: exercises == solutions"
run_mutated "one digit of the 71-digit Q changed in ch07" \
"sed -i 's/740582127325613583022312264370627886761/740582127325613583022312264370627886762/' chapters/ch07-primality-certificates.tex" \
"printed Q == repository"
run_mutated "leaf-count claim drifts (nineteen leaves -> twenty)" \
"sed -i \"s/log's nineteen leaves/log's twenty leaves/\" chapters/ch14-attestation-protocol.tex" \
"ch14 'nineteen leaves'"
run_mutated "worst-case arithmetic drifts (3,824 -> 3,689)" \
"sed -i 's/3{,}824/3{,}689/' chapters/ch13-second-summit.tex" \
"ch13 prints worst total"
run_mutated "a chapter stops ending on its checkpoint" \
"printf '\n\\\\begin{aha}\nstray box after the checkpoint\n\\\\end{aha}\n' >> chapters/ch05-numbers-and-automation.tex" \
"ch05.*last environment is checkpoint"
run_mutated "week plan and heading diverge (heading says fifteen)" \
"sed -i 's/A fourteen-week plan/A fifteen-week plan/' main.tex" \
"week-plan heading"
echo
if [ "$fail" -gt 0 ]; then
echo "SELFTEST RED: $fail defect(s) in the button itself"; exit 1
fi
echo "SELFTEST GREEN: $pass/$pass (control + 7 mutations, each caught on its own check)"
exit 0
}
[ "${1:-}" = "--selftest" ] && selftest
echo "=== Phase 1: build ==="
./build.sh
command -v pdfinfo >/dev/null && command -v pdftotext >/dev/null || {
echo "FAIL: poppler-utils (pdfinfo/pdftotext) required"; exit 1; }
python3 "$HERE/check-book.py" "$HERE"
echo
if [ "${BOOK_LOCAL_ONLY:-0}" = "1" ]; then
echo "VERDICT: build green + local claims green; cross-repo NOT verified."
else
echo "ALL GREEN — the book builds and every countable claim matches"
echo "measured reality (sources, PDF, transparency log, extracted code)."
fi

BIN
main.pdf

Binary file not shown.