A check that cannot see a class of defect reports the same clean as a check that looked
A document converter is a probe that reports on content it does not control: the renderer mapped RIGHTWARDS ARROW to the "fi" ligature in 21 places, including the paper's own hierarchy description and its knowability-tier table, and the first verifier called the file clean because its normaliser stripped the arrow from both sides before comparing — fixed by registering real TrueType families, by comparing raw rather than normalised page text, and by printing every transliteration instead of applying it silently.
Status
Shipped, 27 September 2026. All 45 documents render and verify clean.
Tools: tools/md-to-pdf.py, tools/verify-pdf.py,
tools/make-design-volume.py.
The problem
The operator could not read the .md files comfortably and asked for PDFs — first
of the paper, then of all 41 design records.
Why it matters
These documents are the project's memory and the source the technical paper is written from. A format nobody reads is a format that stops being maintained.
But there is a sharper reason, which this work demonstrated on itself: a document converter is a probe that reports on content it does not control, and it has the same failure mode as everything else in this project. It can drop a table, clip a column, or mangle a character, and the output still looks finished.
What was built
md-to-pdf.py — typesets the repo's Markdown with reportlab, already a
dependency for the operator guides. No LaTeX, no pandoc, no headless browser.
Headings, pipe tables, fenced code, blockquotes, lists, task lists, horizontal
rules, and a clickable contents page. Batch mode with -d.
verify-pdf.py — checks the PDF against its source: every heading present
and in order, every table cell and code line present, no text past the margin,
and every non-ASCII character intact.
make-design-volume.py — binds all 41 records plus the index into one
75-page volume in date order, headings demoted one level so the contents page
lists records rather than their internal sections. The intermediate Markdown is
kept so that "is the source or the renderer wrong?" can be answered by reading
it.
Output: 42 per-record PDFs (87 pages), the bound volume (75 pages), and the paper set (37 + 3 + 2 pages).
The defect that matters
A RIGHTWARDS ARROW was being rendered as the "fi" ligature.
The base-14 Type1 fonts reportlab defaults to are limited to WinAnsi, which has
no →. Rather than failing, reportlab mapped it to a ligature slot. So
Lot → Asset → Hardware became Lot fi Asset fi Hardware, in 21 places across
the documents, including the paper's own hierarchy description and its
knowability-tier table.
PAPER.pdf had already been reported clean by the first verifier.
The reason is the finding. The checker compared source against output through a normaliser that stripped everything except letters and digits — which is exactly how it tolerated wrapped table cells. That same normalisation deleted the arrow from both sides before comparing, so a corrupted arrow and a correct arrow were indistinguishable to it.
A check that cannot see a class of defect reports the same clean as a check that looked and found nothing.
That is the thesis of the paper this pipeline was built to render, arriving uninvited in the renderer's own test harness.
Fixed by registering real TrueType families (Arial for text, Consolas for
code) for genuine Unicode coverage, and transliterating only what no font can
show — currently one character, ★, reported on every run rather than applied
silently. The verifier gained a character-integrity check that compares the
raw page text, not the normalised form.
Three further defects, all found by the checker
Column widths ignored their own content. A flat 15mm minimum is narrower
than 2026-09-22 needs at 8.4pt, so the date column of a 23-row evidence table
wrapped mid-token onto a second line, doubling the table's height. Minimum
column width is now derived from each column's longest unbreakable word.
Commit hashes lost their last character — rendering as b36e3a
with the b on the next line. The minimum was measured with the body font, but
those cells are inline code and render in the wider monospace face. Now measured
with the face each cell will actually use.
Inline code inside a heading was set at a fixed 8.6pt, which inside a 15pt heading put the fragment on its own baseline and split the line in two. Code now inherits the surrounding size, and inside a heading uses the bold monospace face so it does not read as a lighter patch.
What was deliberately NOT built
No new toolchain. reportlab was already a dependency. Installing LaTeX or a headless browser to typeset a Markdown file would have added a large dependency to a repository whose own kiosk work established that a dependency you have to install is one you do not have.
The per-record PDFs are not tracked in git. They regenerate from tracked sources with one command, and each embeds a font subset, so committing all 42 would add ~6.7MB to history on every regeneration. The bound volume is tracked, because that is the one people read.
Silent transliteration was rejected. A character the font cannot show could have been mapped quietly to an ASCII equivalent. That is precisely the defect above with a nicer face on it, so every substitution is printed.
How it was proved
45 documents, all reporting VERDICT: clean: 42 per-record PDFs, the bound
volume, and the three paper documents.
Checked per document: headings present and monotonic, table cells and code lines present, non-ASCII characters intact, nothing past the right margin. The architecture diagram in the paper renders with its widest point at 447pt against a 544pt frame.
The emphasis tokeniser was unit-checked against six inputs before use, including the overlapping case that first broke the build.
Two rounds of false alarms in the checker were fixed in the checker, not waved away: wrapped table cells read as missing, and the running footer's repeated title read as a heading out of order. Only after those were corrected did the single genuine defect stand out — which is the argument for keeping a verifier's false-alarm rate near zero.
Open questions
- Whether the per-record PDFs should be tracked after all. Currently ignored; the operator may prefer them in the repository for backup, at the cost of history size.
- The font choice depends on Arial and Consolas being present. On a machine
without them the tool falls back to the base-14 fonts and will transliterate
→, reporting that it did. It has not been run on such a machine. - Four long Windows registry paths in one record still break mid-token, because an 88-character path cannot fit any column on A4. Acceptable, and reported by the checker as "present but broken mid-token" rather than as loss.
A published copy. Commit references and internal identifiers have been removed and the operator is not named; the engineering, the counts and the stated limits are unchanged.