Skip to content

整书构建:把 20 个单元装配成离线单文件书(HTML + A4 PDF) - #32

Open
zhuguang-ZFG wants to merge 21 commits into
lintsinghua:mainfrom
zhuguang-ZFG:offline-book
Open

zhuguang-ZFG wants to merge 21 commits into
lintsinghua:mainfrom
zhuguang-ZFG:offline-book

Conversation

@zhuguang-ZFG

@zhuguang-ZFG zhuguang-ZFG commented Oct 26, 2026 •

Copy link
Copy Markdown

这是什么

给书加一层构建时的装配:yuyu-book/ 把 20 个单元(前言 + 15 章 + 4 附录)合成每语言一份自包含 HTML(中文 5.5 MB / 英文 5.6 MB,断网双击即读,不加载任何外部字体、图片或脚本)与一份 A4 排版的 PDF(中文 466 页 / 15.3 MB,英文 506 页 / 9.9 MB)。构建器对正文只读不写;正文本身在本 PR 里只被人工改过 24 行(中英各 12 行,行号一处未动)——三处自己跟自己矛盾的数字,见「改动面」。产物由 CI 以 artifact 发布,不进版本库。

做了什么

  • 314 张 Mermaid 图预渲染成 SVG 内嵌(构建时渲染,运行时零 JS)。配色按书的七组色重染:正文里 696 条 classDef、192 条 style 自带的 Material/Tailwind 色按色相并入书色,图版看起来像本书自绘。缓存键 = 配色指纹 + mermaid 版本 + 浏览器版本 + 图源哈希,改一个色值旧图自动失效。
  • 通俗层(只叠加、不改写,可整块 --no-plain 关掉):每章一句白话导读带加一个比方;102 条术语库,其中 44 条会在正文首现处画虚线、屏幕悬停出白话浮窗——一句白话定义、一句「为什么非这样不可」、一个比方,纸面退回章首新词面板;44 条主干术语的原理句全部取自正文自身的论证,书末另排「原理札记」一页,按提出这条理由的那一章归类并链回正文。每个小节盖「起步 / 主干 / 进阶」徽,前言据此给三条阅读路线。
  • 书的结构页:封面、版本与许可、修订史、阅读路线、全书线路图(一条自绘蛇形路走完四个部)、带页码的目录、分部过渡页与每篇题刻、每章「本章问题 / 读完交付 / 本章边界」罗盘与来源锚点、原理札记、来源与许可页、书末主题索引(含 122 条中文词条,中文读者不必先想到英文原名再去 A 底下找)。
  • 来源登记可核:49 条来源钉到具体版本——20 条 GitHub 引用带完整 40 位 SHA(tag 经 API 解引用),社区引用带读取日期并显式标注非规范性;其中 14 条取自开源 harness 生态(router、opencode、OpenHands、ACP、Playwright MCP、Codex/Gemini CLI 等),位置写得很清楚:对照面,不是依据。许可门禁禁止 copyleft 资产,只存快照与用途说明、不转载第三方正文。
  • 26 份手写线描图(扉页四马古车、20 枚章徽、4 幅题刻、章末一枚驿卒斗笠),加上章眉走线、章末车辙、分部道路、提示块墨记,全部是 currentColor 内联矢量:屏幕、暗色模式与纸面共用同一份图,没有一处用字体符号或位图凑数。
  • 按语层:每章章末车辙上方一行老驿卒的旁白(notes.json,20 条),书末「按语集」收齐并链回各章。术语锁要求每句含一个已注册词形、且该词形就在这一单元自己的源文件里——旁白不许凭空造词,也不许替书稿发表它没说过的论断。
  • PDF 后处理:侧边书签、奇偶不同的页眉、页脚页码、目录页码盖章,正文里的「参见第 N 章」自动成为可点击链接;太宽放不下竖版的图版独占一张横向 A4,图里的文字在 PDF 里仍可选中、可检索。书末主题索引的每一行也印出它所指的那一页——这一样盖不上去:PyMuPDF 的 search_for 是子串匹配(Session 会把 SessionStart 那一行也认成自己),而一个锚点被几十行共用、行面几乎相同,落点无从唯一确定。于是索引走两趟打印:第一趟只用来量(读 Chromium 留在 PDF 里的命名目标),页码写进行里本来就预留的空位(按 data-ref 认领,不按字面),第二趟才是成品。真机复核:中文 238 行、英文 116 行全部填上;凡 PDF 里留了链接注释的行(中文 237 行——Chromium 本就漏写其中一行的注释,英文 116 行)逐行核对,印出的数字等于该单元真正落在那一页,且那一页确实印着该单元自己的标题。

改动面

相对上游,本 PR 共动 81 个文件:65 个新增(yuyu-book/ 构建器与数据、CREDITS.md / STRUCTURE.md / CONTRIBUTING.md / LICENSE / Makefile 与 Book CI),16 个既有文件被改。既有文件里工程侧只有 4 个:两份 README 各加一节「离线整书」、.gitignore 忽略派生物、scripts/check_book.py 的读取显式加 utf-8(Windows 控制台默认码页不是 utf-8 时它会崩);其余 12 个是正文。

正文只被人工改过 24 行(中英各 12 行,行号一处未动),修的是三处自己跟自己矛盾的数字:第 8 章通篇说 26 个生命周期事件,而它自己那张汇总表只有 22 行;§13.5.1 标题写「三个维度」,正文与图都是五个,而「前四个维度」那句列的是字段名、还漏了 model;工具数在三处分别是 45+、六十多个,与附录 B 逐条数出来的 55。现在统一为 22 / 五个 / 55,附录 B 第一次在开头报出自己的「12 类 / 55 个」,第 13 章 0.1–0.3ms × N 的启动开销也随之从 6–18ms 改为 5.5–16.5ms。除第 1 章那一格「45+ 工具」外,mermaid 图源未动。

构建器对正文只读不写:标题规范化、软断点、脚注、图版重染全部发生在构建时。

质量门

python -m unittest discover -s yuyu-book/tests 402 项,含 WCAG 4.5 对比度、打印契约(图版/表格/代码在纸上收回版心、来源卡与原理行不跨页断开、章末按语与车辙不成两页、主题索引每行恰一枚页码空位、每章收在它结束的那张纸上)、CSS 书写顺序契约,以及一次真实的 build.py --check。六类门禁专门盯「说错」:

  • 总数对表门禁:正文声明的总数必须等于它下面那张表/那张图——第 8 章说的事件数等于汇总表行数、附录 B 数出的工具数等于全书每一处引用它的句子(连 0.1–0.3ms × N 的算术一起核)、§13.5.1 的维度数以图版画出的枚数为准;被替换掉的旧措辞(45+、六十多个、26 个事件、三个维度)只要再出现就让测试红。同一道门现在也管按语:老驿卒那句里的数字要么不写,写了就必须能被重算——附录 B 那行因此从「五十五枚」改写成 55 枚腰牌 / 55 tallies,写成汉字的数目对门禁是隐形的,这本书正是被一个写成汉字的约数骗过一次。
  • 证据门禁:每条原理必须指向代码围栏之外、且含该术语已登记词形的正文行,中英各一条,长度分别限 52 字 / 190 字符;指向书外、落在样例块里、或英文句里还留着汉字,构建直接失败。这道门在编写过程中当场拒了三处指针,逼着把两处「正文把句首大写」的真实词形补进词形表,而不是改指一句更弱的话。
  • 开关自证门禁:--no-plain 承诺整层关掉,代码里导读带却从 manifest 直接取文而漏网(关掉后成品照样带 20 条白话);现在装配对账多一道——开着带数等于单元数、关掉必须为 0,统计行报出 lead bands,并有一条端到端测试真跑 --check --no-plain 双语。
  • 两趟自证门禁:索引页码是先量后印的,「留过宽度的地方不会因为一个数字而挪页」这件事不能靠祈祷——folio_drift() 比较量的一趟与印的一趟各自的目的地,任何一个数字与量到的不符就让构建失败;实测往一次真打印里塞一个错页码,构建当场回话 index folio(s) moved ... ('ch07', (132, 1))。装配处另有一道对账:索引每行必须恰有一枚空位,交给打印趟的行表必须与页面真正落下的行同名同序,错开一位就是每个数字都落到别人的头词上。
  • 空白页门禁:构建 PDF 后逐页量正文带内的文字块、矢量绘制与图片,任一页为空即 BuildError——排版改动把某章挤成一张白页,是这类整书最常见的静默回归。
  • 文档数字门禁:README / STRUCTURE / CREDITS / 修订史里每一个仓库可算出的数字(术语条数与 core 数、原理句条数、图版份数、来源条数、主题索引条目、单元数、测试项数)都由测试重算比对;句子被改写而数字没跟上、或数字过期,测试直接红。

新增的 Book CI 跑:内容门 → 图门 → 两套单测 → 双语审计装配 → 双语 PDF → artifact(发布步带 if-no-files-found: error,什么都没产出就是红;这个键名本身由一项测试钉住——拼错的 action 输入只会给条注解照样绿,那道门等于不存在)。fork 上已跑绿:https://github.com/zhuguang-ZFG/claude-code-book/actions/runs/40499800613 —— 4 分 26 秒。第一次 attempt 红在 Install CJK fonts:azure 镜像把 61.2 MB 的 fonts-noto-cjk 拖到 12 分钟步骤时限被杀,与本书无关(那道时钟正是为这种镜像加的),重跑即过。PDF 步骤现在每种语言打印两趟(索引页码先量后印)。本 PR 的 workflow 若显示待批准,是仓库 Actions 设置对 fork PR 的默认策略,不是构建失败。

已知取舍

  • 单文件 5.5–5.6 MB 是「离线可读」的代价,--split 留作后续。
  • 索引页码要先量后印,所以 --pdf 每种语言多打印一趟;行尾多出的数字也要地方,两栏各行短了一截,整书因此多一张纸(465 → 466 / 505 → 506),那张纸有内容,不是空转。
  • SVG 由本机浏览器渲染,Edge/Chrome 升级会改变字节;缓存键含浏览器版本,修订史会记录这件事。
  • 中英两版行差不同,PDF 页数与断页各自独立审计,anchor 不跨语言复用。

yuyu-book/ is a manifest-driven builder that merges the Chinese and English
manuscript into a single self-contained HTML file per language, plus an
A4-typeset PDF. The manuscript is only ever read, never written.

- 314 Mermaid diagrams pre-rendered to SVG with one book-wide theme, cached by
  (palette, mermaid version, browser version, source hash); the recolouring pass
  folds the 696 classDef / 192 style colours in the sources into seven book
  families, so the plates look drawn for this book
- a plain-language layer that only adds: 100-term glossary with hover
  explanations on screen and a new-terms panel per chapter on paper, a
  vernacular lead band per chapter, and a starter/main/advanced chip per
  section, which feeds three reading routes in the foreword
- chapter compass (question / deliverable / boundary + source anchors), a
  pinned source registry (35 entries, GitHub refs pinned to a full SHA),
  revision history, page-numbered contents, a route map of the whole book,
  part dividers, and a subject index generated from appendix D
- 25 hand-drawn line illustrations, currentColor only, so screen, dark mode and
  paper share one copy of each drawing
- PDF post-processing: bookmarks, recto/verso running heads, foot folios, and
  clickable "see Chapter N" references
- 342 builder tests, including the CSS and print contracts and a real
  build.py --check run
- CREDITS.md: the drawn assets, the official documentation the text leans on,
  the GitHub and community sources behind the citations, and how to ask for a
  correction
- STRUCTURE.md: what each builder module owns, the contract each CI gate
  enforces, and which command produces what
- CONTRIBUTING.md: the local loop (install, check, build) and the rules a change
  has to satisfy — the manuscript stays the only content source, the licence
  gate rejects copyleft assets, GitHub refs carry a full SHA, community refs
  carry an access date
- LICENSE: the CC BY-NC-SA 4.0 the book is already published under
- both READMEs gain the offline-book section; Makefile wraps the four commands
Book CI runs the content gate, the diagram gate, both test suites, an audit-only
assembly of both editions, and then the two PDFs, uploading dist/ as an artifact
so the derived book never has to live in git.

check_book.py gains an explicit utf-8 on its reads: it crashed on a Windows
console whose default code page is not utf-8.
The ruts and the article's print padding were fixed so that a unit closes on
the page it ends on, but nothing kept a regression from shipping: build_pdf now
sweeps every page's body band before it saves and refuses to write a PDF with a
sheet carrying nothing but a running head and a folio. Text, vector drawing and
image all count as content, and the band is measured against each page's own
height, so a fold-out plate page is never mistaken for a hole.

This also settles the question of what the Linux artifact looks like: the
rendering there uses Chromium and Noto CJK instead of Edge and YaHei, so the
metrics differ, and the gate now says so on the runner rather than only on the
machine that shipped the fix.
A twenty-bullet revision history and a module map both quote counts, and every
count I could check was stale: twenty-four shipped drawings is now twenty-five,
and the builder suite is at 347, not 342. The v0.3.1 note also made the book's
page total its own headline, which meant adding a sentence to that note changed
the pagination and invalidated the sentence — the print pass now reports only
what it guarantees (0 empty sheets, policed by the build) and leaves 446/489 to
the README, where the number is about the artifact rather than the anecdote.
One run spent 26 of its 30 minutes pulling fonts-noto-cjk at 25 kB/s from
azure.archive.ubuntu.com, timed out before reaching the Chromium fetch, and
reported itself as a bare "cancelled" with the culprit buried in the log --
while a healthy run needs under 4 minutes end to end.

The fonts and the browser are now separate steps with 12 and 15 minutes, so a
crippled mirror names itself, and apt re-picks from the runner's mirrorlist
instead of sitting on one host. The job gets 45 minutes: enough for a slow but
living mirror to still ship both PDFs, short enough to catch a build that truly
hangs.
`Agent Harness` and the main loop were the only vocabulary in Chapter 1 with no
plain-language entry: the book's own title was the one thing the 通俗层 never
explained. Two core glosses went in, with the chariot doing the analogy work --
the horse only pulls, while shafts, yoke and carriage decide what gets hauled.
A chapter panel may carry 22 new terms and Chapters 1 and 2 sat at exactly 22,
so 串行 and 快照, transparent to a Chinese reader and still searchable in
Appendix D, dropped to extra and paid for the two additions.

The audit behind this found two more things lying: Appendix D holds 122 terms,
not the 100 both READMEs still claimed, and both docs promised that `extra`
terms reach the chapter panel -- the panel has always been exactly the marked
set, which is now asserted instead of described.

test_docclaims recomputes every count the repository docs quote from the data
they describe (glossary rows, gloss registry, subject index, drawings, sources,
plates per edition, and the suite size the newest revision note states), and it
fails on a reworded sentence as readily as on a stale number. Pinning those is
worth the friction: a pointer written from memory -- Main Loop aimed at the
Agent Harness row -- had just been refused by validate_plain, which is exactly
the failure mode a build gate catches and a human reader never sees.
An audit of every number the plain layer asserts found one it could not pay
for: Chapter 8's band said 26 lifecycle events because the chapter says 26 in
four places -- and the table the chapter itself labels "all 26 events" lists
22 rows. A reader who counts is told by the book's own summary line that they
miscounted, so the band now names the categories the events cover rather than a
figure the manuscript cannot enumerate. The fix stays in the builder layer; the
chapter is untouched and still says 26, which is the author's call to make, not
mine -- the discrepancy is reported, not edited away.

Two English bands had also drifted narrower than their Chinese twins: Appendix D
dropped the related-terms column from its own description, and Chapter 13's
"figures are illustrative" hedge had shrunk to milliseconds only, while the
chapter's disclaimer covers the cost arithmetic too.
Appendix D files every term under its English name -- correct for the
English book, wrong for the Chinese one, where the index was 116 rows of
Latin and a reader who had just met 自动压缩 in ch 7 had to think to look
under A. The translation column is a headword source too now, split on
the slash that lets one term carry several names.

The Chinese rows are zh-only, so the English edition renders none of
them; "中" sorts after Z by itself, which keeps the renderer free of a
special case for where the block goes.

Docs re-pinned by test_docclaims: index 116 -> 238, suite 350 -> 352,
zh PDF 447 -> 450 pages after the extra block was set.
The 中文 block is the only part of the book ordered by radical and stroke
count -- Unicode files the ideographs that way, and the sort falls out of
the codepoint with no work at all. Unexplained, a reader would take the
order for a pile, so the block states its rule in one muted line.

zh PDF 450 -> 452 pages after the note was set; both editions reprinted
with 0 empty sheets.
At 390px a line that cannot fit `CompactionResult` drops the whole token
and spreads the six glyphs left behind across the row. English buys its
way out with hyphens; CJK has nothing to hyphenate, so below 30rem the
Chinese edition reads ragged-right while wide screens and paper keep the
flush edge.
The book named its seams but never checked them against someone else's
harness, so a reader could not tell a real boundary from a Claude Code
habit. Fourteen permissively-licensed repositories join the registry as
cross-checks -- a swappable model route, another context strategy, a
second protocol, what permissions degrade to when nobody is watching --
each wired to the chapters whose boundary it tests, and each pinned to a
tag dereferenced through the GitHub API to a full 40-character SHA.

Two errors the checking caught before they could ship: sst/opencode has
moved to anomalyco/opencode, and the Gemini CLI README hands its
unpaid tier to Antigravity CLI. Both went into `snapshot` rather than
being copied forward as stale self-descriptions.
The plain layer said what a term means and what it is like, but not why it has
to be that way -- the question a first-time reader is actually asking. All 45
core glosses now carry a `why` line backed by `why_evidence`: one prose line per
language, lifted from the manuscript's own argument, refused when it lands in a
code fence or quotes an unregistered surface form. The popover grew to three
lines, and the book gained a back-matter page, 原理札记 / Why It Works This Way,
that files each reason under the chapter arguing for it.
A reader counts rows. Chapter 8 claimed 26 lifecycle events while its own
summary table lists 22, section 13.5.1 was headed three dimensions while the
text and its plate draw five, and the tool count appeared as 45+, as
sixty-odd, and as the 55 rows Appendix B enumerates. The manuscript now says
22, five and 55 in both languages, with the appendix stating its own total and
Chapter 13's per-tool arithmetic following it.

These are the first edits to the book text, made by hand and approved; the
builder still never writes to it. Five gates in test_docclaims keep the class
of error open: a claimed count must equal the rows it summarises, the cache
dimensions are counted from the plate that draws them, and the superseded
wording fails the suite if it reappears.
24 manuscript lines, twelve per language; the rest of the 32 sit in the two
READMEs and the compass questions. Saying "32 lines of text" overstates how much
book prose moved.
`if-no-files-available` is not an input of actions/upload-artifact, and
Actions only annotates an unknown key instead of failing the step, so the
one rule meant to turn "the build published nothing" into a red run had
never been live. The key is corrected and a test pins the whole input set
of that step, because the failure mode of a mistyped action input is a
green checkmark, not an error.
Two ways this layer was not reaching the reader, and both were the same
miss: help that stops where the book's own typography gets dense.

Half the registry's vocabulary is only ever written as an identifier. An
`AbortController` inside a sentence had no prose first mention for the
annotator to find, so the dashed line and its popover landed nowhere.
Inline <code> is now a place marks may be drawn -- but only for shapes a
reader cannot misread as an ordinary word (`_`, digits, camelCase seams),
never inside a code plate, where the copy gets pasted out whole. Where
that pushed a chapter past its 22-term panel, `union-type` was demoted to
`extra` rather than the cap being moved.

The other half is voice: an old postman now sits above the wheel ruts at
the foot of every unit with one line about the leg he just ran, and a
back-matter page collects all twenty in reading order. Commentary is not
allowed to become a second manuscript, so it carries its own gate -- a
term lock. A note must hold a registered term form that also stands in
that unit's own source file, stay under 36 characters in Chinese and 130
in English, and keep Han characters out of the English edition. The lock
refused five drafts on the spot: Chapter 1's Chinese text writes
`Agent Harness`, Chapter 14 writes 规划模式 and Plan Mode, and Appendix D
capitalises the term -- the lock reads spelling, not synonym.

Both land in one commit because the docs and their gate quote them
together: one README sentence carries the code-span rule and the keeper,
and test_docclaims recomputes its numbers from that same sentence.
…to the sheet

The keeper layer shipped with a term lock and no arithmetic. Two gaps, and the
first is the same shape as the errata this branch already fixed: a number
nobody can recheck.

A total written in characters is invisible to every gate here — 六十多个工具
lived beside an appendix listing 55 for exactly that reason. Appendix B's note
now reads 55 枚腰牌 / 55 tallies, and a new test_docclaims rule recomputes it
against the inventory: any digit run in notes.json must be one a test can
recount, while a note that states no number still states none. The gate was
fed 56, a spelled-out total, and a stray 12 in Chapter 3 — all three came back
named by hand.

The second gap is a layout rule that held only because nobody had edited the
CSS yet: the print block already glued the note to its ruts, kept each
collection row whole and stopped the hover lean on paper, with nothing
asserting any of it. Three regexes hold that line now.

STRUCTURE and CONTRIBUTING gained the same rule where an author would meet it:
the notes.json row, the 改哪里 row, and the paragraph telling contributors that
the postman may not invent a number either.

The suite moves 392 -> 394 and the revision note closing 0.3.1 quotes the
suite size, so it moves with it.
…ayer

The docs say `--no-plain` closes the plain layer whole, lead bands included,
and the band's own docstring calls it the one plain-language device that reads
the same on paper and on screen. But its text lives in `manifest.guides` rather
than in the glossary, so it kept assembling while the marks, panels, tier
chips, principle notes and keeper notes all went away -- and that flag exists
precisely to compare the book with the layer against the book without it. The
comparison was running two editions that both had it.

`build_edition` takes `plain_layer` and threads it to `_article`, where the band
is emitted. The assembly gained the reconciliation the other devices already
carry: bands must equal the unit count with the layer on and be zero with it
off, and the build line reports the count, so a device that later forgets the
flag fails the run instead of shipping a book whose off state is a lie. One
end-to-end test really runs `--check --no-plain` for both editions and watches
the zeroes.

The books readers get are unchanged: bands, marks, panel rows, headings,
drawings and tables all count the same as the previous build, and both PDFs
still land on 465 and 505 sheets -- only the revision page grew by the note
describing this.
The back index named a chapter without naming a sheet. Every row reserved a
folio slot (`min-width:1.6rem`), and the screen hid it correctly — a screen has
no pages — but `@media print` inherited that hiding and the PDF pass knew only
the contents rows, so 238 Chinese rows and 116 English ones printed with
nothing at the right edge.

Stamping them the way contents rows are stamped was tried first and does not
work: PyMuPDF's `search_for` matches substrings, so `Session` also claims
`SessionStart`'s line, and one anchor is shared by dozens of near-identical
rows, so no landing point is unique. The measuring print also showed Chromium
dropping one row's link annotation, which rules out annotations as a locator.

The index now takes two prints. `pdf.measure()` reads the named destinations the
first print leaves behind to learn which sheet each unit landed on;
`pages.fill_index_folios()` writes those folios into the slots the rows already
reserve, keyed by the anchor each row links to rather than by the headword's
spelling; the second print is the book. The width was reserved, so pagination
should not move, and `pdf.folio_drift()` turns that into an assertion: the final
print re-reads its destinations and the build refuses if any number has moved.
Handing a real print a wrong folio came back as `index folio(s) moved ...
('ch07', (132, 1))`.

Two reconciliations come with the pass: every printed row carries exactly one
slot, and the row list handed to the PDF pass must match the rendered index in
name and order — off by one, every folio would sit above somebody else's
headword.

Both editions were printed for real: 237 of the 238 Chinese rows and all 116
English ones verify against the printed sheet through their own link
annotations, and every sheet a number names carries that unit's own heading.
The digits do take paper — each column lost a strip of row, so the book gains a
sheet (465 -> 466, 505 -> 506) — and `--pdf` now prints twice per language.

Seven tests, the suite 395 -> 402, and the revision note closing 0.3.1 quotes
the suite size, so it moves with it. README, STRUCTURE and CONTRIBUTING gained
the two-pass rule where a contributor meets it: the pdf.py row, the pages.py
row, the derivation table, the index file row and the gates paragraph.
The opener was where the book read worst: the card of new terms is the tallest
block a chapter owns, and sitting above the prose it spent the whole first
sheet on furniture — Chapter 2 lists 22 terms in Chinese, so the chapter's
first sentence landed on page two. The card now trails the unit, beside the
keeper's line and the wheel ruts, and the prose starts where a reader expects
it.

Slimming it was the other half of that. The card had a dashed frame, a tinted
ground and a ┄ tie per row, which made a chapter open on a plate you look
inside; it is a list you read across now — term, one gold dot, plain wording,
two columns on paper. A dashed line means exactly one thing in this book: the
term carries a gloss, so the route legend, the book's road, the keeper's notes
and the citation list keep solid rules. Corners came down from seven to two
tokens and a circle; paper re-inks `--cinnabar` to #9C4630, because the
screen's mix burns straight through a laser printer's white; the contents page
inks its part numerals instead of outlining them and sets its folios in a
tabular column; and the cover stops framing itself twice — a border plus an
outline reads as a plate that printed one line off register — spending the
sheet kicker to foot.

Moving the card to the tail found two bugs it had been hiding. Fragmented
across a page boundary, it left one English sheet holding nothing but the
clause "every time" and a caption line; with the note moved above the list and
rows unable to break, chapter 7 then printed an entirely blank page, which the
empty-sheet gate caught — 1.6px of bottom padding that would not fit the text
block was enough for Chromium to reserve a sheet for it. On paper the plate no
longer breaks at all: 22 terms in two columns is a third of the text block and
always fits, and each row keeps its air above rather than below.

Fifteen contracts in `DesignVocabulary` and `ChapterOpening` hold the
vocabulary down: a fifth radius, a second dashed rule, a cover that frames
itself twice or a card that opens a chapter all fail the build. 417 builder
tests green. Both editions were printed for real — 467 Chinese pages, 508
English, no empty sheet, and the index folios unmoved between the two passes.
The landscape plate pages still carry three-quarters of a sheet's void; that
one waits on a decision about the page, not the stylesheet.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant