Repository navigation
整书构建:把 20 个单元装配成离线单文件书(HTML + A4 PDF) - #32
Open
zhuguang-ZFG wants to merge 21 commits into
Open
zhuguang-ZFG wants to merge 21 commits into
zhuguang-ZFG wants to merge 21 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
这是什么
给书加一层构建时的装配:
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 发布,不进版本库。做了什么
classDef、192 条style自带的 Material/Tailwind 色按色相并入书色,图版看起来像本书自绘。缓存键 = 配色指纹 + mermaid 版本 + 浏览器版本 + 图源哈希,改一个色值旧图自动失效。--no-plain关掉):每章一句白话导读带加一个比方;102 条术语库,其中 44 条会在正文首现处画虚线、屏幕悬停出白话浮窗——一句白话定义、一句「为什么非这样不可」、一个比方,纸面退回章首新词面板;44 条主干术语的原理句全部取自正文自身的论证,书末另排「原理札记」一页,按提出这条理由的那一章归类并链回正文。每个小节盖「起步 / 主干 / 进阶」徽,前言据此给三条阅读路线。currentColor内联矢量:屏幕、暗色模式与纸面共用同一份图,没有一处用字体符号或位图凑数。notes.json,20 条),书末「按语集」收齐并链回各章。术语锁要求每句含一个已注册词形、且该词形就在这一单元自己的源文件里——旁白不许凭空造词,也不许替书稿发表它没说过的论断。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/tests402 项,含 WCAG 4.5 对比度、打印契约(图版/表格/代码在纸上收回版心、来源卡与原理行不跨页断开、章末按语与车辙不成两页、主题索引每行恰一枚页码空位、每章收在它结束的那张纸上)、CSS 书写顺序契约,以及一次真实的build.py --check。六类门禁专门盯「说错」:45+、六十多个、26 个事件、三个维度)只要再出现就让测试红。同一道门现在也管按语:老驿卒那句里的数字要么不写,写了就必须能被重算——附录 B 那行因此从「五十五枚」改写成55 枚腰牌 / 55 tallies,写成汉字的数目对门禁是隐形的,这本书正是被一个写成汉字的约数骗过一次。--no-plain承诺整层关掉,代码里导读带却从 manifest 直接取文而漏网(关掉后成品照样带 20 条白话);现在装配对账多一道——开着带数等于单元数、关掉必须为 0,统计行报出 lead bands,并有一条端到端测试真跑--check --no-plain双语。folio_drift()比较量的一趟与印的一趟各自的目的地,任何一个数字与量到的不符就让构建失败;实测往一次真打印里塞一个错页码,构建当场回话index folio(s) moved ... ('ch07', (132, 1))。装配处另有一道对账:索引每行必须恰有一枚空位,交给打印趟的行表必须与页面真正落下的行同名同序,错开一位就是每个数字都落到别人的头词上。新增的 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 的默认策略,不是构建失败。已知取舍
--split留作后续。--pdf每种语言多打印一趟;行尾多出的数字也要地方,两栏各行短了一截,整书因此多一张纸(465 → 466 / 505 → 506),那张纸有内容,不是空转。