From 2aab7c039f6b5ab33f8f3e2b818770f370d6363c Mon Sep 17 00:00:00 2001 From: zhuguang-ZFG <168892777+zhuguang-ZFG@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:23:53 +0800 Subject: [PATCH 01/21] feat(book): assemble the 20 units into one offline book 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 --- yuyu-book/build.py | 374 +++ yuyu-book/glosses.json | 2988 +++++++++++++++++++++++ yuyu-book/illustrations/apxa.svg | 1 + yuyu-book/illustrations/apxb.svg | 1 + yuyu-book/illustrations/apxc.svg | 1 + yuyu-book/illustrations/apxd.svg | 1 + yuyu-book/illustrations/ch01.svg | 1 + yuyu-book/illustrations/ch02.svg | 1 + yuyu-book/illustrations/ch03.svg | 1 + yuyu-book/illustrations/ch04.svg | 1 + yuyu-book/illustrations/ch05.svg | 1 + yuyu-book/illustrations/ch06.svg | 1 + yuyu-book/illustrations/ch07.svg | 1 + yuyu-book/illustrations/ch08.svg | 1 + yuyu-book/illustrations/ch09.svg | 1 + yuyu-book/illustrations/ch10.svg | 1 + yuyu-book/illustrations/ch11.svg | 1 + yuyu-book/illustrations/ch12.svg | 1 + yuyu-book/illustrations/ch13.svg | 1 + yuyu-book/illustrations/ch14.svg | 1 + yuyu-book/illustrations/ch15.svg | 1 + yuyu-book/illustrations/cover.svg | 1 + yuyu-book/illustrations/front.svg | 1 + yuyu-book/illustrations/part-p1.svg | 1 + yuyu-book/illustrations/part-p2.svg | 1 + yuyu-book/illustrations/part-p3.svg | 1 + yuyu-book/illustrations/part-p4.svg | 1 + yuyu-book/manifest.json | 1019 ++++++++ yuyu-book/reading-paths.json | 134 + yuyu-book/requirements.txt | 6 + yuyu-book/revisions.json | 94 + yuyu-book/sources.json | 585 +++++ yuyu-book/subject-index.json | 2554 +++++++++++++++++++ yuyu-book/tests/__init__.py | 22 + yuyu-book/tests/test_art.py | 138 ++ yuyu-book/tests/test_audit.py | 104 + yuyu-book/tests/test_build_contracts.py | 454 ++++ yuyu-book/tests/test_crossrefs.py | 117 + yuyu-book/tests/test_manifest.py | 274 +++ yuyu-book/tests/test_mermaid.py | 472 ++++ yuyu-book/tests/test_pages.py | 435 ++++ yuyu-book/tests/test_plain.py | 526 ++++ yuyu-book/tests/test_render_anchors.py | 343 +++ yuyu-book/tests/test_sources.py | 223 ++ yuyu-book/yuyu_book/__init__.py | 1 + yuyu-book/yuyu_book/art.py | 76 + yuyu-book/yuyu_book/audit.py | 105 + yuyu-book/yuyu_book/core.py | 306 +++ yuyu-book/yuyu_book/mermaid.py | 315 +++ yuyu-book/yuyu_book/pages.py | 462 ++++ yuyu-book/yuyu_book/pdf.py | 250 ++ yuyu-book/yuyu_book/plain.py | 340 +++ yuyu-book/yuyu_book/render.py | 604 +++++ yuyu-book/yuyu_book/style.py | 1227 ++++++++++ 54 files changed, 14573 insertions(+) create mode 100644 yuyu-book/build.py create mode 100644 yuyu-book/glosses.json create mode 100644 yuyu-book/illustrations/apxa.svg create mode 100644 yuyu-book/illustrations/apxb.svg create mode 100644 yuyu-book/illustrations/apxc.svg create mode 100644 yuyu-book/illustrations/apxd.svg create mode 100644 yuyu-book/illustrations/ch01.svg create mode 100644 yuyu-book/illustrations/ch02.svg create mode 100644 yuyu-book/illustrations/ch03.svg create mode 100644 yuyu-book/illustrations/ch04.svg create mode 100644 yuyu-book/illustrations/ch05.svg create mode 100644 yuyu-book/illustrations/ch06.svg create mode 100644 yuyu-book/illustrations/ch07.svg create mode 100644 yuyu-book/illustrations/ch08.svg create mode 100644 yuyu-book/illustrations/ch09.svg create mode 100644 yuyu-book/illustrations/ch10.svg create mode 100644 yuyu-book/illustrations/ch11.svg create mode 100644 yuyu-book/illustrations/ch12.svg create mode 100644 yuyu-book/illustrations/ch13.svg create mode 100644 yuyu-book/illustrations/ch14.svg create mode 100644 yuyu-book/illustrations/ch15.svg create mode 100644 yuyu-book/illustrations/cover.svg create mode 100644 yuyu-book/illustrations/front.svg create mode 100644 yuyu-book/illustrations/part-p1.svg create mode 100644 yuyu-book/illustrations/part-p2.svg create mode 100644 yuyu-book/illustrations/part-p3.svg create mode 100644 yuyu-book/illustrations/part-p4.svg create mode 100644 yuyu-book/manifest.json create mode 100644 yuyu-book/reading-paths.json create mode 100644 yuyu-book/requirements.txt create mode 100644 yuyu-book/revisions.json create mode 100644 yuyu-book/sources.json create mode 100644 yuyu-book/subject-index.json create mode 100644 yuyu-book/tests/__init__.py create mode 100644 yuyu-book/tests/test_art.py create mode 100644 yuyu-book/tests/test_audit.py create mode 100644 yuyu-book/tests/test_build_contracts.py create mode 100644 yuyu-book/tests/test_crossrefs.py create mode 100644 yuyu-book/tests/test_manifest.py create mode 100644 yuyu-book/tests/test_mermaid.py create mode 100644 yuyu-book/tests/test_pages.py create mode 100644 yuyu-book/tests/test_plain.py create mode 100644 yuyu-book/tests/test_render_anchors.py create mode 100644 yuyu-book/tests/test_sources.py create mode 100644 yuyu-book/yuyu_book/__init__.py create mode 100644 yuyu-book/yuyu_book/art.py create mode 100644 yuyu-book/yuyu_book/audit.py create mode 100644 yuyu-book/yuyu_book/core.py create mode 100644 yuyu-book/yuyu_book/mermaid.py create mode 100644 yuyu-book/yuyu_book/pages.py create mode 100644 yuyu-book/yuyu_book/pdf.py create mode 100644 yuyu-book/yuyu_book/plain.py create mode 100644 yuyu-book/yuyu_book/render.py create mode 100644 yuyu-book/yuyu_book/style.py diff --git a/yuyu-book/build.py b/yuyu-book/build.py new file mode 100644 index 00000000..04c57483 --- /dev/null +++ b/yuyu-book/build.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +"""yuyu-book: assemble the offline single-file editions of《御舆》. + +Usage (from the repo root): + python yuyu-book/build.py --check + python yuyu-book/build.py --lang zh + python yuyu-book/build.py --lang en --pdf + python yuyu-book/build.py --regen-index +""" +from __future__ import annotations + +import argparse +import re +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from yuyu_book import (art, audit, mermaid as mm, pages, plain, + render) # noqa: E402 +from yuyu_book.core import (BOOK_DIR, REPO_ROOT, BuildError, dump_json, + expected_ids, load_configs, load_json, load_unit, + read_text, unit_stream) # noqa: E402 +from yuyu_book.style import CSS, key_mark, page, rut, trace, wheel # noqa: E402 + + +def glossary_to_index(manifest: dict) -> list[dict]: + """Build the subject index from appendix D tables (both languages).""" + apxd = next(a for a in manifest["appendices"] if a["id"] == "apxd") + entries: dict[str, dict] = {} + + def add(term: str, lang: str, anchor: str, label: str): + key = term.strip().lower() + if not key or len(term) > 64: + return + e = entries.setdefault(key, {"key": key, "letter": "A", lang: {"term": term.strip(), "refs": []}}) + for lk in ("zh", "en"): + e.setdefault(lk, {"term": term.strip(), "refs": []}) + first = re.search(r"[A-Za-z]", term) + if first: + e["letter"] = first.group(0).upper() + refs = e[lang]["refs"] + if not any(r["anchor"] == anchor for r in refs): + refs.append({"anchor": anchor, "label": label}) + + rx_zh = re.compile(r"第\s*(\d{1,2})\s*章") + rx_en = re.compile(r"Chapter\s+(\d{1,2})", re.I) + for lang in ("zh", "en"): + text = read_text(REPO_ROOT / apxd[lang]) + fence = None + for line in text.split("\n"): + m = re.match(r"^\s*(`{3,}|~{3,})(.*)$", line) + if m: + token, info = m.group(1), m.group(2).strip() + if fence is None: + fence = token + elif token[0] == fence[0] and not info: + fence = None + continue + if fence is not None or not line.startswith("|"): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if len(cells) < 2 or set("".join(cells)) <= set("-: "): + continue + term = cells[0].strip("*` ") + tail = " ".join(cells[2:]) + rx = rx_zh if lang == "zh" else rx_en + nums = [int(n) for n in rx.findall(tail) if 1 <= int(n) <= 15] + if not nums or len(term) < 2: + continue + for n in nums[:4]: + add(term, lang, f"ch{n:02d}", + f"第 {n} 章" if lang == "zh" else f"Ch {n}") + out = sorted(entries.values(), key=lambda e: (e["letter"], e["key"])) + return [e for e in out if e.get("zh", {}).get("refs") + or e.get("en", {}).get("refs")] + + +def build_edition(lang: str, manifest: dict, sources: dict, *, + mode: str, browser: str | None, autolink: bool, + out_dir: Path, do_pdf: bool, edge: str | None, + glosses: dict | None = None, + paths: dict | None = None) -> Path: + ids = expected_ids(manifest) + mapping = {i: i for i in ids} + glosses = glosses or {} + paths = paths or {} + layer = plain.Layer(glosses, lang) + + units = [] + for unit_def, kind, part in unit_stream(manifest): + u = load_unit(unit_def, kind, lang, REPO_ROOT) + u.part = part # type: ignore[attr-defined] + units.append(u) + + all_blocks, caps = [], {} + for u in units: + caps.update(mm.captions_for(read_text(REPO_ROOT / u.path), u.blocks, + fallback=pages.ui("plate_fallback", lang))) + all_blocks.extend(u.blocks) + if mode == "render": + diagrams = mm.render_all(all_blocks, caps, browser) + expect_svgs = len(diagrams) + else: + diagrams = mm.source_fallback(all_blocks, caps) + expect_svgs = None + + payloads = [] + for u in units: + payloads.append(render.render_unit(u, mapping, diagrams=diagrams, + autolink=autolink, gloss=layer, + tiers=paths.get(u.id))) + by_id = {p["unit"].id: p for p in payloads} + layer.check_coverage() + + # ---------------- assemble ---------------- + body: list[str] = [] + body.append(pages.cover(manifest, lang)) + body.append(pages.imprint(manifest, lang)) + rev = load_json(BOOK_DIR / "revisions.json")["entries"] + body.append(pages.revisions_page(rev, lang)) + body.append(pages.how_to_read(manifest, lang, paths)) + + front_id = manifest["front"]["id"] + toc_entries: list[dict] = [ + {"kind": "row", "anchor": front_id, "no": pages.FRONT_NO, + "title": by_id[front_id]["unit"].title}] + outline: list[tuple[int, str, str]] = [ + (1, by_id[front_id]["unit"].display or pages.ui("front", lang), + front_id)] + rail = [f'", "") + audit.audit(html_text, n_units=len(units), expect_svgs=expect_svgs) + + # the dashed marks and the chapter panels come from the same hit list; if + # they ever diverge, paper readers would chase underlines that are nowhere + # explained (there is no hover in print) + n_marks = html_text.count('') + n_chips = html_text.count('' + label = u.title + elif kind == "appendix": + seal = f'' + label = u.title + else: + seal = "" + # the emblem rides at the end of the head row, so the top of every unit is + # title + seal + drawing rather than a wide empty band over the sheet + emblem = art.emblem(u.id) + title_html = (f'{seal}

{pages.esc(label)}

{emblem}') + if seal or emblem: + title_html = f'
{title_html}
' + # the foreword is the one unit whose number-free title is the eyebrow text, + # so it renders no eyebrow at all rather than "前言" twice above the H1 + eyebrow_html = (f'

{pages.esc(" · ".join(eyebrow))}

' + if eyebrow else "") + # the head closes on a drawn wire rather than a ruled line: same job, and it + # carries the unit id so no two chapters run the same trace + head = (f'
{eyebrow_html}{title_html}' + f'{trace(u.id)}
') + cls = "chapter" if kind == "chapter" else ("unit" if kind == "front" + else "unit appendix") + lead = pages.lead_band(guide, lang) + compass = pages.compass(u, guide, sources, lang) if guide else "" + panel = pages.new_terms_panel(payload.get("new_terms") or [], lang, kind) + # keep rendered body but drop its own H1 (re-synthesised above) + body_html = re.sub(r"

]*>.*?

|

.*?

", "", payload["html"], + count=1) + # the ruts go inside the body, glued to the block they close: left as a + # trailing sibling they can fall alone onto a sheet the next unit's forced + # page break then empties + body_html = render.glue_closer(body_html, rut(u.id)) + return (f'
{head}' + f'{lead}{compass}{panel}
{body_html}
' + f'
') + + +def main(argv=None): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--lang", default="both", choices=["zh", "en", "both"]) + ap.add_argument("--pdf", action="store_true") + ap.add_argument("--check", action="store_true", + help="build into a temp dir and only validate") + ap.add_argument("--out-dir", default=str(BOOK_DIR / "dist")) + ap.add_argument("--browser", default=None, + help="explicit msedge/chrome executable for mermaid & PDF") + ap.add_argument("--edge", default=None, help="PDF browser executable") + ap.add_argument("--mermaid", default="render", choices=["render", "source"]) + ap.add_argument("--no-autolink", action="store_true") + ap.add_argument("--no-plain", action="store_true", + help="skip the plain-language layer (glosses, tiers)") + ap.add_argument("--regen-index", action="store_true") + args = ap.parse_args(argv) + + try: + manifest, sources = load_configs() + glosses, paths = ({}, {}) if args.no_plain else plain.load_plain() + if not args.no_plain: + plain.validate_plain(glosses, paths, manifest) + if args.regen_index: + entries = glossary_to_index(manifest) + dump_json(BOOK_DIR / "subject-index.json", entries) + zh_n = sum(1 for e in entries if e["zh"]["refs"]) + en_n = sum(1 for e in entries if e["en"]["refs"]) + print(f"subject index: {len(entries)} terms ({zh_n} zh refs, " + f"{en_n} en refs)") + if args.lang == "both" and not args.check: + pass # index-only run still produces files; build continues + langs = ["zh", "en"] if args.lang == "both" else [args.lang] + out_dir = Path(args.out_dir) + if args.check: + out_dir = Path(tempfile.mkdtemp(prefix="yuyu-check-")) + for lang in langs: + print(f"[{lang}]") + build_edition(lang, manifest, sources, mode=args.mermaid, + browser=args.browser, autolink=not args.no_autolink, + out_dir=out_dir, do_pdf=args.pdf and not args.check, + edge=args.edge, glosses=glosses, paths=paths) + print("OK" if not args.check else "CHECK OK") + except BuildError as e: + print(f"BUILD FAILED: {e}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/yuyu-book/glosses.json b/yuyu-book/glosses.json new file mode 100644 index 00000000..a88627d0 --- /dev/null +++ b/yuyu-book/glosses.json @@ -0,0 +1,2988 @@ +{ + "async-generator": { + "term": { + "zh": "异步生成器", + "en": "async generator" + }, + "forms": { + "zh": [ + "异步生成器" + ], + "en": [ + "async generator", + "AsyncGenerator", + "Async Generator" + ] + }, + "plain": { + "zh": "边算边一段段往外交结果的函数,取一段停一段", + "en": "a function that hands out results one piece at a time and can pause between pieces" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:18", + "第一部分-基础篇/01-智能体编程的新范式.md:240", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:16" + ], + "analogy": { + "zh": "像流水线厨师,做好一盘递一盘,不等全桌菜齐", + "en": "a cook passing dishes out as each is ready, not after the whole meal is done" + } + }, + "yield": { + "term": { + "zh": "yield", + "en": "yield" + }, + "forms": { + "zh": [ + "yield" + ], + "en": [ + "yield" + ] + }, + "plain": { + "zh": "交出一个结果,然后停在原地等对方来取下一个", + "en": "hand out one value, then sit still until the caller asks for the next" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:252", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:18", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:18" + ], + "analogy": { + "zh": "出菜口:炒好一盘举起来,服务员取走才炒下一盘", + "en": "the wok pass: hold up a plate and wait for the runner to take it" + } + }, + "backpressure": { + "term": { + "zh": "背压", + "en": "backpressure" + }, + "forms": { + "zh": [ + "背压控制", + "背压" + ], + "en": [ + "backpressure control", + "backpressure" + ] + }, + "plain": { + "zh": "下游处理不过来,就反过来让上游慢一点", + "en": "when the consumer falls behind, it pushes the producer to slow down" + }, + "priority": "core", + "evidence": [ + "00-前言.md:129", + "第一部分-基础篇/01-智能体编程的新范式.md:316", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:314", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:314" + ], + "analogy": { + "zh": "下水道堵了,得有人去把水龙头拧小", + "en": "a clogged drain sends someone running to turn the tap down" + } + }, + "producer-consumer": { + "term": { + "zh": "生产者与消费者", + "en": "producer and consumer" + }, + "forms": { + "zh": [ + "生产者", + "消费者" + ], + "en": [ + "producer", + "consumer" + ] + }, + "plain": { + "zh": "一边只管做、一边只管用,两边速度不同就得协调", + "en": "one side only makes, the other only takes; their speeds rarely match" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:22", + "第一部分-基础篇/01-智能体编程的新范式.md:316", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:22" + ], + "analogy": { + "zh": "分拣带跑得比唯一一名装车员快,包裹就堆成山", + "en": "a parcel belt moving faster than the single worker loading trucks" + } + }, + "race-condition": { + "term": { + "zh": "竞态条件", + "en": "race condition" + }, + "forms": { + "zh": [ + "竞态条件" + ], + "en": [ + "race condition", + "Race condition" + ] + }, + "plain": { + "zh": "两件事同时抢着改同一份数据,结果取决于谁手快", + "en": "two paths touch the same data at once; the outcome depends on timing" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:382", + "第三部分-高级模式篇/11-技能系统与插件架构.md:87", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:87" + ], + "analogy": { + "zh": "两个收银员同时从同一格抽屉拿钱找零,账必乱", + "en": "two cashiers grabbing change from the same drawer at the same second" + } + }, + "idempotent": { + "term": { + "zh": "幂等", + "en": "idempotent" + }, + "forms": { + "zh": [ + "幂等" + ], + "en": [ + "idempotent" + ] + }, + "plain": { + "zh": "同一件事做一次和做五遍,最后结果一模一样", + "en": "doing it once or five times leaves exactly the same result" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:208", + "附录/D-术语表.md:222", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:208" + ], + "analogy": { + "zh": "电梯按钮按一次和按十次,都只来一趟", + "en": "pressing the lift button once or ten times still brings one trip" + } + }, + "serialization": { + "term": { + "zh": "序列化", + "en": "serialization" + }, + "forms": { + "zh": [ + "序列化", + "反序列化" + ], + "en": [ + "serialization", + "serialize", + "deserialization" + ] + }, + "plain": { + "zh": "把内存里的对象摊平成一段文字,好存好发;反过来叫还原", + "en": "flattening an object into text so it can be stored or sent, and undoing it" + }, + "priority": "core", + "evidence": [ + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:165", + "第三部分-高级模式篇/09-子智能体与Fork模式.md:329", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:165" + ], + "analogy": { + "zh": "家具拆平装箱寄走,到货后再按原样拼回去", + "en": "flat-packing furniture into a box, then rebuilding it at the other end" + } + }, + "state-machine": { + "term": { + "zh": "状态机", + "en": "state machine" + }, + "forms": { + "zh": [ + "状态机" + ], + "en": [ + "state machine", + "State Machine" + ] + }, + "plain": { + "zh": "把流程画成几个格子,箭头规定哪格能跳到哪格", + "en": "a flow drawn as boxes, with arrows listing the legal jumps" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:328", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:342", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:220" + ], + "analogy": { + "zh": "售货机:投币、选货、出货,跳错一步机器不认", + "en": "a vending machine: coin in, select, dispense, never out of order" + } + }, + "dependency-injection": { + "term": { + "zh": "依赖注入", + "en": "dependency injection" + }, + "forms": { + "zh": [ + "依赖注入" + ], + "en": [ + "dependency injection", + "Dependency Injection" + ] + }, + "plain": { + "zh": "要用的东西不自己造,而是由外面递进来,随时能换", + "en": "you don't build what you need; it's handed in from outside and swappable" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:292", + "第一部分-基础篇/01-智能体编程的新范式.md:254", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:12" + ], + "analogy": { + "zh": "食材由助手送进来,主厨换成谁,菜谱照用", + "en": "a prep cook delivers the ingredients, so the chef can be swapped out" + } + }, + "test-mock": { + "term": { + "zh": "mock", + "en": "mock" + }, + "forms": { + "zh": [ + "mock" + ], + "en": [ + "mock", + "mocks", + "spy" + ] + }, + "plain": { + "zh": "测试时用的假替身,照剧本回应,不去碰真的外部服务", + "en": "a stand-in built for tests that answers from a script instead of the real thing" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:304", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:418", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:302" + ], + "analogy": { + "zh": "演习时请人扮客户,照台词问答,不是真买家", + "en": "an actor playing the customer during rehearsal, reading from a script" + } + }, + "singleton": { + "term": { + "zh": "单例", + "en": "singleton" + }, + "forms": { + "zh": [ + "惰性单例", + "单例" + ], + "en": [ + "singleton" + ] + }, + "plain": { + "zh": "全程只许存在一份的对象,谁来拿都是同一个", + "en": "one shared instance for the whole program; everyone gets the same one" + }, + "priority": "core", + "evidence": [ + "第三部分-高级模式篇/11-技能系统与插件架构.md:87", + "第三部分-高级模式篇/11-技能系统与插件架构.md:97", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:87" + ], + "analogy": { + "zh": "办公室只有一台打印机,所有人都排同一条队", + "en": "the office has one printer and everyone queues at the same machine" + } + }, + "observer-pattern": { + "term": { + "zh": "观察者模式", + "en": "observer pattern" + }, + "forms": { + "zh": [ + "观察者模式" + ], + "en": [ + "observer pattern", + "Observer Pattern", + "Observer" + ] + }, + "plain": { + "zh": "谁关心就登记,事一发生挨个通知,发事的不用认识谁", + "en": "anyone interested signs up; when something happens, all get told" + }, + "priority": "core", + "evidence": [ + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:19", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:689", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:19" + ], + "analogy": { + "zh": "物业在业主群发一条通知,不必挨家敲门", + "en": "the property manager posts once in the residents' group chat" + } + }, + "chain-of-responsibility": { + "term": { + "zh": "责任链模式", + "en": "chain of responsibility" + }, + "forms": { + "zh": [ + "责任链模式", + "责任链" + ], + "en": [ + "Chain of Responsibility", + "chain of responsibility" + ] + }, + "plain": { + "zh": "一件事依次经手多人,谁有权限谁拦下,否则往后传", + "en": "a request passes down a line; whoever can handle it stops it there" + }, + "priority": "core", + "evidence": [ + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:19", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:687", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:19" + ], + "analogy": { + "zh": "报销单从组长到经理再到财务,一级级往上签", + "en": "an expense form travels from team lead to manager to finance" + } + }, + "generics": { + "term": { + "zh": "泛型", + "en": "generics" + }, + "forms": { + "zh": [ + "泛型接口", + "泛型" + ], + "en": [ + "generic", + "Generic" + ] + }, + "plain": { + "zh": "先留个空类型位,用时才填,一份定义套多种数据", + "en": "a type slot left blank until use, so one definition fits many data kinds" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:26", + "第一部分-基础篇/03-工具系统-Agent的双手.md:32", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:26" + ], + "analogy": { + "zh": "同一款收纳箱,能装鞋也能装书,看放什么", + "en": "one storage bin design that holds shoes or books, depending on you" + } + }, + "union-type": { + "term": { + "zh": "联合类型", + "en": "union type" + }, + "forms": { + "zh": [ + "联合类型" + ], + "en": [ + "union type" + ] + }, + "plain": { + "zh": "一个位置只允许列出的几种形状之一,用前得先分辨是哪种", + "en": "one slot holding any of several listed shapes; you must tell which first" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:61", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:193", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:61" + ], + "analogy": { + "zh": "信封里可能是发票、贺卡或照片,拆开先看是哪样", + "en": "an envelope with an invoice, card, or photo - open it to see which" + } + }, + "closure": { + "term": { + "zh": "闭包", + "en": "closure" + }, + "forms": { + "zh": [ + "闭包" + ], + "en": [ + "closure" + ] + }, + "plain": { + "zh": "函数带着它出生时的环境一起走,别人够不着那份数据", + "en": "a function that carries its birth environment wherever it goes" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:312", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:956", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:312" + ], + "analogy": { + "zh": "店员随身带自己那串仓库钥匙,别的柜台借不到", + "en": "a clerk keeps her own stockroom keys; no other counter can borrow them" + } + }, + "promise": { + "term": { + "zh": "Promise", + "en": "Promise" + }, + "forms": { + "zh": [ + "Promise 链", + "Promise" + ], + "en": [ + "Promise", + "promise" + ] + }, + "plain": { + "zh": "一张“将来给你结果”的凭条,只兑一次,成或败都算交代", + "en": "a receipt for a value that arrives later; it settles exactly once" + }, + "priority": "core", + "evidence": [ + "00-前言.md:129", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:69", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:67" + ], + "analogy": { + "zh": "修表给张取货单,只兑一次:修好了或没修好", + "en": "a claim check from the watchmaker: you redeem it once, done or not" + } + }, + "callback": { + "term": { + "zh": "回调", + "en": "callback" + }, + "forms": { + "zh": [ + "回调地狱", + "回调" + ], + "en": [ + "callback", + "callbacks" + ] + }, + "plain": { + "zh": "把接下来要做的事托给对方,他办完再回头喊你", + "en": "you hand over what to do next and get called when it finishes" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:99", + "00-前言.md:129", + "en/00-Foreword.md:100" + ], + "analogy": { + "zh": "修手机留个号,修好打给你;留十个号就乱成粥", + "en": "leave your number for repairs; chain ten of them and you lose track" + } + }, + "cancellation-propagation": { + "term": { + "zh": "取消传播", + "en": "cancellation propagation" + }, + "forms": { + "zh": [ + "取消传播", + "级联取消" + ], + "en": [ + "cancellation propagation" + ] + }, + "plain": { + "zh": "上面一喊停,底下每一层都收到停,各自收尾", + "en": "a stop call at the top reaches every layer below, each wrapping up" + }, + "priority": "core", + "evidence": [ + "00-前言.md:129", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:316", + "en/00-Foreword.md:100" + ], + "analogy": { + "zh": "导演喊卡,摄影、灯光、场务一层层传下去收工", + "en": "the director calls cut; camera, lights and crew all wrap up" + } + }, + "abort-controller": { + "term": { + "zh": "AbortController", + "en": "AbortController" + }, + "forms": { + "zh": [ + "AbortController" + ], + "en": [ + "AbortController", + "abortController" + ] + }, + "plain": { + "zh": "一个能随时按下的停止开关,正在干活的得自己去瞅它", + "en": "a stop switch anyone can flip; running work must check it to bail out" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:316", + "第一部分-基础篇/03-工具系统-Agent的双手.md:354", + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:685", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:316" + ], + "analogy": { + "zh": "车间的急停按钮,每台机器动手前都先看一眼", + "en": "the shop-floor emergency stop that every machine glances at" + } + }, + "immutable-state": { + "term": { + "zh": "不可变", + "en": "immutability" + }, + "forms": { + "zh": [ + "不可变性", + "不可变" + ], + "en": [ + "immutable", + "immutability" + ] + }, + "plain": { + "zh": "数据不涂改,要改就整份抄新的再改,旧的照原样留着", + "en": "never overwrite data; copy it, change the copy, keep the original" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:378", + "00-前言.md:145", + "en/00-Foreword.md:116" + ], + "analogy": { + "zh": "病历不改原页,每次另开一张,原单留档可查", + "en": "a chart is never erased; each change goes on a fresh page" + } + }, + "side-effect": { + "term": { + "zh": "副作用", + "en": "side effect" + }, + "forms": { + "zh": [ + "副作用" + ], + "en": [ + "side effect", + "side effects" + ] + }, + "plain": { + "zh": "算数的同时还改了外面:写文件、发消息、动全局状态", + "en": "besides computing, it also changes things outside itself: files, messages, state" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:228", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:441", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:441" + ], + "analogy": { + "zh": "只是去问个路,顺手把人家的花盆碰倒了", + "en": "you only asked for directions, then knocked over their flowerpot" + } + }, + "blocking": { + "term": { + "zh": "阻塞", + "en": "blocking" + }, + "forms": { + "zh": [ + "阻塞" + ], + "en": [ + "blocking", + "block" + ] + }, + "plain": { + "zh": "卡在原地干等,等的时候什么都干不了", + "en": "stuck waiting in place, unable to do anything else meanwhile" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:99", + "第一部分-基础篇/01-智能体编程的新范式.md:267", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:99" + ], + "analogy": { + "zh": "银行只开一个窗口,前面慢,整队人干站着", + "en": "one teller window, and the whole line stands still behind a slow customer" + } + }, + "concurrency": { + "term": { + "zh": "并发", + "en": "concurrency" + }, + "forms": { + "zh": [ + "并发安全", + "并发" + ], + "en": [ + "concurrency", + "concurrent" + ] + }, + "plain": { + "zh": "几件事同时在推进,抢同一份资源就得安排先后", + "en": "several tasks in flight at once, competing for shared things" + }, + "priority": "core", + "evidence": [ + "00-前言.md:115", + "第一部分-基础篇/01-智能体编程的新范式.md:57", + "en/00-Foreword.md:86" + ], + "analogy": { + "zh": "四个厨师一口锅,同时开火就排班定谁先用", + "en": "four cooks on one stove: someone has to set the order" + } + }, + "circuit-breaker": { + "term": { + "zh": "断路器", + "en": "circuit breaker" + }, + "forms": { + "zh": [ + "断路器模式", + "断路器", + "熔断" + ], + "en": [ + "circuit breaker", + "Circuit Breaker" + ] + }, + "plain": { + "zh": "连着失败够多次就先跳过这步,别硬试拖累全局", + "en": "after enough failures in a row it skips the call instead of dragging everything" + }, + "priority": "core", + "evidence": [ + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:106", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:266", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:6" + ], + "analogy": { + "zh": "家里电闸跳了,先断一路,别让整栋楼烧起来", + "en": "the fuse blows and cuts one line before the whole house burns" + } + }, + "graceful-degradation": { + "term": { + "zh": "优雅降级", + "en": "graceful degradation" + }, + "forms": { + "zh": [ + "优雅降级", + "降级" + ], + "en": [ + "graceful degradation", + "degrade" + ] + }, + "plain": { + "zh": "出问题时不整个瘫掉,砍掉非必要部分,保住还能用的", + "en": "on failure, drop the nice-to-haves and keep the core running" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:246", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:86", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:84" + ], + "analogy": { + "zh": "航班超售,先减免费餐食,飞机照飞", + "en": "the flight is oversold: cut the free meals but still take off" + } + }, + "exponential-backoff": { + "term": { + "zh": "指数退避", + "en": "exponential backoff" + }, + "forms": { + "zh": [ + "指数退避" + ], + "en": [ + "exponential backoff", + "backoff" + ] + }, + "plain": { + "zh": "每次重试之间等得越来越久,别追着人家猛敲门", + "en": "wait longer between each retry instead of hammering a struggling service" + }, + "priority": "core", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:248", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:248" + ], + "analogy": { + "zh": "店没开门,隔一分、两分、四分再去问一次", + "en": "if the shop isn't open, ask again after one, two, then four minutes" + } + }, + "cache-hit-rate": { + "term": { + "zh": "缓存命中率", + "en": "cache hit rate" + }, + "forms": { + "zh": [ + "缓存命中率" + ], + "en": [ + "cache hit rate", + "hit rate" + ] + }, + "plain": { + "zh": "要用的东西已在手边备好的比例,低了就得现跑现取", + "en": "how often the thing you want is already kept nearby, ready to grab" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:344", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:196", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:196" + ], + "analogy": { + "zh": "常用工具挂墙上,伸手就有和得跑仓库的比例", + "en": "the share of jobs where the tool is on the wall, not in the warehouse" + } + }, + "dead-code-elimination": { + "term": { + "zh": "死代码消除", + "en": "dead code elimination" + }, + "forms": { + "zh": [ + "死代码消除", + "死代码" + ], + "en": [ + "dead code elimination", + "dead code" + ] + }, + "plain": { + "zh": "出厂前把永远跑不到的代码整块扔掉,成品更小也不透密", + "en": "the build strips code that can never run, shrinking the artifact" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:134", + "第一部分-基础篇/03-工具系统-Agent的双手.md:136", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:136" + ], + "analogy": { + "zh": "搬家时从没穿过的衣服不进箱,箱子小一半", + "en": "clothes you never wear never make it into the moving box" + } + }, + "tree-shaking": { + "term": { + "zh": "tree-shaking", + "en": "tree-shaking" + }, + "forms": { + "zh": [ + "tree-shaking" + ], + "en": [ + "tree-shaking" + ] + }, + "plain": { + "zh": "打包时抖落用不上的分支代码,只留真被摇到的那几根", + "en": "shaking unused branches off the bundle so only reachable code ships" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:138", + "第四部分-工程实践篇/13-流式架构与性能优化.md:363", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:138" + ], + "analogy": { + "zh": "装箱前把没贴标签的配件全抖回货架,箱里只剩要用的", + "en": "before packing, shake off every unlabelled part back onto the shelf" + } + }, + "lazy-loading": { + "term": { + "zh": "延迟加载", + "en": "lazy loading" + }, + "forms": { + "zh": [ + "延迟加载", + "懒加载" + ], + "en": [ + "lazy loading", + "lazy load" + ] + }, + "plain": { + "zh": "用到的那一刻才去取,没用到就一直不搬进来", + "en": "fetch a thing only when it's first needed, not all at the start" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:277", + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:69", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:69" + ], + "analogy": { + "zh": "餐馆不预先摆满桌,来一桌客人摆一桌", + "en": "the cafe sets out tables only as guests arrive" + } + }, + "circular-dependency": { + "term": { + "zh": "循环依赖", + "en": "circular dependency" + }, + "forms": { + "zh": [ + "循环依赖" + ], + "en": [ + "circular dependency", + "circular dependencies" + ] + }, + "plain": { + "zh": "A 要等 B 先就绪,B 又回头等 A,谁都起不了步", + "en": "A waits for B ready and B waits for A ready, so neither can start" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:277", + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:69", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:277" + ], + "analogy": { + "zh": "开箱要钥匙,钥匙又锁在箱子里", + "en": "the key to the box is locked inside the box" + } + }, + "memory-overflow": { + "term": { + "zh": "内存溢出", + "en": "memory overflow" + }, + "forms": { + "zh": [ + "内存溢出" + ], + "en": [ + "memory overflow", + "out of memory" + ] + }, + "plain": { + "zh": "攒着不放的东西太多,撑爆存放处,程序当场倒下", + "en": "too much kept at once runs out of room and the process dies" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:316", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:316" + ], + "analogy": { + "zh": "购物车越堆越高,最后顶穿了仓库屋顶", + "en": "piles keep growing until they punch through the storeroom roof" + } + }, + "regex": { + "term": { + "zh": "正则表达式", + "en": "regular expression" + }, + "forms": { + "zh": [ + "正则表达式", + "正则匹配", + "正则" + ], + "en": [ + "regular expression", + "regex", + "Regex" + ] + }, + "plain": { + "zh": "写一串模式符号,让机器照它在文本里筛出匹配的内容", + "en": "a pattern string the machine uses to pick out matching text" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:222", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:328", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:222" + ], + "analogy": { + "zh": "门卫只认“两位字母加四位数字”,照格式放车", + "en": "the gatekeeper accepts only two letters plus four digits" + } + }, + "sandbox": { + "term": { + "zh": "沙箱", + "en": "sandbox" + }, + "forms": { + "zh": [ + "沙箱模式", + "沙箱" + ], + "en": [ + "sandbox", + "sandboxed" + ] + }, + "plain": { + "zh": "圈一块受限的地方让代码玩,出事也溅不到外面", + "en": "an enclosed area where code plays without touching anything outside" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:331", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:115", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:115" + ], + "analogy": { + "zh": "小区里的儿童沙坑,玩脏了也不糟蹋别家花园", + "en": "a kids' sandpit inside the compound; the mess stays in it" + } + }, + "least-privilege": { + "term": { + "zh": "最小权限", + "en": "least privilege" + }, + "forms": { + "zh": [ + "最小权限" + ], + "en": [ + "least privilege" + ] + }, + "plain": { + "zh": "只发刚好够干这活的权限,多一分都不给", + "en": "grant only what this task needs and nothing more" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:216", + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:586", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:216" + ], + "analogy": { + "zh": "保洁的工牌只开储物间,开不了金库", + "en": "the cleaner's badge opens the supply room, never the vault" + } + }, + "separation-of-concerns": { + "term": { + "zh": "关注点分离", + "en": "separation of concerns" + }, + "forms": { + "zh": [ + "关注点分离" + ], + "en": [ + "separation of concerns" + ] + }, + "plain": { + "zh": "每块代码只管自己那件事,别把判断和跑腿搅一起", + "en": "each piece of code handles one job and doesn't mix in another's" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:75", + "00-前言.md:130", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:75" + ], + "analogy": { + "zh": "后厨切配的管切配、掌勺的管掌勺,不互相插手", + "en": "the prep cook preps, the wok cook cooks; they don't cross lines" + } + }, + "event-stream": { + "term": { + "zh": "事件流", + "en": "event stream" + }, + "forms": { + "zh": [ + "事件流" + ], + "en": [ + "event stream" + ] + }, + "plain": { + "zh": "发生的事一条条排队送出来,边到边处理,不等攒齐", + "en": "happenings come out one by one and are handled as they arrive" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:318", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:69", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:69" + ], + "analogy": { + "zh": "传送带一件件出包裹,扫一件贴一件", + "en": "parcels come down the belt one at a time, scanned as they land" + } + }, + "streaming": { + "term": { + "zh": "流式", + "en": "streaming" + }, + "forms": { + "zh": [ + "流式" + ], + "en": [ + "streaming", + "Streaming" + ] + }, + "plain": { + "zh": "结果一点点到就一点点用;书里指模型回复逐字涌出", + "en": "handle bits as they land; here, the model's reply arrives word by word" + }, + "priority": "core", + "evidence": [ + "00-前言.md:24", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:18", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:18" + ], + "analogy": { + "zh": "接自来水不必等水缸装满,开着龙头就能洗", + "en": "wash under the running tap instead of waiting for the tub to fill" + } + }, + "event-emitter": { + "term": { + "zh": "事件发射器", + "en": "event emitter" + }, + "forms": { + "zh": [ + "事件发射器" + ], + "en": [ + "event emitter", + "EventEmitter" + ] + }, + "plain": { + "zh": "会广播动静的物件,谁想听谁自己来挂上监听", + "en": "an object that broadcasts happenings to whoever subscribed" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:99", + "第一部分-基础篇/01-智能体编程的新范式.md:252", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:99" + ], + "analogy": { + "zh": "门口那台大喇叭,要听的人自己搬椅子来坐", + "en": "a loudspeaker by the door; listeners bring their own chairs" + } + }, + "runtime-validation": { + "term": { + "zh": "运行时验证", + "en": "runtime validation" + }, + "forms": { + "zh": [ + "运行时验证" + ], + "en": [ + "runtime validation", + "Runtime Validation" + ] + }, + "plain": { + "zh": "跑起来那一刻才查数据合不合规格,当场挡住脏输入", + "en": "checking the data while the program runs, so bad input stops right there" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:62", + "00-前言.md:135", + "en/00-Foreword.md:106" + ], + "analogy": { + "zh": "快递进站还得过一台称重和查违禁品的机器", + "en": "packages are still weighed and screened as they enter the depot" + } + }, + "snapshot": { + "term": { + "zh": "快照", + "en": "snapshot" + }, + "forms": { + "zh": [ + "状态快照", + "快照" + ], + "en": [ + "snapshot", + "Snapshot" + ] + }, + "plain": { + "zh": "把某一瞬间的全部原样抄一份留着,之后怎么变都不影响它", + "en": "a copy of everything at one instant, untouched by later changes" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:182", + "第一部分-基础篇/01-智能体编程的新范式.md:380", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:182" + ], + "analogy": { + "zh": "出门前拍张桌面照,回来再乱也有底可查", + "en": "photograph the desk before leaving; later mess can't change the photo" + } + }, + "serial-execution": { + "term": { + "zh": "串行", + "en": "serial" + }, + "forms": { + "zh": [ + "串行" + ], + "en": [ + "serial", + "serially" + ] + }, + "plain": { + "zh": "一件彻底办完才轮到下一件,中间绝不重叠", + "en": "one job finishes completely before the next one starts" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:57", + "第一部分-基础篇/01-智能体编程的新范式.md:224", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:57" + ], + "analogy": { + "zh": "独木桥一次只过一辆车,后头的排队等", + "en": "a single-plank bridge: one vehicle at a time, the rest queue" + } + }, + "yield-star": { + "term": { + "zh": "yield*", + "en": "yield*" + }, + "forms": { + "zh": [ + "yield*" + ], + "en": [ + "yield*" + ] + }, + "plain": { + "zh": "把往外逐个交活这整件事,转包给另一段同样的代码", + "en": "delegate the whole hand-out job to another generator and pass its output along" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:242", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:63", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:63" + ], + "analogy": { + "zh": "组长说“这段小刘全程负责,你们直接找他”", + "en": "the shift lead says: Liu owns this section end to end, go to him" + } + }, + "coroutine": { + "term": { + "zh": "协程", + "en": "coroutine" + }, + "forms": { + "zh": [ + "协程" + ], + "en": [ + "coroutine" + ] + }, + "plain": { + "zh": "能主动让位、之后从让处接着跑的一段流程", + "en": "a routine that gives up the floor and resumes exactly where it stopped" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:22", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:22" + ], + "analogy": { + "zh": "织毛衣打个结放下,明天从那一针接着织", + "en": "tie the yarn and set knitting down; tomorrow you resume that stitch" + } + }, + "strategy-pattern": { + "term": { + "zh": "策略模式", + "en": "strategy pattern" + }, + "forms": { + "zh": [ + "策略模式" + ], + "en": [ + "Strategy Pattern", + "strategy pattern" + ] + }, + "plain": { + "zh": "同一件事备几套做法,按情况挑一套,主干不动", + "en": "several interchangeable ways for one job; pick one, core code unchanged" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/09-子智能体与Fork模式.md:32", + "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md:32" + ], + "analogy": { + "zh": "外卖平台可切“最快”或“最省”路线,骑手照单跑", + "en": "the app swaps fastest or cheapest routing; riders just follow" + } + }, + "adapter-pattern": { + "term": { + "zh": "适配器模式", + "en": "adapter pattern" + }, + "forms": { + "zh": [ + "适配器模式", + "适配器" + ], + "en": [ + "Adapter pattern", + "adapter pattern", + "adapter" + ] + }, + "plain": { + "zh": "中间垫一层做转换,两边各说各话也能接上", + "en": "a middle layer translates so two mismatched sides connect" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:686", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:64", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:64" + ], + "analogy": { + "zh": "旅行转换插头,圆脚插进扁孔,电器不用改", + "en": "a travel plug adapter: round pins into flat holes, no rewiring" + } + }, + "factory-function": { + "term": { + "zh": "工厂函数", + "en": "factory function" + }, + "forms": { + "zh": [ + "工厂函数" + ], + "en": [ + "factory function", + "Factory Function" + ] + }, + "plain": { + "zh": "专门造东西的一道工序,规格统一,要几个现造几个", + "en": "a routine whose only job is building objects to one spec, on demand" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:270", + "第一部分-基础篇/03-工具系统-Agent的双手.md:98", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:98" + ], + "analogy": { + "zh": "酱料台每份照同一配方现调,不提前大批做", + "en": "the sauce station builds each portion to the same recipe, to order" + } + }, + "json-schema": { + "term": { + "zh": "JSON Schema", + "en": "JSON Schema" + }, + "forms": { + "zh": [ + "JSON Schema" + ], + "en": [ + "JSON Schema" + ] + }, + "plain": { + "zh": "一份书面格式说明:这段数据该有哪些字段、各是什么类型", + "en": "a written spec of which fields a payload must carry and their types" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:205", + "第一部分-基础篇/01-智能体编程的新范式.md:212", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:205" + ], + "analogy": { + "zh": "报名表印好必填栏目,填错的当场退回", + "en": "the form prints required boxes; a wrongly filled one bounces back" + } + }, + "destructuring": { + "term": { + "zh": "解构", + "en": "destructuring" + }, + "forms": { + "zh": [ + "解构" + ], + "en": [ + "destructured", + "destructuring" + ] + }, + "plain": { + "zh": "一次把整包数据里的各部件分名别类取出,省得逐个翻", + "en": "pull many named parts out of one bundle in a single move" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:378", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:178", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:182" + ], + "analogy": { + "zh": "快递一到就分拣:笔放桌上、衣服进衣柜", + "en": "the parcel lands: pen to the desk, clothes to the closet, at once" + } + }, + "atomic-update": { + "term": { + "zh": "原子更新", + "en": "atomic update" + }, + "forms": { + "zh": [ + "原子更新" + ], + "en": [ + "atomic update" + ] + }, + "plain": { + "zh": "一组改动要么全生效要么全不生效,不会改到一半", + "en": "a batch of changes either all land or none does, never halfway" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:182", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:182" + ], + "analogy": { + "zh": "点菜要么整单进厨房,不会上两道下两道", + "en": "the order goes to the kitchen whole or not at all" + } + }, + "tombstone": { + "term": { + "zh": "墓碑消息", + "en": "tombstone" + }, + "forms": { + "zh": [ + "墓碑消息" + ], + "en": [ + "tombstone", + "Tombstone" + ] + }, + "plain": { + "zh": "立个牌子写明“这条已作废”,而不是悄悄抹掉", + "en": "a marker saying this entry is dead, instead of silently removing it" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:61", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:57", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:57" + ], + "analogy": { + "zh": "档案柜留一张“此件已撤销”的空夹,免得有人再找", + "en": "a folder marked withdrawn stays so nobody hunts for the original" + } + }, + "deadlock": { + "term": { + "zh": "死锁", + "en": "deadlock" + }, + "forms": { + "zh": [ + "死锁" + ], + "en": [ + "deadlock" + ] + }, + "plain": { + "zh": "两边各握着对方要的东西都不撒手,一起僵住不动", + "en": "each side holds what the other needs and neither lets go" + }, + "priority": "extra", + "evidence": [ + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:465", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:466" + ], + "analogy": { + "zh": "两辆车在独木桥两头互不相让,谁都动不了", + "en": "two cars meet on a narrow bridge and neither reverses" + } + }, + "fail-fast": { + "term": { + "zh": "快速失败", + "en": "fail fast" + }, + "forms": { + "zh": [ + "快速失败" + ], + "en": [ + "fail fast", + "failing fast" + ] + }, + "plain": { + "zh": "一看出事立刻停手报错,不硬撑到后面代价更大", + "en": "stop at the first sign of trouble instead of pushing on at higher cost" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:200", + "第一部分-基础篇/03-工具系统-Agent的双手.md:191", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:200" + ], + "analogy": { + "zh": "发现鸡蛋坏了就停手,别把整锅料都倒下去", + "en": "if the egg is bad, stop; don't pour the whole batter in" + } + }, + "avalanche": { + "term": { + "zh": "雪崩效应", + "en": "avalanche" + }, + "forms": { + "zh": [ + "雪崩效应" + ], + "en": [ + "avalanche" + ] + }, + "plain": { + "zh": "一处出问题把压力传给邻居,一路跟着倒,越滚越大", + "en": "one failure loads its neighbours, which fail too, compounding outward" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:106", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:472", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:106" + ], + "analogy": { + "zh": "一台收银机死机,旁边几条队全堵住了", + "en": "one checkout lane dies and the lines beside it pile up" + } + }, + "standard-streams": { + "term": { + "zh": "标准输入输出", + "en": "standard streams" + }, + "forms": { + "zh": [ + "stdin", + "stdout", + "stderr" + ], + "en": [ + "stdin", + "stdout", + "stderr" + ] + }, + "plain": { + "zh": "程序约定好的三个口:喂进去的话、打印的话、抱怨的话", + "en": "three agreed channels: what you feed in, what it prints, what it complains" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:991", + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:280", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:280" + ], + "analogy": { + "zh": "服务窗口分三栏:收件、出件、投诉,各走各的", + "en": "the counter has three slots: intake, output, complaints" + } + }, + "websocket": { + "term": { + "zh": "WebSocket", + "en": "WebSocket" + }, + "forms": { + "zh": [ + "WebSocket" + ], + "en": [ + "WebSocket" + ] + }, + "plain": { + "zh": "一条一直留着的双向通道,两头随时都能主动开口", + "en": "a connection kept open so either side can speak up any time" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:92", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:110", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:92" + ], + "analogy": { + "zh": "不是写信等回信,而是电话不挂,随时能插话", + "en": "not letters back and forth but a phone line left open" + } + }, + "oauth": { + "term": { + "zh": "OAuth", + "en": "OAuth" + }, + "forms": { + "zh": [ + "OAuth2", + "OAuth" + ], + "en": [ + "OAuth" + ] + }, + "plain": { + "zh": "给第三方一张限额授权的通行条,不必交出主密码", + "en": "hands a third party a limited pass instead of your main password" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:1084", + "第三部分-高级模式篇/09-子智能体与Fork模式.md:663", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:1084" + ], + "analogy": { + "zh": "酒店房卡只开你那间,开不了保险柜和别家门", + "en": "the hotel key opens your room only, not the safe or other doors" + } + }, + "bun-runtime": { + "term": { + "zh": "Bun", + "en": "Bun" + }, + "forms": { + "zh": [ + "Bun" + ], + "en": [ + "Bun" + ] + }, + "plain": { + "zh": "跑这类程序的一套引擎,兼管出厂打包,书里图它起得快", + "en": "the engine that runs and builds the program; chosen here for speed" + }, + "priority": "extra", + "evidence": [ + "00-前言.md:135", + "第一部分-基础篇/01-智能体编程的新范式.md:202", + "en/00-Foreword.md:106" + ], + "analogy": { + "zh": "小作坊一台多功能机床,顶过去两台老机器", + "en": "one multi-tool machine in the shop replaces two older ones" + } + }, + "bundling": { + "term": { + "zh": "打包", + "en": "bundler" + }, + "forms": { + "zh": [ + "打包" + ], + "en": [ + "bundler", + "bundle", + "Bundle" + ] + }, + "plain": { + "zh": "把散在各处的零件压成一份成品,好分发、好起跑", + "en": "squash many source files into one distributable artifact" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/11-技能系统与插件架构.md:480", + "附录/D-术语表.md:127", + "en/Appendices/D-Glossary.md:125" + ], + "analogy": { + "zh": "寄件前把零散东西装进同一个箱、贴同一张单", + "en": "loose parts go into one carton with one label before shipping" + } + }, + "compile-time": { + "term": { + "zh": "编译时", + "en": "compile time" + }, + "forms": { + "zh": [ + "编译期", + "编译时" + ], + "en": [ + "compile time", + "Compile-time", + "compile-time" + ] + }, + "plain": { + "zh": "出厂定稿的那一刻,跟跑起来才现场决定相对", + "en": "the moment the program is built, as opposed to while it runs" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:136", + "附录/C-功能标志速查表.md:272", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:136" + ], + "analogy": { + "zh": "书在印刷厂定稿,便签是读者拿到后自己贴", + "en": "the book is fixed at the printer; sticky notes come later, by readers" + } + }, + "cold-start": { + "term": { + "zh": "冷启动", + "en": "cold start" + }, + "forms": { + "zh": [ + "冷启动" + ], + "en": [ + "cold start" + ] + }, + "plain": { + "zh": "啥都没预热,从零把程序拉起来花的那段时间", + "en": "bringing the program up from nothing, with nothing warmed beforehand" + }, + "priority": "extra", + "evidence": [ + "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md:113", + "en/Part-4-Engineering-Practice/15-Building-Your-Own-Agent-Harness.md:113" + ], + "analogy": { + "zh": "冬天早上第一次发动车,得热一会儿才肯走", + "en": "a winter morning: the engine idles a while before it will move" + } + }, + "prefetch": { + "term": { + "zh": "预取", + "en": "prefetch" + }, + "forms": { + "zh": [ + "预取" + ], + "en": [ + "prefetch", + "prefetching", + "Prefetch" + ] + }, + "plain": { + "zh": "还没用上就先把要的东西搬到手边,用时不必现等", + "en": "fetch what you'll need before you need it, so nothing waits later" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:224", + "第一部分-基础篇/01-智能体编程的新范式.md:228", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:224" + ], + "analogy": { + "zh": "高峰前先把米泡上、料切好,客人一到就开炒", + "en": "soak the rice and chop the veg before the rush, not during it" + } + }, + "garbage-collection": { + "term": { + "zh": "垃圾回收", + "en": "garbage collection" + }, + "forms": { + "zh": [ + "垃圾回收" + ], + "en": [ + "garbage collection", + "garbage-collected" + ] + }, + "plain": { + "zh": "没人再引用的内存由系统定时清走,别的资源仍要自己收尾", + "en": "the runtime reclaims memory nothing references; other resources don't" + }, + "priority": "extra", + "evidence": [ + "第四部分-工程实践篇/13-流式架构与性能优化.md:18", + "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md:18" + ], + "analogy": { + "zh": "物业收走没人认领的杂物,你自己的水桶得自己倒", + "en": "the building hauls away abandoned stuff; you still empty your own bin" + } + }, + "unicode": { + "term": { + "zh": "Unicode", + "en": "Unicode" + }, + "forms": { + "zh": [ + "Unicode" + ], + "en": [ + "Unicode" + ] + }, + "plain": { + "zh": "给天下文字统一编号的一张表,各系统认字不打架", + "en": "one shared numbering table so every system reads the same characters" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:327", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:327" + ], + "analogy": { + "zh": "各国商品统一条码,扫码机不管货是哪国产的", + "en": "one barcode system so the scanner works regardless of country" + } + }, + "cache-invalidation": { + "term": { + "zh": "缓存失效", + "en": "cache invalidation" + }, + "forms": { + "zh": [ + "缓存失效" + ], + "en": [ + "cache invalidation", + "Cache invalidation" + ] + }, + "plain": { + "zh": "备好的那份不再算数,得扔掉重做,白白慢一次", + "en": "the kept copy no longer counts, so it's thrown out and rebuilt" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:196", + "第一部分-基础篇/03-工具系统-Agent的双手.md:177", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:192" + ], + "analogy": { + "zh": "菜单改了价,印好的传单全作废,只能重印", + "en": "prices changed, so the flyers are scrap and need reprinting" + } + }, + "end-to-end": { + "term": { + "zh": "端到端", + "en": "end-to-end" + }, + "forms": { + "zh": [ + "端到端" + ], + "en": [ + "end-to-end" + ] + }, + "plain": { + "zh": "从最开头一路走到最结尾,中间每一环都真连上", + "en": "all the way from the first step to the last, every link real" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:453", + "第四部分-工程实践篇/13-流式架构与性能优化.md:294", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:453" + ], + "analogy": { + "zh": "从下单到签收走一遍,不是只在仓库里试打包", + "en": "order it and receive it, not just rehearse packing in the warehouse" + } + }, + "ab-testing": { + "term": { + "zh": "A/B 实验", + "en": "A/B test" + }, + "forms": { + "zh": [ + "A/B 实验", + "A/B 测试" + ], + "en": [ + "A/B" + ] + }, + "plain": { + "zh": "新旧两版同时上线,各给一部分人用,比谁效果好", + "en": "run two versions for different slices of users at once and compare" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:370", + "附录/C-功能标志速查表.md:272", + "en/Appendices/C-Feature-Flag-Reference.md:272" + ], + "analogy": { + "zh": "收银台左边试新流程、右边留老流程,一周看哪边快", + "en": "one lane tries the new flow, the other keeps the old, then compare" + } + }, + "whitelist": { + "term": { + "zh": "白名单", + "en": "allowlist" + }, + "forms": { + "zh": [ + "白名单", + "黑白名单" + ], + "en": [ + "whitelist", + "allowlist" + ] + }, + "plain": { + "zh": "事先列好许可名册,名册外一律挡在门外", + "en": "a prepared list of the permitted; everything else is denied" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:335", + "00-前言.md:130", + "en/00-Foreword.md:101" + ], + "analogy": { + "zh": "小区门禁认名册,不在册的刷不开", + "en": "the gate opens only for names on the resident list" + } + }, + "symbolic-link": { + "term": { + "zh": "符号链接", + "en": "symbolic link" + }, + "forms": { + "zh": [ + "符号链接" + ], + "en": [ + "symbolic link", + "symlink" + ] + }, + "plain": { + "zh": "一个指路牌式的假条目,打开它其实走到别处的真文件", + "en": "a shortcut entry that quietly sends you to a file somewhere else" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/11-技能系统与插件架构.md:87", + "第三部分-高级模式篇/11-技能系统与插件架构.md:93", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:87" + ], + "analogy": { + "zh": "柜上贴条“东西在储藏室”,你照条去拿,柜里是空的", + "en": "a note says the box is in the storage room, so you go there" + } + }, + "defense-in-depth": { + "term": { + "zh": "纵深防御", + "en": "defense in depth" + }, + "forms": { + "zh": [ + "纵深防御" + ], + "en": [ + "defense in depth", + "Defense in Depth" + ] + }, + "plain": { + "zh": "层层关卡各管一段,漏了一层还有下一层兜着", + "en": "several independent checks in a row; if one leaks, the next catches" + }, + "priority": "extra", + "evidence": [ + "00-前言.md:130", + "第一部分-基础篇/01-智能体编程的新范式.md:333", + "en/00-Foreword.md:101" + ], + "analogy": { + "zh": "机场先值机、再过安检、登机口还查一次证件", + "en": "check-in, then security, then another ID check at the gate" + } + }, + "technical-debt": { + "term": { + "zh": "技术债", + "en": "technical debt" + }, + "forms": { + "zh": [ + "技术债" + ], + "en": [ + "technical debt" + ] + }, + "plain": { + "zh": "为赶工欠下的马虎账,日后要连本带利花时间还", + "en": "the shortcut you take today, paid back with interest later" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:993", + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:584", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:993" + ], + "analogy": { + "zh": "偷懒不洗油烟机,三个月后炒什么都带糊味", + "en": "skip cleaning the hood tonight; in three months every dish tastes burnt" + } + }, + "feature-flag": { + "term": { + "zh": "功能开关", + "en": "feature flag" + }, + "forms": { + "zh": [ + "功能开关", + "功能标志" + ], + "en": [ + "feature flag", + "Feature Flag", + "Feature flag" + ] + }, + "plain": { + "zh": "代码里留的一道闸,不重装机就能让某功能开或关", + "en": "a built-in switch that turns a capability on or off without rebuilding" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:330", + "第一部分-基础篇/03-工具系统-Agent的双手.md:138", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:136" + ], + "analogy": { + "zh": "店里总电闸旁边的分闸,想关凉菜间就扳下那一路", + "en": "a sub-breaker beside the main one: cut the salad station only" + } + }, + "token-budget": { + "term": { + "zh": "Token 预算", + "en": "token budget" + }, + "forms": { + "zh": [ + "Token 预算" + ], + "en": [ + "Token Budget", + "token budget", + "Token budget" + ] + }, + "plain": { + "zh": "一轮允许花掉的篇幅上限,快到线就提醒或收尾", + "en": "the cap on how much text a round may spend before it wraps up" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:388", + "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md:878", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:216" + ], + "analogy": { + "zh": "逛市场给了固定现钱,快到数就先停手别加菜", + "en": "you set a cash cap at the market and stop before it's gone" + } + }, + "permission-mode": { + "term": { + "zh": "权限模式", + "en": "permission mode" + }, + "forms": { + "zh": [ + "权限模式", + "授权模式" + ], + "en": [ + "permission mode", + "Permission Mode", + "permissionMode", + "permission modes" + ] + }, + "plain": { + "zh": "Agent 动手前该找谁批准、多久问一次的规矩", + "en": "the rule for who approves each action, and how often the agent has to stop and ask" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/04-权限管线-Agent的护栏.md:7", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:7", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:175", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:175" + ], + "analogy": { + "zh": "小区门禁:业主登记过就随便进,访客每次都得刷卡", + "en": "a gated community: residents wave through, every visitor is stopped and logged at the gate" + } + }, + "permission-pipeline": { + "term": { + "zh": "权限管线", + "en": "permission pipeline" + }, + "forms": { + "zh": [ + "权限管线" + ], + "en": [ + "permission pipeline", + "Permission Pipeline" + ] + }, + "plain": { + "zh": "每次调用工具都要连闯的几道关卡,任一道说停就停", + "en": "several gates every tool call must pass in order; if any gate says stop, it stops right there" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:247", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:247", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:13", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:13" + ], + "analogy": { + "zh": "机场安检:证件、X 光、人身检查,一层不过就登不上机", + "en": "airport security: ID check, X-ray, body scan -- fail one station and you never reach the plane" + } + }, + "hook": { + "term": { + "zh": "钩子", + "en": "hook" + }, + "forms": { + "zh": [ + "钩子系统", + "钩子" + ], + "en": [ + "Hook", + "hook", + "Hooks", + "hooks" + ] + }, + "plain": { + "zh": "在关键节点外挂一段你自己的脚本,时刻一到就自动跑", + "en": "your own script hung on a lifecycle moment so it runs by itself when that moment arrives" + }, + "priority": "core", + "evidence": [ + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:23", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:23", + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:15" + ], + "analogy": { + "zh": "门框上的门铃磁铁:门一开就响,不用重新布线", + "en": "a doorbell magnet on the frame: the door opens, it rings, and the house wiring stays untouched" + } + }, + "system-prompt": { + "term": { + "zh": "系统提示", + "en": "system prompt" + }, + "forms": { + "zh": [ + "系统提示词", + "系统提示" + ], + "en": [ + "system prompt", + "System Prompt", + "system prompts" + ] + }, + "plain": { + "zh": "每轮对话都置顶的岗位说明书:它是谁、能做什么、听谁的", + "en": "the standing instruction sent on top of every request: who it is, what it may do, what it must obey" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:65", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:65", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:14", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:14" + ], + "analogy": { + "zh": "新员工入职手册,每天开工前都要重念一遍", + "en": "the onboarding handbook a new hire rereads before every single shift" + } + }, + "context-window": { + "term": { + "zh": "上下文窗口", + "en": "context window" + }, + "forms": { + "zh": [ + "上下文窗口" + ], + "en": [ + "context window", + "Context Window", + "context windows" + ] + }, + "plain": { + "zh": "模型一轮能装下的内容总量上限,装满了就得把旧的挤出去", + "en": "how much text the model can hold in mind at once; new material pushes the oldest material off the edge" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:46", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:46", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:16", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:16" + ], + "analogy": { + "zh": "一块固定大小的白板,要写新东西必须先擦掉最早几行", + "en": "a whiteboard of fixed size: to write anything new you first erase the oldest lines" + } + }, + "auto-compact": { + "term": { + "zh": "自动压缩", + "en": "auto-compact" + }, + "forms": { + "zh": [ + "自动压缩", + "AutoCompact", + "Autocompact" + ], + "en": [ + "auto-compact", + "auto-compaction", + "AutoCompact", + "Autocompact", + "AutoCompact(" + ] + }, + "plain": { + "zh": "聊天快装满时,把前面的内容自动折成一份摘要", + "en": "when the chat nears full, the harness summarizes the earlier part so the conversation can keep going" + }, + "priority": "core", + "evidence": [ + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:28", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:28", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:198", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:198" + ], + "analogy": { + "zh": "连续剧太长了,先播两分钟前情提要再继续拍", + "en": "a long serialized drama: play two minutes of recap before shooting the next episode" + } + }, + "tool-result": { + "term": { + "zh": "工具结果", + "en": "tool result" + }, + "forms": { + "zh": [ + "工具结果", + "工具执行结果" + ], + "en": [ + "tool result", + "tool results", + "Tool Result", + "Tool results" + ] + }, + "plain": { + "zh": "一次动作的返回内容:文件正文、命令输出或报错,被塞回对话", + "en": "what an action hands back -- file text, command output or an error -- fed into the chat as a new message" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:249", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:249", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:16", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:16" + ], + "analogy": { + "zh": "跑腿买回来的菜和小票,得摆在桌上让人看得见", + "en": "the groceries and the receipt the runner brings back, both laid on the table where you can see them" + } + }, + "sub-agent": { + "term": { + "zh": "子智能体", + "en": "sub-agent" + }, + "forms": { + "zh": [ + "子智能体", + "子 Agent" + ], + "en": [ + "sub-agent", + "Sub-agent", + "sub-agents", + "Sub-Agent" + ] + }, + "plain": { + "zh": "被派出去办一件小事的另一条对话线,办完只带回结论", + "en": "a second agent spawned for one small task with its own message history, reporting back only the answer" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:107", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:107", + "第一部分-基础篇/03-工具系统-Agent的双手.md:234", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:234" + ], + "analogy": { + "zh": "老板派实习生查资料,实习生只把结论带回会议室", + "en": "the boss sends an intern to dig up data; the intern returns with the conclusion, not the whole archive" + } + }, + "coordinator": { + "term": { + "zh": "协调器", + "en": "coordinator" + }, + "forms": { + "zh": [ + "协调器模式", + "协调器", + "协调者" + ], + "en": [ + "Coordinator pattern", + "coordinator", + "Coordinator", + "coordinators" + ] + }, + "plain": { + "zh": "只派活、收结论,自己从不动手写代码的那个角色", + "en": "the one agent that splits the work among workers and merges their answers without touching tools itself" + }, + "priority": "core", + "evidence": [ + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:21", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:21", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:140" + ], + "analogy": { + "zh": "工地上的包工头:分派任务、验收成果,不亲自砌砖", + "en": "a site foreman: assigns tasks and inspects results, but never lays a brick himself" + } + }, + "skill": { + "term": { + "zh": "技能", + "en": "skill" + }, + "forms": { + "zh": [ + "技能", + "技能系统" + ], + "en": [ + "Skill", + "skill", + "skills", + "Skills" + ] + }, + "plain": { + "zh": "一份事先写好的操作说明书,被点到时交给模型照着做", + "en": "a written-down procedure the model can be handed and follow, instead of being re-taught every time" + }, + "priority": "core", + "evidence": [ + "第三部分-高级模式篇/11-技能系统与插件架构.md:7", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:7", + "第一部分-基础篇/01-智能体编程的新范式.md:358", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:358" + ], + "analogy": { + "zh": "厨房墙上的菜谱,客人点这道菜才取下照着做", + "en": "a recipe card on the kitchen wall: pulled down only when that dish is ordered" + } + }, + "mcp-server": { + "term": { + "zh": "MCP 服务器", + "en": "MCP server" + }, + "forms": { + "zh": [ + "MCP 服务器" + ], + "en": [ + "MCP server", + "MCP servers", + "MCP Server" + ] + }, + "plain": { + "zh": "按统一协议接在外面的服务,给 Agent 补上额外的工具和资料", + "en": "an outside program that speaks one shared protocol, handing the agent extra tools and data" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:152", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:152", + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:496", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:496" + ], + "analogy": { + "zh": "家电都插同一种插座,不必为每个牌子配转换头", + "en": "appliances all plugging into the same socket, so no brand needs its own adapter" + } + }, + "claude-md": { + "term": { + "zh": "CLAUDE.md", + "en": "CLAUDE.md" + }, + "forms": { + "zh": [ + "CLAUDE.md" + ], + "en": [ + "CLAUDE.md" + ] + }, + "plain": { + "zh": "项目里那份说明文件,每次开工都被自动塞进背景里", + "en": "the project's note file, dropped into the background on every start so its rules survive a reset" + }, + "priority": "core", + "evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:132", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:132", + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:144", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:144" + ], + "analogy": { + "zh": "贴在操作台上的店规,每个班次开工先念一遍", + "en": "the house rules taped to the counter, read out at the start of every shift" + } + }, + "plan-mode": { + "term": { + "zh": "计划模式", + "en": "plan mode" + }, + "forms": { + "zh": [ + "计划模式", + "Plan 模式", + "规划模式" + ], + "en": [ + "plan mode", + "Plan mode", + "Plan Mode", + "Planning Mode" + ] + }, + "plain": { + "zh": "先只看不改,把方案写清楚给人过目,点头之后才动手", + "en": "a look-but-do-not-touch phase: the agent drafts a proposal first and only edits after you approve it" + }, + "priority": "core", + "evidence": [ + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:13", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:13", + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:5", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:5" + ], + "analogy": { + "zh": "装修先出图纸,签字确认后才允许砸墙", + "en": "an architect draws the blueprint and gets it signed before a single wall is knocked down" + } + }, + "fork-pattern": { + "term": { + "zh": "Fork 模式", + "en": "Fork pattern" + }, + "forms": { + "zh": [ + "Fork 模式", + "完美分叉" + ], + "en": [ + "Fork pattern", + "Fork Pattern", + "fork pattern", + "Fork mode" + ] + }, + "plain": { + "zh": "把此刻的对话原样复制成几份,让几路任务同时开工", + "en": "copying the conversation as it stands into several branches so parallel jobs share one starting point" + }, + "priority": "core", + "evidence": [ + "第三部分-高级模式篇/09-子智能体与Fork模式.md:228", + "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md:228", + "第一部分-基础篇/01-智能体编程的新范式.md:344", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:344" + ], + "analogy": { + "zh": "同一张底片洗出好几张照片,再各自上色", + "en": "printing several photos from one negative, then retouching each print separately" + } + }, + "memory": { + "term": { + "zh": "记忆", + "en": "memory" + }, + "forms": { + "zh": [ + "记忆系统", + "记忆" + ], + "en": [ + "memory", + "Memory" + ] + }, + "plain": { + "zh": "跨对话留在硬盘上的小纸条,下次开机还会被读进来", + "en": "notes kept on disk between conversations and re-read next time, so the agent does not start from zero" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:9", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:9", + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:216", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:216" + ], + "analogy": { + "zh": "抽屉里的老客户档案,换人接待也不会重新自我介绍", + "en": "a drawer of regular-client files: each new clerk opens it instead meeting an old friend as a stranger" + } + }, + "plugin": { + "term": { + "zh": "插件", + "en": "plugin" + }, + "forms": { + "zh": [ + "插件", + "插件系统" + ], + "en": [ + "plugin", + "Plugin", + "plugins", + "Plugins" + ] + }, + "plain": { + "zh": "别人打包好的一包能力,装上就多一批工具和命令", + "en": "someone else's bundled package of tools, commands and settings that you install in one go" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:19", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:19", + "第一部分-基础篇/01-智能体编程的新范式.md:359", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:359" + ], + "analogy": { + "zh": "给相机换个镜头,机身不必重造", + "en": "swapping a lens on the camera rather than rebuilding the body" + } + }, + "session": { + "term": { + "zh": "会话", + "en": "session" + }, + "forms": { + "zh": [ + "会话" + ], + "en": [ + "session", + "Session" + ] + }, + "plain": { + "zh": "从打开到退出的一段连续对话,中途记录会被存下来", + "en": "one continuous run of conversation whose history can be saved and picked up again later" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/04-权限管线-Agent的护栏.md:11", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:11", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:83", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:83" + ], + "analogy": { + "zh": "一通电话:挂了可以重拨,之前说过的还在录音里", + "en": "a phone call: hang up and redial, and the earlier words are still on the recording" + } + }, + "transcript": { + "term": { + "zh": "会话记录", + "en": "transcript" + }, + "forms": { + "zh": [ + "会话记录", + "Transcript" + ], + "en": [ + "transcript", + "Transcript" + ] + }, + "plain": { + "zh": "一整段对话被逐条写成的流水账,恢复和审计都靠它", + "en": "the message-by-message log of a conversation, used to resume it later or to audit what actually happened" + }, + "priority": "extra", + "evidence": [ + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:689", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:690", + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:73", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:255" + ], + "analogy": { + "zh": "法庭速记稿:散庭后照着它复原当天每句话", + "en": "the court stenographer's pages: after the session ends, they are what the day is rebuilt from" + } + }, + "prompt-cache": { + "term": { + "zh": "提示缓存", + "en": "prompt cache" + }, + "forms": { + "zh": [ + "提示缓存", + "Prompt 缓存", + "Prompt Caching" + ], + "en": [ + "prompt cache", + "Prompt cache", + "Prompt Cache", + "prompt caching", + "Prompt Caching" + ] + }, + "plain": { + "zh": "前面那段没变的话服务端已经记着,不必重新算钱重新算时间", + "en": "the service remembers the unchanged front part of your request, so it is neither re-processed nor re-charged" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:296", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:296", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:170" + ], + "analogy": { + "zh": "演员已背熟的台词不必再排,只排新加的那几句", + "en": "lines the cast already knows need no rehearsal; only the new ones take stage time" + } + }, + "permission-classifier": { + "term": { + "zh": "分类器", + "en": "classifier" + }, + "forms": { + "zh": [ + "分类器", + "模式分类器" + ], + "en": [ + "classifier", + "Classifier" + ] + }, + "plain": { + "zh": "另一个模型替你估计这次操作危不危险,决定放行还是来问你", + "en": "a second model that judges how risky an action looks, so it can approve or escalate without waiting for you" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/04-权限管线-Agent的护栏.md:144", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:144" + ], + "analogy": { + "zh": "地铁口的预检员:看一眼就放行,可疑的才送去人工开包", + "en": "the clerk at the station entrance: waves most people through and sends only the odd bag to a hand check" + } + }, + "deferred-tool-discovery": { + "term": { + "zh": "延迟工具发现", + "en": "deferred tool discovery" + }, + "forms": { + "zh": [ + "延迟工具发现", + "延迟发现" + ], + "en": [ + "deferred tool discovery", + "Deferred tool discovery", + "deferred discovery" + ] + }, + "plain": { + "zh": "开场先只报工具名字,真要用了才把完整说明书取来", + "en": "the model first sees only a list of tool names; the full manual is fetched only once it actually needs one" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:146", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:146" + ], + "analogy": { + "zh": "餐厅先递目录页,点了菜厨房才拿出详细菜谱", + "en": "the waiter hands you the list of dish names first; the full recipe sheet comes out only after you order" + } + }, + "mcp-resource": { + "term": { + "zh": "MCP 资源", + "en": "MCP resource" + }, + "forms": { + "zh": [ + "MCP 资源", + "资源列表", + "资源" + ], + "en": [ + "MCP resource", + "MCP resources", + "resource", + "resources", + "Resource" + ] + }, + "plain": { + "zh": "外部服务摆出来给 Agent 翻阅的现成资料,只能读不能执行", + "en": "readable data an outside service exposes -- files, records, docs -- as opposed to actions it can run" + }, + "priority": "extra", + "evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:90", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:90", + "第一部分-基础篇/03-工具系统-Agent的双手.md:127", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:127" + ], + "analogy": { + "zh": "冰箱里的食材:可以拿出来看,不能指望它自己变熟", + "en": "the pantry: you may take things out and look at them, but nothing in there cooks itself" + } + }, + "interrupt": { + "term": { + "zh": "中断", + "en": "interrupt" + }, + "forms": { + "zh": [ + "中断" + ], + "en": [ + "interrupt", + "interrupts", + "Interrupt", + "interruption", + "Interruption", + "Interrupting" + ] + }, + "plain": { + "zh": "你按停那一刻:正在跑的活要不要立刻收手、留下什么", + "en": "what happens the instant you hit stop: whether the running action is killed and what it leaves behind" + }, + "priority": "extra", + "evidence": [ + "00-前言.md:131", + "en/00-Foreword.md:102", + "第一部分-基础篇/01-智能体编程的新范式.md:107", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:107" + ], + "analogy": { + "zh": "你喊停的那一秒,厨师关火,锅里半成品怎么处理得先说清", + "en": "the second you shout stop: the burner goes off, and someone must say what becomes of the half-cooked dish" + } + }, + "destructive": { + "term": { + "zh": "破坏性", + "en": "destructive" + }, + "forms": { + "zh": [ + "破坏性", + "破坏性标记" + ], + "en": [ + "destructive", + "Destructive", + "isDestructive" + ] + }, + "plain": { + "zh": "这一步做完就退不回去,比如删文件、覆盖数据", + "en": "a flag meaning the action cannot be undone afterwards, so it deserves a stricter warning" + }, + "priority": "extra", + "evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:268", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:268", + "第一部分-基础篇/03-工具系统-Agent的双手.md:210", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:210" + ], + "analogy": { + "zh": "碎纸机不是文件柜:纸进去就出不来", + "en": "a shredder, not a filing cabinet: paper that goes in does not come out" + } + }, + "telemetry": { + "term": { + "zh": "遥测", + "en": "telemetry" + }, + "forms": { + "zh": [ + "遥测" + ], + "en": [ + "telemetry", + "Telemetry" + ] + }, + "plain": { + "zh": "程序运行途中悄悄记下发生了什么,事后传回来供分析", + "en": "the program writes down what happened while running and ships the numbers back for later analysis" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:352", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:352", + "第四部分-工程实践篇/13-流式架构与性能优化.md:562", + "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md:565" + ], + "analogy": { + "zh": "航班黑匣子:平日没人看,出事时第一个被打开", + "en": "a flight recorder: nobody reads it day to day, but it is the first thing opened after an incident" + } + }, + "settings-json": { + "term": { + "zh": "settings.json", + "en": "settings.json" + }, + "forms": { + "zh": [ + "settings.json", + "配置文件" + ], + "en": [ + "settings.json" + ] + }, + "plain": { + "zh": "存放选项的纯文本文件,改里面的字段就等于改 Agent 的行为", + "en": "the plain text file where options live: edit a field there and the agent behaves differently next run" + }, + "priority": "extra", + "evidence": [ + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:148", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:148", + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:223", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:223" + ], + "analogy": { + "zh": "门口的总开关面板,拨哪个开关,哪个房间就变", + "en": "the switch panel by the door: flip one breaker and that room changes, no rewiring needed" + } + } +} diff --git a/yuyu-book/illustrations/apxa.svg b/yuyu-book/illustrations/apxa.svg new file mode 100644 index 00000000..51056340 --- /dev/null +++ b/yuyu-book/illustrations/apxa.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/apxb.svg b/yuyu-book/illustrations/apxb.svg new file mode 100644 index 00000000..0101817e --- /dev/null +++ b/yuyu-book/illustrations/apxb.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/apxc.svg b/yuyu-book/illustrations/apxc.svg new file mode 100644 index 00000000..1ea8897f --- /dev/null +++ b/yuyu-book/illustrations/apxc.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/apxd.svg b/yuyu-book/illustrations/apxd.svg new file mode 100644 index 00000000..616d50aa --- /dev/null +++ b/yuyu-book/illustrations/apxd.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch01.svg b/yuyu-book/illustrations/ch01.svg new file mode 100644 index 00000000..e38ec242 --- /dev/null +++ b/yuyu-book/illustrations/ch01.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch02.svg b/yuyu-book/illustrations/ch02.svg new file mode 100644 index 00000000..03a660ac --- /dev/null +++ b/yuyu-book/illustrations/ch02.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch03.svg b/yuyu-book/illustrations/ch03.svg new file mode 100644 index 00000000..bf7c2ca2 --- /dev/null +++ b/yuyu-book/illustrations/ch03.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch04.svg b/yuyu-book/illustrations/ch04.svg new file mode 100644 index 00000000..815b9c39 --- /dev/null +++ b/yuyu-book/illustrations/ch04.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch05.svg b/yuyu-book/illustrations/ch05.svg new file mode 100644 index 00000000..847ea652 --- /dev/null +++ b/yuyu-book/illustrations/ch05.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch06.svg b/yuyu-book/illustrations/ch06.svg new file mode 100644 index 00000000..95885f46 --- /dev/null +++ b/yuyu-book/illustrations/ch06.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch07.svg b/yuyu-book/illustrations/ch07.svg new file mode 100644 index 00000000..6d2ee95e --- /dev/null +++ b/yuyu-book/illustrations/ch07.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch08.svg b/yuyu-book/illustrations/ch08.svg new file mode 100644 index 00000000..11ab48b7 --- /dev/null +++ b/yuyu-book/illustrations/ch08.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch09.svg b/yuyu-book/illustrations/ch09.svg new file mode 100644 index 00000000..eb26c9a6 --- /dev/null +++ b/yuyu-book/illustrations/ch09.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch10.svg b/yuyu-book/illustrations/ch10.svg new file mode 100644 index 00000000..95326670 --- /dev/null +++ b/yuyu-book/illustrations/ch10.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch11.svg b/yuyu-book/illustrations/ch11.svg new file mode 100644 index 00000000..de8ace41 --- /dev/null +++ b/yuyu-book/illustrations/ch11.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch12.svg b/yuyu-book/illustrations/ch12.svg new file mode 100644 index 00000000..b1cafc79 --- /dev/null +++ b/yuyu-book/illustrations/ch12.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch13.svg b/yuyu-book/illustrations/ch13.svg new file mode 100644 index 00000000..9286a18b --- /dev/null +++ b/yuyu-book/illustrations/ch13.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch14.svg b/yuyu-book/illustrations/ch14.svg new file mode 100644 index 00000000..5d1c001d --- /dev/null +++ b/yuyu-book/illustrations/ch14.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/ch15.svg b/yuyu-book/illustrations/ch15.svg new file mode 100644 index 00000000..97841911 --- /dev/null +++ b/yuyu-book/illustrations/ch15.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/cover.svg b/yuyu-book/illustrations/cover.svg new file mode 100644 index 00000000..4ec2145f --- /dev/null +++ b/yuyu-book/illustrations/cover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yuyu-book/illustrations/front.svg b/yuyu-book/illustrations/front.svg new file mode 100644 index 00000000..54315512 --- /dev/null +++ b/yuyu-book/illustrations/front.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/part-p1.svg b/yuyu-book/illustrations/part-p1.svg new file mode 100644 index 00000000..86134ae6 --- /dev/null +++ b/yuyu-book/illustrations/part-p1.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/part-p2.svg b/yuyu-book/illustrations/part-p2.svg new file mode 100644 index 00000000..a8d66f79 --- /dev/null +++ b/yuyu-book/illustrations/part-p2.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/part-p3.svg b/yuyu-book/illustrations/part-p3.svg new file mode 100644 index 00000000..8c43ae4b --- /dev/null +++ b/yuyu-book/illustrations/part-p3.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/illustrations/part-p4.svg b/yuyu-book/illustrations/part-p4.svg new file mode 100644 index 00000000..de206173 --- /dev/null +++ b/yuyu-book/illustrations/part-p4.svg @@ -0,0 +1 @@ + diff --git a/yuyu-book/manifest.json b/yuyu-book/manifest.json new file mode 100644 index 00000000..3199f1f9 --- /dev/null +++ b/yuyu-book/manifest.json @@ -0,0 +1,1019 @@ +{ + "version": "0.3.1", + "date": "2026-10-26", + "author": "LinTsinghua", + "repository": "https://github.com/lintsinghua/claude-code-book", + "license": { + "name": "CC BY-NC-SA 4.0", + "url": "https://creativecommons.org/licenses/by-nc-sa/4.0/" + }, + "title": { + "zh": "御舆:解码 Agent Harness", + "en": "Yù Yú: Decoding Agent Harness" + }, + "subtitle": { + "zh": "Claude Code 架构深度剖析", + "en": "A Deep Architectural Analysis of Claude Code" + }, + "epigraph": { + "zh": "一器而工聚焉者,车为多。——《周礼·考工记》", + "en": "Of all artifacts that bring many crafts together, the chariot calls upon the most. — Kǎo Gōng Jì" + }, + "start_reading": [ + { + "label": { + "zh": "初次阅读", + "en": "First visit" + }, + "body": { + "zh": "从前言开始,再读第 1 章 → 第 2 章 → 第 4 章 → 第 15 章。", + "en": "Begin at the Foreword, then read Chapter 1 → 2 → 4 → 15." + } + }, + { + "label": { + "zh": "动手构建", + "en": "Build an agent" + }, + "body": { + "zh": "先读基础篇与工程实践篇;增加记忆和扩展能力时查阅核心系统篇与高级模式篇。", + "en": "Read Parts 1 and 4 first; consult Parts 2 and 3 when adding memory and extension capabilities." + } + }, + { + "label": { + "zh": "按需查阅", + "en": "Look things up" + }, + "body": { + "zh": "使用四份附录定位模块、工具、功能标志和术语;文末主题索引按字母给出术语的章节位置。", + "en": "Use the four appendices to locate modules, tools, feature flags and terms; the back-matter index maps glossary terms to chapters." + } + }, + { + "label": { + "zh": "离线整书", + "en": "Offline edition" + }, + "body": { + "zh": "本文件即离线整书:单文件 HTML,图、目录、交叉引用、来源锚点断网可用;PDF 版含书签、页码与页眉。", + "en": "This file is the offline edition: a single HTML with diagrams, TOC, cross-references and source anchors working without network; the PDF adds bookmarks, page numbers and running heads." + } + } + ], + "parts": [ + { + "id": "p1", + "no": { + "zh": "第一部分", + "en": "Part 1" + }, + "title": { + "zh": "基础篇", + "en": "Foundations" + }, + "motto": { + "zh": "建立心智模型", + "en": "Building Mental Models" + }, + "goal": { + "zh": "理解 Agent 编程的范式转移,建立对 Agent Harness 的整体认知框架。", + "en": "Understand the paradigm shift in agent programming and establish a holistic cognitive framework for the Agent Harness." + }, + "units": [ + { + "id": "ch01", + "zh": "第一部分-基础篇/01-智能体编程的新范式.md", + "en": "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md" + }, + { + "id": "ch02", + "zh": "第一部分-基础篇/02-对话循环-Agent的心跳.md", + "en": "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md" + }, + { + "id": "ch03", + "zh": "第一部分-基础篇/03-工具系统-Agent的双手.md", + "en": "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md" + }, + { + "id": "ch04", + "zh": "第一部分-基础篇/04-权限管线-Agent的护栏.md", + "en": "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md" + } + ] + }, + { + "id": "p2", + "no": { + "zh": "第二部分", + "en": "Part 2" + }, + "title": { + "zh": "核心系统篇", + "en": "Core Systems" + }, + "motto": { + "zh": "深入子系统", + "en": "Deep into Subsystems" + }, + "goal": { + "zh": "拆解 Agent Harness 的四大核心子系统——配置、记忆、上下文、钩子。", + "en": "Dissect the four core subsystems of the harness — configuration, memory, context, and hooks." + }, + "units": [ + { + "id": "ch05", + "zh": "第二部分-核心系统篇/05-设置与配置-Agent的基因.md", + "en": "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md" + }, + { + "id": "ch06", + "zh": "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md", + "en": "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md" + }, + { + "id": "ch07", + "zh": "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md", + "en": "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md" + }, + { + "id": "ch08", + "zh": "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md", + "en": "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md" + } + ] + }, + { + "id": "p3", + "no": { + "zh": "第三部分", + "en": "Part 3" + }, + "title": { + "zh": "高级模式篇", + "en": "Advanced Patterns" + }, + "motto": { + "zh": "Agent 的组合与扩展", + "en": "Composition and Extension" + }, + "goal": { + "zh": "探索 Agent 如何组合、编排和扩展——从子智能体到 MCP 协议桥接。", + "en": "Explore how agents compose, orchestrate, and extend — from sub-agents to MCP protocol bridging." + }, + "units": [ + { + "id": "ch09", + "zh": "第三部分-高级模式篇/09-子智能体与Fork模式.md", + "en": "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md" + }, + { + "id": "ch10", + "zh": "第三部分-高级模式篇/10-协调器模式-多智能体编排.md", + "en": "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md" + }, + { + "id": "ch11", + "zh": "第三部分-高级模式篇/11-技能系统与插件架构.md", + "en": "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md" + }, + { + "id": "ch12", + "zh": "第三部分-高级模式篇/12-MCP集成与外部协议.md", + "en": "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md" + } + ] + }, + { + "id": "p4", + "no": { + "zh": "第四部分", + "en": "Part 4" + }, + "title": { + "zh": "工程实践篇", + "en": "Engineering Practice" + }, + "motto": { + "zh": "从原理到构建", + "en": "From Principles to Construction" + }, + "goal": { + "zh": "性能优化的工程细节,以及从零构建一个完整 Harness 的实战路线图。", + "en": "Engineering details of performance, and a hands-on roadmap to building a complete Harness from scratch." + }, + "units": [ + { + "id": "ch13", + "zh": "第四部分-工程实践篇/13-流式架构与性能优化.md", + "en": "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md" + }, + { + "id": "ch14", + "zh": "第四部分-工程实践篇/14-Plan模式与结构化工作流.md", + "en": "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md" + }, + { + "id": "ch15", + "zh": "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md", + "en": "en/Part-4-Engineering-Practice/15-Building-Your-Own-Agent-Harness.md" + } + ] + } + ], + "front": { + "id": "front", + "zh": "00-前言.md", + "en": "en/00-Foreword.md" + }, + "appendices": [ + { + "id": "apxa", + "zh": "附录/A-源码导航地图.md", + "en": "en/Appendices/A-Architecture-Navigation-Map.md" + }, + { + "id": "apxb", + "zh": "附录/B-工具完整清单.md", + "en": "en/Appendices/B-Complete-Tool-Inventory.md" + }, + { + "id": "apxc", + "zh": "附录/C-功能标志速查表.md", + "en": "en/Appendices/C-Feature-Flag-Reference.md" + }, + { + "id": "apxd", + "zh": "附录/D-术语表.md", + "en": "en/Appendices/D-Glossary.md" + } + ], + "guides": { + "front": { + "question": { + "zh": "为什么调用 LLM 的聊天机器人远远不够,Agent 需要 Harness 这一层工程基础设施?", + "en": "Why does an agent need a runtime layer—an Agent Harness—rather than a thin wrapper around the LLM API?" + }, + "deliverable": { + "zh": "一张全书四部分知识地图,加一条匹配自身角色(架构师/工程师/开发者/研究者)的阅读路径。", + "en": "A map of the book's four parts and a reading path matched to your role—architect, senior engineer, app developer, or researcher." + }, + "boundary": { + "zh": "不展开任何子系统的实现细节,也不提供 API 教程或技巧清单;架构拆解从第 1 章开始。", + "en": "No subsystem implementation details, no API how-to or tips list; the architecture walkthrough starts in Chapter 1." + }, + "sources": [ + "cc-changelog", + "docs-how-it-works", + "docs-best-practices", + "gh-book-repo", + "ld-topic-1870593" + ], + "plain": { + "zh": "模型会动手后,缺的是一层管住工具、权限和状态的外壳;这本书按公开行为推演它", + "en": "acting models need a shell for tools, permissions and state; the book deduces it from public behavior" + }, + "analogy": { + "zh": "像医院接诊:再能诊断,也得有分诊台、处方签章和病历本才敢发药", + "en": "a hospital: sharp diagnoses still need triage, a signed script and a chart before anyone gets medicine" + }, + "plain_evidence": [ + "00-前言.md:93", + "00-前言.md:95", + "00-前言.md:143", + "00-前言.md:197", + "en/00-Foreword.md:64", + "en/00-Foreword.md:66", + "en/00-Foreword.md:114", + "en/00-Foreword.md:168" + ] + }, + "ch01": { + "question": { + "zh": "从 Chatbot 到自主智能体的范式转移中,Claude Code 的架构骨架与五大设计原则是什么?", + "en": "What shifts AI programming from chatbot to autonomous agent, and what architectural skeleton and design principles does Claude Code embody?" + }, + "deliverable": { + "zh": "一张五模块分层架构图,并能独立完成安装与 /doctor 首次诊断。", + "en": "A layered diagram of the five core modules and a working /doctor diagnostic run you can perform unaided." + }, + "boundary": { + "zh": "只做全景导览;对话循环、工具系统、权限管线的内部实现分别见第 2、3、4 章。", + "en": "A panoramic tour only; loop internals, tool contracts, and permission stages are detailed in Chapters 2, 3, and 4." + }, + "sources": [ + "docs-how-it-works", + "blog-building-effective-agents", + "docs-best-practices", + "cc-release-v2-1-310" + ], + "plain": { + "zh": "模型动手后的门槛在工程侧:权限、状态、恢复谁来管?答案是一层运行时加五条原则", + "en": "the hard part moves to engineering: permissions, state, recovery - one runtime and five principles handle it" + }, + "analogy": { + "zh": "像装修:电器再好也得先布线、装漏电保护、列回路清单,不能把插头直接拧进插座", + "en": "a renovation: before any appliance you run wiring, fit a leakage breaker and label every circuit" + }, + "plain_evidence": [ + "第一部分-基础篇/01-智能体编程的新范式.md:52", + "第一部分-基础篇/01-智能体编程的新范式.md:59", + "第一部分-基础篇/01-智能体编程的新范式.md:115", + "第一部分-基础篇/01-智能体编程的新范式.md:286", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:52", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:59", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:115", + "en/Part-1-Foundations/01-The-New-Paradigm-of-Agent-Programming.md:286" + ] + }, + "ch02": { + "question": { + "zh": "对话主循环为何选异步生成器?一次 Turn 如何从请求流转到终止?", + "en": "Why is the dialog loop an async generator, and how does one turn flow from request to termination?" + }, + "deliverable": { + "zh": "一张 Turn 生命周期与状态转换图,能独立实现最小 AsyncGenerator 循环。", + "en": "A turn lifecycle and state-transition diagram, plus a minimal async-generator loop you can implement yourself." + }, + "boundary": { + "zh": "只讲循环本身;四级压缩管线细节见第 7 章,工具编排见第 3 章,权限见第 4 章。", + "en": "Covers the loop only: compression pipeline internals belong to Chapter 7, tool orchestration to Chapter 3, permissions to Chapter 4." + }, + "sources": [ + "docs-how-it-works", + "docs-interactive-mode", + "docs-glossary", + "cc-changelog" + ], + "plain": { + "zh": "一轮问模型、跑工具、回填的循环:能暂停能取消,续跑七条路,退出十种因", + "en": "ask model, run tools, backfill: a pausable cancellable loop with seven continue paths and ten exit reasons" + }, + "analogy": { + "zh": "像收发室:登记、寄出、等回执,没回来就再问一次;叫停时必写明停在哪一步", + "en": "a mailroom: log, send, await the receipt, ask again if none comes; every halt names where it stopped" + }, + "plain_evidence": [ + "第一部分-基础篇/02-对话循环-Agent的心跳.md:18", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:141", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:251", + "第一部分-基础篇/02-对话循环-Agent的心跳.md:390", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:18", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:141", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:251", + "en/Part-1-Foundations/02-The-Dialog-Loop-Heartbeat-of-an-Agent.md:390" + ] + }, + "ch03": { + "question": { + "zh": "45 个以上工具如何靠同一份类型契约注册、发现、并发调度,而不改动引擎代码?", + "en": "How do 45-plus tools share one type contract for registration, discovery, and concurrent scheduling without touching the engine?" + }, + "deliverable": { + "zh": "一张并发分区批次图,能用 buildTool 独立实现一个自定义工具。", + "en": "A batch-partition diagram for concurrent scheduling, and one custom tool built independently with buildTool." + }, + "boundary": { + "zh": "覆盖工具契约与编排引擎;管线内的权限判定见第 4 章,MCP 外部工具见第 12 章。", + "en": "Stops at the tool contract and orchestrator; permission decisions inside the pipeline are Chapter 4, MCP-sourced tools are Chapter 12." + }, + "sources": [ + "docs-tools-reference", + "blog-writing-tools-for-agents", + "docs-permissions", + "cc-changelog" + ], + "plain": { + "zh": "几十个工具共用一份五要素契约:只读的成批跑,改东西的单独排队,边听指令边开工", + "en": "tools share a five-part contract: reads batch together, writes queue alone, work starts mid-reply" + }, + "analogy": { + "zh": "像后厨出单:洗切可以一起上,动火的只能一个人来,做好也得按下单顺序报号", + "en": "a kitchen: chopping runs in parallel, only one cook may touch the flame, dishes get called in order" + }, + "plain_evidence": [ + "第一部分-基础篇/03-工具系统-Agent的双手.md:34", + "第一部分-基础篇/03-工具系统-Agent的双手.md:253", + "第一部分-基础篇/03-工具系统-Agent的双手.md:317", + "第一部分-基础篇/03-工具系统-Agent的双手.md:344", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:34", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:253", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:317", + "en/Part-1-Foundations/03-The-Tool-System-Agent-Hands.md:344" + ] + }, + "ch04": { + "question": { + "zh": "一次工具调用如何穿过四阶段权限管线,在自主效率与安全控制之间取得平衡?", + "en": "How does a tool call traverse the four-stage pipeline, and how is autonomy balanced against safety?" + }, + "deliverable": { + "zh": "一张四阶段决策流程图,能独立编写项目级 allow/deny 权限配置。", + "en": "A four-stage decision-flow diagram and a project-level allow/deny ruleset you can author from scratch." + }, + "boundary": { + "zh": "只覆盖权限判定;生命周期钩子扩展点见第 8 章,配置分层与合并见第 5 章。", + "en": "Permission evaluation only; lifecycle hooks are Chapter 8, and settings layering and merge rules are Chapter 5." + }, + "sources": [ + "docs-permissions", + "docs-permission-modes", + "docs-env-vars", + "cc-issues", + "cc-changelog" + ], + "plain": { + "zh": "工具请求要过四道关:验格式、查规则、工具自审、人点头;显式拒绝压过一切放行", + "en": "every call clears four gates: schema, rules, tool self-check, human nod - an explicit deny outranks any allow" + }, + "analogy": { + "zh": "像报销审批:验票、查黑名单、主管复核、老板签字;写明不予报销的,谁签都没用", + "en": "expense approval: receipt check, blocklist lookup, manager review, boss signs - a written denial ends it all" + }, + "plain_evidence": [ + "第一部分-基础篇/04-权限管线-Agent的护栏.md:19", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:121", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:138", + "第一部分-基础篇/04-权限管线-Agent的护栏.md:209", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:19", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:121", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:138", + "en/Part-1-Foundations/04-The-Permission-Pipeline-Agent-Guardrails.md:209" + ] + }, + "ch05": { + "question": { + "zh": "六层配置源按什么优先级合并成 Agent 的“基因”,安全边界又划在哪里?", + "en": "How do six configuration sources merge into the agent's operating defaults, and where are the trust boundaries?" + }, + "deliverable": { + "zh": "一张六层优先级与合并规则对照表,能独立设计团队分层配置策略。", + "en": "A priority-and-merge table for the six layers, plus a tiered settings strategy you can design for a team." + }, + "boundary": { + "zh": "聚焦配置加载、功能开关与状态存储;权限规则语义见第 4 章,钩子配置项见第 8 章。", + "en": "Covers loading, merging, flags, and state storage; permission rule semantics stay in Chapter 4, hook options in Chapter 8." + }, + "sources": [ + "docs-settings", + "docs-env-vars", + "npm-claude-code", + "cc-changelog" + ], + "plain": { + "zh": "六层配置层层覆盖决定它怎么行动;外来仓库改不动安全项,运行时状态只换不改", + "en": "six config layers override in order; a foreign repo can't set security keys, and state is swapped not edited" + }, + "analogy": { + "zh": "像办公室公告栏:新通知压在旧通知上面,但权限清单只增不减,消防通道归物业管", + "en": "an office notice board: new memos cover old ones, the access list only grows, fire exits are the landlord's" + }, + "plain_evidence": [ + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:23", + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:76", + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:223", + "第二部分-核心系统篇/05-设置与配置-Agent的基因.md:413", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:23", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:76", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:223", + "en/Part-2-Core-Systems/05-Settings-and-Configuration-Agent-DNA.md:413" + ] + }, + "ch06": { + "question": { + "zh": "哪些信息值得跨会话长期保存?后台提取又如何在提示缓存约束下进行?", + "en": "What deserves durable storage across sessions, and how does background extraction run within prompt-cache limits?" + }, + "deliverable": { + "zh": "一份四类型分类判据速查表,能独立写出合规的 frontmatter 记忆文件。", + "en": "A four-type decision checklist and correctly formatted memory files you can write unaided." + }, + "boundary": { + "zh": "只管跨会话的长期记忆;会话内的工作记忆与压缩见第 7 章,配置信任边界见第 5 章。", + "en": "Long-term memory only; in-session working memory and compaction are Chapter 7, settings trust boundaries are Chapter 5." + }, + "sources": [ + "docs-memory", + "blog-context-engineering", + "docs-glossary", + "cc-changelog" + ], + "plain": { + "zh": "跨会话只记代码里读不出的四件事:你是谁、你纠正过什么、为何这么定、链接在哪", + "en": "it keeps only what code can't reveal: who you are, what you corrected, why it was decided, where links live" + }, + "analogy": { + "zh": "像图书馆目录柜:只登记书在哪层架,正文不抄进卡片;真要引用还得走回架子核对", + "en": "a card catalog: it notes which shelf, never copies the text, and you still walk over to check the book" + }, + "plain_evidence": [ + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:7", + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:17", + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:232", + "第二部分-核心系统篇/06-记忆系统-Agent的长期记忆.md:540", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:7", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:17", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:232", + "en/Part-2-Core-Systems/06-The-Memory-System-Agent-Long-Term-Memory.md:540" + ] + }, + "ch07": { + "question": { + "zh": "上下文逼近窗口上限时,四级压缩按何顺序介入,各自付出多少成本?", + "en": "As the window fills, in what order do the four compression levels intervene, and what does each cost?" + }, + "deliverable": { + "zh": "一张令牌阈值与四级压缩成本图,能自行算出有效窗口并选定压缩策略。", + "en": "A token-threshold and compression-cost map, plus the arithmetic to size an effective window yourself." + }, + "boundary": { + "zh": "聚焦单会话内的窗口与预算;持久记忆见第 6 章,PreCompact 钩子见第 8 章,Fork 子代理见第 9 章。", + "en": "Working memory within one session; durable memory is Chapter 6, PreCompact hooks Chapter 8, forked sub-agents Chapter 9." + }, + "sources": [ + "docs-context-window", + "blog-context-engineering", + "docs-memory", + "docs-prompt-caching", + "cc-changelog" + ], + "plain": { + "zh": "白板写满就得擦:腾地方从轻到重分四级,压缩前先留好下笔的余量,连败三次就收手", + "en": "when the board fills, erase in four graded steps, save room before summarizing, quit after three failures" + }, + "analogy": { + "zh": "像库房腾货架:先并零箱,再压缩打包,实在装不下才把旧货誊成一页台账贴门口", + "en": "clearing a crowded shelf: consolidate loose boxes, vacuum-pack, and only then write old stock onto one ledger" + }, + "plain_evidence": [ + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:30", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:77", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:120", + "第二部分-核心系统篇/07-上下文管理-Agent的工作记忆.md:282", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:30", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:77", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:120", + "en/Part-2-Core-Systems/07-Context-Management-Agent-Working-Memory.md:282" + ] + }, + "ch08": { + "question": { + "zh": "26 个生命周期事件上,五种钩子类型如何观察、拦截并改写 Agent 的行为?", + "en": "Across 26 lifecycle events, how do five hook types observe, intercept, and reshape agent behavior?" + }, + "deliverable": { + "zh": "一份事件×退出码×响应字段速查表,能独立写出可上线的 PreToolUse 钩子。", + "en": "An event/exit-code/response-field cheat sheet and a working PreToolUse hook you can ship." + }, + "boundary": { + "zh": "讲扩展点本身;权限管线各阶段顺序见第 4 章,钩子配置的六层合并规则见第 5 章。", + "en": "The extension points themselves; permission-stage ordering stays in Chapter 4, hook config merge rules in Chapter 5." + }, + "sources": [ + "docs-hooks", + "docs-permissions", + "cc-changelog", + "cc-issues" + ], + "plain": { + "zh": "不改源码就能插手 Agent 的关键节点:五种钩子挂在 26 个生命周期事件上,能拦、能改写输入、能补上下文", + "en": "five hook kinds hang on 26 lifecycle events: watch, block, rewrite tool input, add context - no source edits" + }, + "analogy": { + "zh": "像小区门禁:保安不盖楼,但每次进出都能拦下、改道或在登记本上补一句", + "en": "an estate gatehouse: it builds nothing, yet stops, redirects or notes every comings and goings" + }, + "plain_evidence": [ + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:15", + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:53", + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:211", + "第二部分-核心系统篇/08-钩子系统-Agent的生命周期扩展点.md:655", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:15", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:53", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:211", + "en/Part-2-Core-Systems/08-The-Hook-System-Agent-Lifecycle-Extension-Points.md:655" + ] + }, + "ch09": { + "question": { + "zh": "一次对话装不下的大任务,Claude Code 怎样拆给子智能体,又不让 token 与缓存失控?", + "en": "When one conversation cannot hold the task, how does Claude Code split work into subagents without letting tokens or the prompt cache run away?" + }, + "deliverable": { + "zh": "一张 AgentTool 同步/异步/Fork 三条路径的分流图,加上一份带工具白名单与 maxTurns 的自定义智能体定义。", + "en": "A map of AgentTool's sync, async and Fork paths, plus a custom agent definition with a tool allowlist, maxTurns and cache-prefix token math." + }, + "boundary": { + "zh": "只讲委派与并行机制;多智能体显式编排和失败再分配见第10章,技能与插件注入见第11章。", + "en": "Delegation mechanics only; explicit multi-agent orchestration and reassignment are Chapter 10, skill and plugin injection is Chapter 11." + }, + "sources": [ + "docs-sub-agents", + "blog-multi-agent-research", + "docs-agent-sdk-overview", + "gh-agent-sdk-python", + "cc-changelog" + ], + "plain": { + "zh": "把活交给专职子智能体:内置与自定义智能体怎么定义,Fork 靠字节级继承父上下文并行且不浪费 token", + "en": "hand work to specialist sub-agents; Fork runs them in parallel by reusing the parent's exact cached prefix" + }, + "analogy": { + "zh": "像复印同一份会议底稿:每人拿一份完全一样的原件各写各结论,不必重新打印", + "en": "one meeting brief photocopied for the table: each person annotates a copy, nobody reprints the original" + }, + "plain_evidence": [ + "第三部分-高级模式篇/09-子智能体与Fork模式.md:9", + "第三部分-高级模式篇/09-子智能体与Fork模式.md:226", + "第三部分-高级模式篇/09-子智能体与Fork模式.md:253", + "第三部分-高级模式篇/09-子智能体与Fork模式.md:343", + "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md:9", + "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md:226", + "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md:253", + "en/Part-3-Advanced-Patterns/09-Sub-Agents-and-the-Fork-Pattern.md:343" + ] + }, + "ch10": { + "question": { + "zh": "当子任务彼此依赖、发现需要共享、工人还会失败时,谁来当那个项目经理?", + "en": "Once subtasks depend on each other, discoveries must be shared, and workers fail mid-run, who plays the project manager?" + }, + "deliverable": { + "zh": "一套 Coordinator 模式启用与工作者工具池的选型流程,含四阶段工作流和 Scratchpad 共享目录的落地方案。", + "en": "A decision flow for enabling Coordinator mode and sizing worker tool pools, with the four-phase workflow and a Scratchpad sharing plan." + }, + "boundary": { + "zh": "不复推 Fork 的字节级缓存前缀(第9章),也不涉及技能与 MCP 扩展层(第11、12章)。", + "en": "Does not re-derive Fork's byte-level cache prefix (Chapter 9), nor the skill and MCP extension layers (Chapters 11 and 12)." + }, + "sources": [ + "docs-agent-teams", + "blog-multi-agent-research", + "docs-sub-agents", + "gh-agent-sdk-typescript", + "cc-changelog" + ], + "plain": { + "zh": "一个脑子忙不过来时:协调者只派活不干活,发消息、共用草稿区,还兜住worker失败和半成品", + "en": "when one agent isn't enough: a lead assigns but never executes, absorbing failures and half-finished work" + }, + "analogy": { + "zh": "像酒席总厨:自己从不颠勺,各灶领单,一张备料单大家看,谁倒下了马上补位", + "en": "a banquet head chef: no pan himself, tickets out to stations, one prep list, covers whoever drops out" + }, + "plain_evidence": [ + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:9", + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:325", + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:543", + "第三部分-高级模式篇/10-协调器模式-多智能体编排.md:587", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:9", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:325", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:544", + "en/Part-3-Advanced-Patterns/10-The-Coordinator-Pattern-Multi-Agent-Orchestration.md:588" + ] + }, + "ch11": { + "question": { + "zh": "怎样把一段反复粘贴的提示词,变成可分发、可覆盖、可控权限的能力包?", + "en": "How does a prompt you keep retyping become a capability package that can be distributed, overridden, and permission-scoped?" + }, + "deliverable": { + "zh": "一个带 frontmatter、参数替换与路径触发的 SKILL.md,加一张 managed 到 bundled 的五层覆盖图。", + "en": "A SKILL.md with frontmatter, argument substitution and path triggers, plus a layering map from managed through user, project, plugin to bundled." + }, + "boundary": { + "zh": "只到技能与插件的加载执行;MCP 传输与权限归第12章,工作流编排与定时调度归第14章。", + "en": "Stops at skill and plugin loading and execution; MCP transports belong to Chapter 12, workflow orchestration and scheduling to Chapter 14." + }, + "sources": [ + "docs-skills", + "docs-plugins", + "docs-env-vars", + "cc-changelog" + ], + "plain": { + "zh": "把反复要交代的话写成 Markdown 技能:五个来源分层覆盖、按路径动态发现,插件再整包带进来", + "en": "turn repeated instructions into Markdown skills: tiered sources override each other, plugins ship whole packs" + }, + "analogy": { + "zh": "像墙上的菜谱卡:总部、本店、自家各一叠,同名听级别高的,要用时念名字抽一张", + "en": "a wall of recipe cards: HQ, house and home stacks; same name defers upward; call it out and pull one" + }, + "plain_evidence": [ + "第三部分-高级模式篇/11-技能系统与插件架构.md:17", + "第三部分-高级模式篇/11-技能系统与插件架构.md:302", + "第三部分-高级模式篇/11-技能系统与插件架构.md:413", + "第三部分-高级模式篇/11-技能系统与插件架构.md:512", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:17", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:302", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:413", + "en/Part-3-Advanced-Patterns/11-The-Skill-System-and-Plugin-Architecture.md:513" + ] + }, + "ch12": { + "question": { + "zh": "当工具、资源或 IDE 都在 Claude Code 之外,怎样安全接进来并管住权限?", + "en": "When the tools, resources, or the IDE itself live outside Claude Code, how are they wired in safely and kept under permission control?" + }, + "deliverable": { + "zh": "一份可用的 .mcp.json,一张八类连接的选型对照表,以及 mcp__server__tool 命名贯穿四层门控的排查路径。", + "en": "A working .mcp.json, a selection table across the eight connection types, and a trace of mcp__server__tool naming through the four permission gates." + }, + "boundary": { + "zh": "工具接口本体见第3章,权限管线内部机制见第4章;本章只讲 MCP 如何接入这两套系统。", + "en": "The tool interface itself (Chapter 3) and the permission pipeline internals (Chapter 4) are assumed, not re-explained; only MCP's integration is." + }, + "sources": [ + "docs-mcp", + "mcp-spec-2026-07-28", + "docs-plugins", + "blog-writing-tools-for-agents", + "cc-changelog" + ], + "plain": { + "zh": "接外部工具不再一家一个适配:连接配置怎么分八类、工具改名映射、逐层审批,外加 IDE 双向桥", + "en": "one protocol replaces per-tool adapters: connection configs, tool naming, layered approvals, an IDE bridge" + }, + "analogy": { + "zh": "像小区快递驿站:任何商家只送一次就能被取,货架按楼栋分格贴名,取件先验码", + "en": "a shared parcel room: any courier drops once, shelves sorted and labelled by building, code scanned first" + }, + "plain_evidence": [ + "第三部分-高级模式篇/12-MCP集成与外部协议.md:64", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:100", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:283", + "第三部分-高级模式篇/12-MCP集成与外部协议.md:672", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:64", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:100", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:283", + "en/Part-3-Advanced-Patterns/12-MCP-Integration-and-External-Protocols.md:672" + ] + }, + "ch13": { + "question": { + "zh": "模型还在吐字,工具为何已能开跑?延迟、并发与 token 成本之间怎么权衡?", + "en": "Why can tools start running while the model is still talking, and how are latency, concurrency, and token cost traded off against each other?" + }, + "deliverable": { + "zh": "一份性能排查清单:流式管线、并/串行规则、启动并行预取、用量累加与缓存命中率的计算方法。", + "en": "A performance checklist covering the QueryEngine stream, the executor's parallel-versus-serial rules, startup prefetch, usage accounting, and cache-hit math." + }, + "boundary": { + "zh": "不重讲对话循环(第2章)与压缩策略(第7章);Fork 的字节级继承细节以第9章为主。", + "en": "The main loop is Chapter 2 and compaction is Chapter 7; Fork's byte-exact inheritance is detailed in Chapter 9, summarized here." + }, + "sources": [ + "docs-prompt-caching", + "docs-agent-sdk-overview", + "docs-context-window", + "cc-release-v2-1-310", + "npm-claude-code" + ], + "plain": { + "zh": "别干等:token 还在流就开工,工具该并行的并行、启动活儿同时跑,成本和缓存一路盯(数字多为示意)", + "en": "act while tokens arrive: overlap tool runs and startup, watch cost and cache breaks (ms figures illustrative)" + }, + "analogy": { + "zh": "像跑堂接菜:厨房喊一道先端一道,上桌仍按点单顺序;一道洒了,同批的先停", + "en": "a waiter carrying each dish as it's called, serving still in order, halting the whole batch if one spills" + }, + "plain_evidence": [ + "第四部分-工程实践篇/13-流式架构与性能优化.md:132", + "第四部分-工程实践篇/13-流式架构与性能优化.md:280", + "第四部分-工程实践篇/13-流式架构与性能优化.md:294", + "第四部分-工程实践篇/13-流式架构与性能优化.md:479", + "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md:132", + "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md:280", + "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md:294", + "en/Part-4-Engineering-Practice/13-Streaming-Architecture-and-Performance-Optimization.md:482" + ] + }, + "ch14": { + "question": { + "zh": "如何让 Agent 先想清楚再动手,并把「想清楚」变成可读回、可审批、可定时的资产?", + "en": "How do you make an agent think before acting, and turn that thinking into an artifact that can be re-read, approved, and scheduled?" + }, + "deliverable": { + "zh": "一条 EnterPlanMode 到计划文件再到 ExitPlanMode 审批的完整链路,加 Cron 与远程触发的选型建议。", + "en": "The full EnterPlanMode-to-plan-file-to-ExitPlanMode approval chain, plus a selection guide for Cron, RemoteTrigger, workflow scripts, and SleepTool." + }, + "boundary": { + "zh": "权限模式的实现底座在第4章,验证智能体的对抗提示在第9章,本章不展开。", + "en": "Permission-mode plumbing lives in Chapter 4 and the verification agent's adversarial prompting in Chapter 9; neither is developed here." + }, + "sources": [ + "docs-permission-modes", + "docs-interactive-mode", + "docs-tools-reference", + "cc-changelog" + ], + "plain": { + "zh": "先只读想清楚、批准了再动手改:计划能校验能续存,还能接上定时触发和后台长跑", + "en": "think read-only before writing: an approvable, recoverable plan, plus cron triggers and background runs" + }, + "analogy": { + "zh": "像搬家:先列清单估价,老板签字才叫车;清单收进抽屉,下次接着用", + "en": "moving day: list and price first, the boss signs before a truck is called, the list is filed for reuse" + }, + "plain_evidence": [ + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:13", + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:17", + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:422", + "第四部分-工程实践篇/14-Plan模式与结构化工作流.md:661", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:13", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:17", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:423", + "en/Part-4-Engineering-Practice/14-Plan-Mode-and-Structured-Workflows.md:662" + ] + }, + "ch15": { + "question": { + "zh": "抛开 Claude Code 从零起步,一个最小可用的 Agent Harness 需要哪几根承重梁,按什么顺序搭?", + "en": "Strip Claude Code away: which load-bearing parts does a minimal working agent harness need, and in what order would you build them?" + }, + "deliverable": { + "zh": "六步路线图与可运行的最小 Harness:对话循环、buildTool、四阶段权限、压缩、记忆、钩子,附生产指标清单。", + "en": "A six-step roadmap to a runnable minimal harness — loop, buildTool factory, four-stage permission pipeline, compaction, memory, hooks — plus production metrics." + }, + "boundary": { + "zh": "只提炼可迁移的取舍,各系统源码细节仍见第2—12章;不覆盖特定平台的部署运维。", + "en": "Recaps transferable principles only; per-system source detail stays in Chapters 2-12, and platform-specific deployment operations are out of scope." + }, + "sources": [ + "docs-headless", + "docs-cli-reference", + "docs-agent-sdk-overview", + "gh-agent-sdk-python", + "gh-agent-sdk-typescript" + ], + "plain": { + "zh": "全书收口成一张施工图:按六步搭出最小可跑 Harness(代码是教学示例),再谈架构教训与生产化", + "en": "capstone: six steps to a minimal harness; the code is a teaching example, then production concerns" + }, + "analogy": { + "zh": "像照邻居厨房自己搭灶:六样大致按顺序来,中间反复回去改,最后能自己开火", + "en": "copying a neighbour's kitchen to build your own: six parts in rough order, fixing earlier ones as you learn" + }, + "plain_evidence": [ + "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md:62", + "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md:167", + "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md:186", + "第四部分-工程实践篇/15-构建你自己的Agent-Harness.md:503", + "en/Part-4-Engineering-Practice/15-Building-Your-Own-Agent-Harness.md:62", + "en/Part-4-Engineering-Practice/15-Building-Your-Own-Agent-Harness.md:167", + "en/Part-4-Engineering-Practice/15-Building-Your-Own-Agent-Harness.md:186", + "en/Part-4-Engineering-Practice/15-Building-Your-Own-Agent-Harness.md:511" + ] + }, + "apxa": { + "question": { + "zh": "要改一个不熟的功能:它归哪个模块、上下游依赖谁、数据实际怎么流过?", + "en": "You must change an unfamiliar feature: which module owns it, what does it depend on, and how does the data actually flow through?" + }, + "deliverable": { + "zh": "一张模块索引表(职责与关键数据结构)、依赖关系图、数据流路径速查与接口契约概述,每条回指正文章节。", + "en": "A module index with responsibilities and key data structures, a dependency graph, dataflow path diagrams, contract summaries, and chapter pointers." + }, + "boundary": { + "zh": "只做导航与回指,机制推导以正文各章为准;模块划分与命名随版本演进,不保证恒定。", + "en": "A conceptual map, not a mechanism derivation — the chapters carry the reasoning; module names and boundaries may shift between releases." + }, + "sources": [ + "cc-changelog", + "cc-issues", + "gh-book-repo", + "docs-settings", + "ld-topic-1871393" + ], + "plain": { + "zh": "用法:这是概念性地图不是文件清单——A.1 查模块归谁、看哪章,A.3 顺着一条数据流走一遍", + "en": "how to use: a conceptual map, not a file list - A.1 for who owns what and which chapter, A.3 to trace a flow" + }, + "analogy": { + "zh": "像物业的水电管线总图:敲墙前先查这根管子归谁、水从哪进往哪出", + "en": "the building's utility plan: before hammering, check whose pipe it is and where water enters and exits" + }, + "plain_evidence": [ + "附录/A-源码导航地图.md:3", + "附录/A-源码导航地图.md:6", + "附录/A-源码导航地图.md:14", + "附录/A-源码导航地图.md:221", + "en/Appendices/A-Architecture-Navigation-Map.md:3", + "en/Appendices/A-Architecture-Navigation-Map.md:6", + "en/Appendices/A-Architecture-Navigation-Map.md:14", + "en/Appendices/A-Architecture-Navigation-Map.md:221" + ] + }, + "apxb": { + "question": { + "zh": "想确认某工具是否存在,或它是只读、破坏性、可并行,还是被标志关在门外?", + "en": "Checking whether a tool exists at all, or whether it is read-only, destructive, concurrency-safe, or locked behind a feature flag?" + }, + "deliverable": { + "zh": "十二类工具的属性对照(readOnly/destructive/concurrencySafe)、启用条件、注册默认值与延迟/消耗画像。", + "en": "Answers on every registered tool across twelve categories: three permission flags, enablement conditions, buildTool defaults, and rough latency and token profiles." + }, + "boundary": { + "zh": "只登记属性与分类,不解释工具实现(第3章)与权限判定流程(第4章);清单随版本增删。", + "en": "Registers attributes only; tool implementation is Chapter 3 and permission decisions are Chapter 4. The roster grows and shrinks with each release." + }, + "sources": [ + "docs-tools-reference", + "docs-permissions", + "cc-changelog" + ], + "plain": { + "zh": "用法:拿不准某个工具时按分类查——能不能并行、算不算破坏性、要开哪个标志、和谁搭配用", + "en": "how to use: look a tool up by category - destructive, parallel-safe, which flag gates it, what it pairs with" + }, + "analogy": { + "zh": "像五金店零件柜:每格标签写着能不能随手拿、刃口利不利、要不要店员开钥匙", + "en": "a hardware-shop parts cabinet: each bin's tag says grab-and-go, blade sharp, or needs the clerk's key" + }, + "plain_evidence": [ + "附录/B-工具完整清单.md:3", + "附录/B-工具完整清单.md:5", + "附录/B-工具完整清单.md:255", + "附录/B-工具完整清单.md:378", + "en/Appendices/B-Complete-Tool-Inventory.md:3", + "en/Appendices/B-Complete-Tool-Inventory.md:5", + "en/Appendices/B-Complete-Tool-Inventory.md:255", + "en/Appendices/B-Complete-Tool-Inventory.md:378" + ] + }, + "apxc": { + "question": { + "zh": "某个能力在你的构建里不见了——是编译时没打进去,还是运行时门控没放行?", + "en": "A capability is missing from your build: was it never compiled in, or is it compiled in but still gated off at runtime?" + }, + "deliverable": { + "zh": "每个标志的类型、作用、影响范围、前置依赖与章节指引,外加典型场景的标志组合建议与依赖总图。", + "en": "Flag type, effect, blast radius, prerequisites, and chapter pointer for each flag, grouped in twelve categories, with recommended combinations per scenario." + }, + "boundary": { + "zh": "只管功能标志;settings 层级与环境变量见第5章,标志解锁的工具见附录B;名称与数量非版本恒定。", + "en": "Flags only — settings tiers and environment variables are Chapter 5, unlocked tools are Appendix B. Flag names and counts are not stable across versions." + }, + "sources": [ + "docs-env-vars", + "docs-cli-reference", + "cc-release-v2-1-310", + "docs-settings" + ], + "plain": { + "zh": "用法:想知道某功能为什么不存在就查这表——标志管什么、依赖谁、影响哪些工具;数目随版本会变", + "en": "how to use: feature missing? look up what a flag gates, its dependencies and tools; counts drift by version" + }, + "analogy": { + "zh": "像配电箱上的回路表:哪个闸管哪盏灯、跳了会连带断哪一路,看清了才敢伸手", + "en": "the breaker panel's circuit label: which switch feeds which light, and what else goes dark when one trips" + }, + "plain_evidence": [ + "附录/C-功能标志速查表.md:3", + "附录/C-功能标志速查表.md:9", + "附录/C-功能标志速查表.md:321", + "附录/C-功能标志速查表.md:325", + "en/Appendices/C-Feature-Flag-Reference.md:3", + "en/Appendices/C-Feature-Flag-Reference.md:9", + "en/Appendices/C-Feature-Flag-Reference.md:321", + "en/Appendices/C-Feature-Flag-Reference.md:325" + ] + }, + "apxd": { + "question": { + "zh": "读到一半对某个名词失焦,或要确认本书中英译名是否统一、该词在哪章首次展开?", + "en": "Halfway through a chapter a term slips out of focus, or you need the book's canonical English-Chinese rendering and where it is first discussed?" + }, + "deliverable": { + "zh": "约百条术语的英文原名、推荐中译、简明定义、相关术语与首现章节,另附核心概念关系图。", + "en": "For roughly a hundred core terms: original English name, recommended Chinese translation, concise definition, related terms, and chapter of first appearance." + }, + "boundary": { + "zh": "只给定义与交叉引用,讲解在正文各章;译名为本书约定,词条随版本增删,不保证恒定。", + "en": "Defines, never teaches — the chapters do that. Translations are this book's convention, and entries keep changing as releases add features." + }, + "sources": [ + "docs-glossary", + "docs-how-it-works", + "blog-building-effective-agents" + ], + "plain": { + "zh": "用法:读到生词按字母查——一条给出英文原名、中文译名、短定义、相关词,和正文首次讨论的章节", + "en": "how to use: hit an unfamiliar term, sort by letter for name, translation, short definition and the chapter" + }, + "analogy": { + "zh": "像夹在书末的中英对照小词典:先给译名和一句话解释,再告诉你哪一章细说", + "en": "the bilingual dictionary bound in at the back: the translation and a one-line gloss first, then which chapter goes on" + }, + "plain_evidence": [ + "附录/D-术语表.md:3", + "附录/D-术语表.md:5", + "附录/D-术语表.md:10", + "en/Appendices/D-Glossary.md:3", + "en/Appendices/D-Glossary.md:5", + "en/Appendices/D-Glossary.md:10" + ] + } + } +} diff --git a/yuyu-book/reading-paths.json b/yuyu-book/reading-paths.json new file mode 100644 index 00000000..6d4945bc --- /dev/null +++ b/yuyu-book/reading-paths.json @@ -0,0 +1,134 @@ +{ + "ch01": [ + "start", + "start", + "core", + "core", + "deep", + "core" + ], + "ch02": [ + "start", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch03": [ + "start", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch04": [ + "start", + "core", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch05": [ + "start", + "core", + "core", + "deep", + "deep", + "core" + ], + "ch06": [ + "start", + "core", + "core", + "deep", + "deep", + "core" + ], + "ch07": [ + "start", + "core", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch08": [ + "start", + "core", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch09": [ + "start", + "core", + "core", + "core", + "deep", + "deep", + "deep", + "core" + ], + "ch10": [ + "start", + "deep", + "core", + "core", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch11": [ + "start", + "core", + "core", + "deep", + "core", + "deep", + "core" + ], + "ch12": [ + "start", + "core", + "core", + "deep", + "deep", + "core" + ], + "ch13": [ + "start", + "core", + "deep", + "core", + "deep", + "deep", + "core" + ], + "ch14": [ + "start", + "core", + "core", + "core", + "deep", + "deep", + "core" + ], + "ch15": [ + "start", + "deep", + "core", + "deep", + "core", + "deep", + "core" + ] +} \ No newline at end of file diff --git a/yuyu-book/requirements.txt b/yuyu-book/requirements.txt new file mode 100644 index 00000000..4a88bf93 --- /dev/null +++ b/yuyu-book/requirements.txt @@ -0,0 +1,6 @@ +Markdown>=3.7,<4 +Pygments>=2.18,<3 +playwright>=1.45,<2 +PyMuPDF>=1.24,<2 +fonttools>=4.50,<5 # subsetting the CJK running-head face +Pillow>=10.4,<13 diff --git a/yuyu-book/revisions.json b/yuyu-book/revisions.json new file mode 100644 index 00000000..bfeacf22 --- /dev/null +++ b/yuyu-book/revisions.json @@ -0,0 +1,94 @@ +{ + "entries": [ + { + "version": "0.3.1", + "date": "2026-10-26", + "zh": [ + "扉页改为自绘:一辆四马单辕、圆盖三十辐的古车白描矢量图,取代原先从仓库封面位图裁切重染的做法。构建器从此不派生也不写入任何位图,纸面同时撤掉那层会把图版光栅化的滤镜;中文版整书 HTML 由 5.6 MB 降到 5.2 MB,PDF 由 16.1 MB 降到 15.7 MB。", + "章首页的罗盘回到三卡一行:A4 版心只有 665px,永远低于窄屏断点,于是三张卡在纸上竖成一列、章首半页空着。现在纸上与屏幕同为三列,罗盘连同新词面板一页收齐。", + "标题不再自我重复:前言的眉题与它自己的大标题同字,改为不渲染眉题。", + "措辞按单元类型走:罗盘三张卡原先一律写「本章」,而前言讲的是全书、附录不是章;新词面板同样分「开篇新词 / 本章新词 / 本附录新词」三种称呼。", + "手绘的线路进了正文:每章章眉下多一条走线——焊点、支线、两处抬弯,由单元 id 定种,20 章没有两条相同,重建时又一根根回到原处;四类提示块的 ✦ ⚠ ↗ ◎ ※ 字体符号换成手写墨记(中文版 74 处);小节之间的 ❖ 改为一对手绘焊盘夹一段耦合线(111 处)。都是 currentColor 的内联矢量,屏幕、暗色与纸面共用一份。", + "英文版不再中英夹杂:来源页每一行的读取快照、许可声明与用途说明都是本书自己写的中文,此前整段照登在英文页上。现在 35 条来源的 title / authority / snapshot / license / scope 一律配英文镜像字段,渲染按语言取值;两条社区帖的中文标题也给了英译,原文仍可凭 URL 与帖子号回溯。门禁随之收紧:镜像缺失、或镜像里仍带汉字,构建直接失败。", + "标点按语言走:英文版的提示块标签与「Source anchors」后面挂的是全角冒号,整书 142 处;现在冒号随语言切换,英文版归零。新增一条测试,断言英文来源页与罗盘里不出现汉字。", + "补上一批从未运行的测试:tests/test_pages.py 的共享基类 Config 没有继承 TestCase,封面、版权页、罗盘、目录与来源页的 22 个用例被 unittest 静默跳过。改成 TestCase 后它们真的在跑了,整套 303 个用例全绿;英文版整书 PDF 为 488 页 / 10.8 MB。", + "来源页不再把一条来源切成两半:每条登记是一枚带边框的卡片,跨页断开就成了两个破框,现在整行 break-inside:avoid,版式契约里也补了这条断言。", + "分部过渡页多了一条自绘的路:部目标与章表之间横卧一条手绘道路——起点一枚辐轮,每章一枚站圈,站间一块里程石,末站再套一道光环收束这段行程;由部 id 定种,四个部(4/4/4/3 章)画的便不是同一条。章末也补上了车辙:两轮压出的双线一路延伸,末尾散成几段短划,一章原本戛然留白的地方从此有了落笔处。两者同为 currentColor 的内联矢量、不带任何 id,重建时分毫不差;页面测试与版式契约各补了断言。", + "纸上两处收口:分部过渡页原先顶着上一章的页眉——PDF 的页眉分区表故意跳过了部页,于是「第一部分」那一页右上印的是「前言」;现在每个书签都开自己的一页眉,这张表也从构建流程里拆成了可测的 `pdf_meta()`。主题索引里 COWORKER_TYPE_TELEMETRY 这类长标识符会把点引线挤成一小截,现在术语可断行、引线有 1.2rem 保底,版式契约各补一条。", + "全书多了一张线路图:目录之后自成一页,把二十个单元画成一条路——前言是路头的辐轮,轮下标 00;十五章是四段蛇形路上依次排开的站圈;附录走虚线,是停在路边的岔线,末尾一枚同心圆收车。图形由种子定形,重建分毫不差;图下的列表把同一条路线用文字再走一遍并带链接,屏幕阅读器与搜索都够得着(图本身 aria-hidden)。纸上为守住一页版心隐去每段目标句——分部过渡页本就用大字重述它——并把铜色重染为深铜。侧栏目录也多了一枚带轮的入口。", + "修掉一处没人看见的空栏:宽屏规则 nav.rail{display:block} 写在 nav.rail{display:none} 之前,同特异度后写者胜,于是 1360px 以上的屏幕为侧栏预留了 17.5rem,却什么都不画,正文边上一条白。顺序调正,侧栏回来了;版式契约补一条断言,规定「显示」必须写在「隐藏」之后。", + "白话浮窗不再被裁掉:浮窗原先只在 44rem(704px)以下退回视口底部的说明条,可一个坐在 46rem 行末的术语,浮窗还要向右再伸出 10.5rem——720px 的屏幕上整整 100px 悬在页外,整份文档因此多出一条横向滚动,读者得往右挪才看得懂自己悬停的东西。断点改为算出来的:版心 + 浮窗 = 67rem。另一处更隐蔽——宽表自带横向滚动,会把画在它里面的浮窗一并裁掉,实测 1360px 以上有 13 张表的浮窗整块落在可见单元格之外(悬停什么也没有),其中一张还凭空多出 78px 的假滚动。表格里的术语从此在任何宽度都用底部说明条。两条各补一条 CSS 契约测试,其中一条断言两种触发共用同一份几何,改一处不会忘了改另一处。", + "纸边外那一列没人认领:宽表在 390px 屏幕上向右伸出 493px,而浏览器在被拖动之前不画滚动条,读者看到的是一列被切掉一半——以为那就是最后一列。外壳现在带真正的 tabindex=\"0\",图版框早就有的那根细铜色滚动条也分它一份(原先 CSS 里写的 tab-index:0 根本不是个属性,从来没生效过);代码块同理,最长的一行伸出 1555px,键盘此前完全够不着。实测按 14 次右方向键,能把 471px 的隐藏部分一路走到底,铜色描边落在板内不被裁掉。中英两版按 390 / 720 / 1360px 重扫全书:越界盒子 0,无人认领的横向滚动 0。", + "长名字终于有地方可以断了:中文正文是两端对齐的,下一根词如果整根放不下,这一行就会被拉成字距空洞。桌面宽度上量到 56 个各自占掉 30% 以上行宽、且完全无从下刀的 ``/``,最狠的 `getFeatureValue_CACHED_MAY_BE_STALE` 在 390px 上独占 96% 行宽。CSS 修不了——对照实验里 `line-break:normal` 一点没变,`overflow-wrap:anywhere` 只让四个样本中的一个多断了一次,而显式 `` 把它们拆成 7 到 13 段:`overflow-wrap` 既不制造“优先断点”,也不参与 min-content 计算,所以纸面上它连早先附录 B 那张被顶出版心的表都没真正救回来。新加的 `soften_long_names()` 是全书最后一趟(术语匹配与目录都得按原文读,不能被切开),只看 ≥12 字符的连续串,且只在 `_`、`/` 与 camelCase 边界下刀,`
`/``/标题/HTML 实体一律不动,普通英文词永不被切断。同一段落开合两次截图对照,中英两版按 390–1440px 重扫:越界盒子 0,无人认领的横向滚动 0,构建器测试 331 项全绿。",
+        "章末不再把一张空纸留在身后:车辙原先孤零零排在单元末尾,纸面上它连同那点留白放不下时,整张纸就空着——中文版 3 张、英文版 2 张这样的页。现在车辙与它收束的那一段缝成一整块(超过一页高的尾巴不缝),另有一处更隐蔽:章块 2.2rem 的屏幕下边距在纸上成了版心够不着的气口,Chromium 会为它单独留一张纸,而下一单元的强制换页使它永久化——纸上这条内边距归零。两版重印后各自 446 页与 489 页,空白纸 0 张。修订说明也顺手排了版:笔记里的反引号改成真的代码元素,长标识符的断点从逐单元改到整书装配后统一施加,因为前置与后置内容从不走单元管线——一条引用 `getFeatureValue_CACHED_MAY_BE_STALE` 的修订说明,此前把整份文档在 390px 屏幕上顶出 37px。构建器测试 342 项全绿。"
+      ],
+      "en": [
+        "A drawn cover, not a cropped one: the front page now carries a hand-inked four-horse chariot in the same line idiom as the emblems, replacing the raster derived from the repository's cover image. The builder writes no bitmap at all, and the print filter that was rasterising plates is gone; the Chinese HTML fell from 5.6 MB to 5.2 MB and the PDF from 16.1 MB to 15.7 MB.",
+        "The compass is three cards across again: the A4 type block is 665px, always under the narrow-screen breakpoint, so on paper the cards stacked into one tall column and half the opener sat empty. Opener and new-terms panel now fit a single sheet.",
+        "Titles stopped repeating themselves: the foreword's eyebrow was its own H1, so it renders no eyebrow at all.",
+        "Wording follows the unit type: the compass said 本章 of the foreword and the appendices, which are not chapters; the panel is now 开篇新词 / 本章新词 / 本附录新词 to match.",
+        "Wiring drawn, not typed: every chapter head now closes on a hand-run trace -- pads, stubs, two bends lifted off the baseline -- seeded from the unit id, so all 20 differ and a rebuild redraws each one exactly where it was. The five callout marks (✦ ⚠ ↗ ◎ ※) are inked drawings rather than font glyphs (74 in the Chinese edition), and the ❖ between sections became a pair of pads around a coupling line (111 of them). All inline currentColor vectors, so screen, dark mode and paper share one copy.",
+        "The English book stops code-switching: every line of the sources page -- access snapshot, licence note, and what the source is used for -- is text this book wrote itself, and until now it shipped in Chinese inside the English edition. All 35 entries now carry English mirrors of title, authority, snapshot, license and scope, and the renderer picks by language; the two forum threads keep a searchable English title too, with the original reachable through the URL and post id. The registry now refuses to build if a mirror is missing or still contains Han characters.",
+        "Punctuation follows the language: the English edition wore a full-width colon after every callout label and after Source anchors -- 142 of them -- and now carries none. A new test asserts the English sources page and compass contain no Han characters at all.",
+        "Twenty-two tests that never ran: the shared Config base class in tests/test_pages.py did not extend unittest.TestCase, so discovery silently dropped the cover, imprint, compass, contents and registry cases. They run now; the suite is 303 tests green, and the English PDF stands at 488 pages / 10.8 MB.",
+        "A source entry is no longer cut in half: each registry row is a bordered card, and a page break through one left two broken frames. Rows now hold together, and the layout contract asserts it.",
+        "The part dividers gained a drawn road: between the part's goal and its chapter list runs one hand-inked route — a spoked wheel to start, a station ring per chapter, a milepost between each pair and a second ring where the leg closes, seeded by the part id so the four parts (4/4/4/3 chapters) do not roll the same road. Chapter ends picked up their wheel ruts: two tracks that run on and fray into short dashes, giving the bare band a chapter used to stop in something to end on. Both are currentColor inline vectors carrying no id, identical rebuild to rebuild; the page tests and the layout contract each assert them.",
+        "Two paper fixes: a part divider used to wear the running head of the chapter that ended before it, because the PDF section map skipped part pages — every bookmark now opens its own head, and that map came out of the build path into a testable `pdf_meta()`. And a long identifier such as COWORKER_TYPE_TELEMETRY squeezed its dotted leader down to a stub; terms may now break, with a 1.2rem floor under the leader.",
+        "The book has a route map now: a page of its own after the contents draws all twenty units as one road -- the foreword the spoked wheel at the head of it, wearing 00; the fifteen chapters station rings set out along four serpentine legs; the appendices a dashed siding running off the end, closed by a target ring. Seeded, so the map is the same map rebuild to rebuild. The list under it walks the same route in text and carries the links, because the picture is aria-hidden and a search box cannot reach a drawn number. On paper the per-leg goals are dropped -- each part divider already sets its goal in large type -- which keeps the page inside one sheet, and the bronze is re-inked for the printer. The sidebar picked up a wheeled entry of its own.",
+        "One empty gutter nobody noticed: the wide-screen rule nav.rail{display:block} was written before nav.rail{display:none}, so at equal specificity the later one won and every screen 1360px or wider reserved 17.5rem for a sidebar that never drew. The order is fixed, the rail is back, and the layout contract now asserts that showing must be written after hiding.",
+        "The plain-language popover stopped being cut off. It fell back to a bottom caption only below 44rem (704px), but a term sitting at the end of the 46rem measure needs another 10.5rem to its right, so on a 720px screen 100px of the bubble hung outside the page and the whole document grew a sideways scrollbar the reader had to follow just to read a definition. The breakpoint is now arithmetic: measure + bubble = 67rem. A less visible case sat behind it -- a wide table scrolls sideways and clips whatever is painted inside it, so at 1360px and above 13 of the book's tables hid their bubbles entirely outside the visible cells (hovering showed nothing) and one grew 78px of phantom scroll. Terms inside a table take the bottom caption at every width. Both rules have CSS contract tests, one of which pins the two triggers to the same geometry.",
+        "A column cut off at the edge belonged to nobody. On a 390px screen the widest appendix table ran 493px past the box while the browser painted no scrollbar until dragged, so the reader saw a half-sliced column and took it for the last one. The wrapper now carries a real tabindex=\"0\", and the thin bronze bar the plate frames have always had was given to tables too -- the `tab-index:0` previously written in the stylesheet is not a CSS property at all and never applied. Code plates got the same stop: their longest line overhangs by 1555px and was unreachable without a mouse. Fourteen presses of ArrowRight now walk a 471px block to its end, with the ring drawn inside the frame. Both editions were re-swept at 390 / 720 / 1360px: no boxes off the page, no unadvertised sideways scroll.",
+        "Long identifiers finally got somewhere to break. Chinese prose is justified, so when the next token cannot fit, the line is stretched into a channel of whitespace. At desktop width 56 inline ``/`` tokens were measured that each took more than 30% of a line with no break opportunity at all -- `getFeatureValue_CACHED_MAY_BE_STALE` filled 96% of a 390px line. CSS cannot fix this: in a controlled comparison `line-break:normal` changed nothing, `overflow-wrap:anywhere` bought one extra break out of four samples, while an explicit `` split the same tokens into 7 to 13 fragments. `overflow-wrap` creates no preferred break and feeds no min-content width, which is also why it had never really rescued the Appendix B table that overflowed the page box. The new `soften_long_names()` runs last over the whole unit (term matching and the outline have to read the text as written), examines only runs of twelve characters or more, cuts only at `_`, `/` and camelCase boundaries, and leaves `
`, ``, headings and HTML entities alone, so ordinary English words are never hyphen-hacked. The same paragraph was screenshotted with and without the pass; both editions were re-swept from 390 to 1440px -- no boxes off the page, no unadvertised sideways scroll, 331 builder tests green.",
+        "Chapters stopped leaving a blank sheet behind them. The ruts sat alone at the end of a unit, and when they and their trailing gap would not fit, the whole sheet stayed empty — 3 such pages in Chinese, 2 in English. Each pair is now stitched to the block it closes (a tail taller than a page is left alone). A second, subtler one: the article's 2.2rem bottom padding is air between chapters on screen, but on paper a strip the text block cannot reach, so Chromium kept a sheet alive just to paint it and the next unit's forced break made it permanent; that padding is zero in print now. Both editions reprinted: 446 pages Chinese, 489 English, 0 empty sheets. The revision notes were typeset too — backticked names became real code elements, and the break points for long identifiers moved from per-unit to the assembled book, because front and back matter never runs the unit pipeline: a note quoting `getFeatureValue_CACHED_MAY_BE_STALE` had pushed the document 37px past a 390px screen. 342 builder tests green."
+      ]
+    },
+    {
+      "version": "0.3.0",
+      "date": "2026-10-26",
+      "zh": [
+        "纸面留白:篇分隔页的天地留白收紧,分级图例、引注清单与章尾注各自成块、不再被页底切开,正文段落设了孤寡行下限;两语版全书零页顶出版心。",
+        "图版尺寸:竖版里实在放不下的长条图,改占一张自己的横向 A4 折页(中文版 15 张、英文版 22 张);特别高的图按自身比例把高度上限抬到 150–245mm(中文版 6 张、英文版 9 张),不再被压成一团看不清的色块。",
+        "图版可检索:删掉打印时套在图版上的一层滤镜——它会把整张图光栅化,图里的字退化成图片、PDF 体积翻倍。改后图版里的文字恢复成真文字,可选中、可检索;中文版 PDF 从 30.8 MB 降到 16.1 MB,英文版从 26.6 MB 降到 11.9 MB。",
+        "表格与标签:长标志名(如 CONCURRENCY_SAFE_TOOLS)在构建时插入软断点,不再把整张表顶出版心;来源标签显示完整名称不再截断,题词块继承所在容器的对齐方式。",
+        "折页不再碎版:一张横向图版原先会裂成两三页(图在上一页、图题独占下一页,前后还各留一页空白),现在图与图题稳稳落在同一张纸上,中文版省 2 页、英文版省 4 页。"
+      ],
+      "en": [
+        "Whitespace on paper: part openers breathe less, while the tier legend, the citation list and each chapter's closing notes now hold together as blocks instead of being sliced by the foot of the page, paragraphs got orphan/widow floors, and nothing still spills past the type block in either language.",
+        "Plate sizing: a band too wide to fit a portrait column now takes a landscape fold-out of its own (15 in Chinese, 22 in English), and very tall plates raise their own height cap to 150–245mm (6 and 9 respectively) rather than being crushed into an unreadable smear.",
+        "Searchable plates: a CSS filter applied to plates for print was rasterising the whole subtree — the words inside a diagram became pixels and the PDF doubled. Those words are real text again, selectable and searchable; the Chinese PDF fell from 30.8 MB to 16.1 MB and the English one from 26.6 MB to 11.9 MB.",
+        "Tables and chips: long flag names such as CONCURRENCY_SAFE_TOOLS get soft break points at build time, so one of them can no longer shove an entire table past the type block; source chips show their whole label, and epigraphs inherit the alignment of the container they sit in.",
+        "Fold-outs stopped fragmenting: a landscape plate used to break across two or three sheets — drawing here, caption on the next, an empty sheet on either side. Drawing and caption now hold one sheet, saving two pages in Chinese and four in English."
+      ]
+    },
+    {
+      "version": "0.2.0",
+      "date": "2026-10-26",
+      "zh": [
+        "通俗层:每章一句白话导读带加一个比方;术语首现处画虚线,屏幕悬停出浮窗、纸面退回章首「本章新词」面板;每个小节盖「起步 / 主干 / 进阶」徽,前言据此给出三条阅读路线。术语库 100 条,中文版落 292 处标注、英文版 289 处。",
+        "插图:20 枚单元章徽与 4 幅篇题刻,全部手写单线白描、只用 currentColor 上色,一份图同时管屏幕、暗色模式与纸面。",
+        "图版配色:正文里 696 条 classDef、192 条 style 自带的 Material/Tailwind 色,构建时按色相重染进本书七组色(蓝→青金、绿→玉、橙黄→铜、红→朱砂、紫→藕、近白→卡纸、其余→素纸)。角色区分保留,色板与正文一致;Markdown 一字未改。纸面上图版高度上限由 200mm 降到 150mm,让半数留白的页面就能容下图,消掉页底大片空洞。",
+        "版式:正文栏与图版/表格/代码栏分宽(measure 与 furniture),中文与拉丁字母各配自己的书口宽度;章首、罗盘、新词面板与代码板的留白整体收紧,纸面退回整幅版心。"
+      ],
+      "en": [
+        "A plain-language layer: a one-sentence lead band and an everyday analogy at the head of every chapter; a dotted underline at each term's first use, opening a popover on screen and falling back to a 'new terms in this chapter' panel on paper; a Start / Core / Advanced stamp on every section, which the foreword's three reading routes are built from. 100 glosses, 292 marks in the Chinese edition and 289 in the English one.",
+        "Illustrations: 20 unit emblems and 4 part headpieces, hand-drawn single-weight line art inked entirely in currentColor, so one drawing serves screen, dark mode and paper.",
+        "Diagram palette: the 696 classDef and 192 style directives the manuscript brings with it are re-inked at build time by hue into seven book families (blue to sky, green to jade, orange to brass, red to cinnabar, purple to plum, near-white to card, the rest to plain paper). Role distinctions survive, the manuscript is untouched. On paper a plate now caps at 150mm instead of 200mm, so any page with half its column left can host one — which removes most of the holes at the foot of the page.",
+        "Typography: the prose measure and the furniture width are now separate, Latin and CJK get different line lengths, and the chapter head, compass cards, term panels and code plates are set tighter — the cap stands down on paper."
+      ]
+    },
+    {
+      "version": "0.1.0",
+      "date": "2026-10-25",
+      "zh": [
+        "首个离线整书版:20 个单元(前言、15 章、4 附录)合成单文件 HTML/PDF。",
+        "每单元注入问题/交付/边界罗盘与来源锚点;314 个 Mermaid 图预渲染为统一主题 SVG。",
+        "跨章引用(第N章/附录X)自动链接;附录 D 术语表生成书末主题索引。"
+      ],
+      "en": [
+        "First offline edition: 20 units (foreword, 15 chapters, 4 appendices) assembled into a single HTML/PDF.",
+        "Each unit carries a question/deliverable/boundary compass with source anchors; all 314 Mermaid diagrams pre-rendered as themed SVG plates.",
+        "Cross-chapter mentions (Chapter N/Appendix X) auto-linked; the glossary feeds the back-matter subject index."
+      ]
+    }
+  ]
+}
diff --git a/yuyu-book/sources.json b/yuyu-book/sources.json
new file mode 100644
index 00000000..24cb34b2
--- /dev/null
+++ b/yuyu-book/sources.json
@@ -0,0 +1,585 @@
+{
+  "cc-changelog": {
+    "title": "Claude Code CHANGELOG",
+    "authority": "Anthropic (anthropics/claude-code)",
+    "kind": "github",
+    "url": "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md",
+    "pinned": {
+      "commit": "318b7e6ac4e726e88a4ad326214866a93c373b1b",
+      "release_at_head": "2.1.310"
+    },
+    "snapshot": "2026-10-25 读取(raw.githubusercontent 该 commit 下 CHANGELOG.md,HTTP 200,首条版本号为 2.1.310)",
+    "snapshot_en": "2026-10-25 read (CHANGELOG.md at that commit via raw.githubusercontent, HTTP 200; first listed version 2.1.310)",
+    "accessed": "2026-10-25",
+    "license": "© Anthropic PBC, All rights reserved;链接引用,不转载",
+    "license_en": "© Anthropic PBC, all rights reserved; linked, never republished",
+    "scope": "全书时序核对的总账本:凡本书写「某版本引入/修复某行为」的能力叙述(对话循环、工具集增减、权限询问、hooks 触发时机、子智能体与 Fork、技能/插件加载、MCP、压缩与缓存、Plan 模式)都用它逐条比对,避免把旧版本行为写成现状。",
+    "scope_en": "Chronology ledger for the whole book: every claim that a version introduced or fixed a behaviour — the conversation loop, tool-set changes, permission prompts, hook timing, subagents and forks, skill and plugin loading, MCP, compaction and caching, Plan mode — is checked against it line by line, so older behaviour is never written up as current."
+  },
+  "cc-release-v2-1-310": {
+    "title": "Claude Code Release v2.1.310",
+    "authority": "Anthropic (anthropics/claude-code)",
+    "kind": "github",
+    "url": "https://github.com/anthropics/claude-code/releases/tag/v2.1.310",
+    "pinned": {
+      "tag": "v2.1.310",
+      "commit": "b70dc4113ad72e0ee44f7b1b95afaf510b53ee98"
+    },
+    "snapshot": "2026-10-25 读取(GitHub Releases API:最新 release 为 v2.1.310,发布于 2026-10-23T23:39:00Z,tag 指向 commit b70dc411)",
+    "snapshot_en": "2026-10-25 read (GitHub Releases API: latest release v2.1.310, published 2026-10-23T23:39:00Z; tag points at commit b70dc411)",
+    "accessed": "2026-10-25",
+    "license": "© Anthropic PBC, All rights reserved;链接引用,不转载",
+    "license_en": "© Anthropic PBC, all rights reserved; linked, never republished",
+    "scope": "全书「分析基准版本」的锚点:凡本书声称「以 vX.Y.Z 为准」的能力清单、默认值与行为差异(尤其附录 C 功能标志、第 13 章性能相关改动)都以此 release 与其 changelog 条目对齐。",
+    "scope_en": "Anchor for the book's stated baseline version: the capability lists, defaults and behavioural differences attributed to 'as of vX.Y.Z' — the feature flags in Appendix C and the performance changes of Chapter 13 above all — are aligned with this release and its changelog entry."
+  },
+  "cc-issues": {
+    "title": "Claude Code Issue Tracker",
+    "authority": "Anthropic (anthropics/claude-code)",
+    "kind": "github",
+    "url": "https://github.com/anthropics/claude-code/issues",
+    "pinned": {
+      "commit": "318b7e6ac4e726e88a4ad326214866a93c373b1b"
+    },
+    "snapshot": "2026-10-25 读取(页面 HTTP 200;issue 列表为流动内容,故同时钉住仓库 HEAD commit 作为核对基线)",
+    "snapshot_en": "2026-10-25 read (page HTTP 200; the issue list is fluid content, so the repository HEAD commit is pinned as the comparison baseline as well)",
+    "accessed": "2026-10-25",
+    "license": "© Anthropic PBC, All rights reserved;链接引用,不转载",
+    "license_en": "© Anthropic PBC, all rights reserved; linked, never republished",
+    "scope": "「文档没写但实际会踩」的行为证据源:权限询问回归、hooks 在 Windows/Git Bash 缺失时的放行、压缩后仍超上下文、子智能体与 Fork 的并发问题等第 4、8、9、13 章的缺陷叙述与边界案例,用 issue 报告核对而非凭印象断言。",
+    "scope_en": "Evidence for behaviour the documentation does not state but users actually hit: permission-prompt regressions, hooks letting actions through when Windows/Git Bash pieces are missing, context still overflowing after compaction, subagent and fork concurrency. The defect accounts and edge cases of Chapters 4, 8, 9 and 13 are checked against reports rather than asserted from memory."
+  },
+  "gh-agent-sdk-python": {
+    "title": "Claude Agent SDK for Python",
+    "authority": "Anthropic (anthropics/claude-agent-sdk-python)",
+    "kind": "github",
+    "url": "https://github.com/anthropics/claude-agent-sdk-python",
+    "pinned": {
+      "tag": "v0.2.174",
+      "commit": "0e89b298e771259378a6cd3734e00d323dff5703"
+    },
+    "snapshot": "2026-10-25 读取(releases/latest 返回 v0.2.174,发布于 2026-10-24T00:24:14Z;tag 解析到 commit 0e89b298)",
+    "snapshot_en": "2026-10-25 read (releases/latest returned v0.2.174, published 2026-10-24T00:24:14Z; tag resolves to commit 0e89b298)",
+    "accessed": "2026-10-25",
+    "license": "MIT;链接引用,不转载源码片段之外的内容",
+    "license_en": "MIT; linked and cited, nothing beyond minimal source fragments republished",
+    "scope": "第 15 章「构建你自己的 Agent Harness」与第 2、8、9 章的 SDK 侧对照:query 循环、工具注册、hooks 回调、子智能体声明、结构化输出等 API 形态是否与我们从 Claude Code 内部推演出的接口一致。",
+    "scope_en": "Chapter 15's build-your-own-harness work and the SDK-side comparisons in Chapters 2, 8 and 9: whether the query loop, tool registration, hook callbacks, subagent declarations and structured output match the interfaces we derived from Claude Code's internals."
+  },
+  "gh-agent-sdk-typescript": {
+    "title": "Claude Agent SDK for TypeScript",
+    "authority": "Anthropic (anthropics/claude-agent-sdk-typescript)",
+    "kind": "github",
+    "url": "https://github.com/anthropics/claude-agent-sdk-typescript",
+    "pinned": {
+      "tag": "v0.3.310",
+      "commit": "019515d3268e6f94946eb2d8e9edc1ea79c5a6aa"
+    },
+    "snapshot": "2026-10-25 读取(releases/latest 返回 v0.3.310,发布于 2026-10-23T23:39:13Z;tag 解析到 commit 019515d3)",
+    "snapshot_en": "2026-10-25 read (releases/latest returned v0.3.310, published 2026-10-23T23:39:13Z; tag resolves to commit 019515d3)",
+    "accessed": "2026-10-25",
+    "license": "仓库未公开 SPDX 声明,以仓库内 LICENSE 为准;链接引用,不转载",
+    "license_en": "No SPDX declaration published in the repository; its own LICENSE file governs; linked, never republished",
+    "scope": "第 15 章 harness 骨架的 TypeScript 参照实现(Claude Code 本体即 TS/Node 生态),并用于核对第 13 章流式事件与消息迭代器的对外类型;SDK 版本号尾数与 CLI 2.1.310 同步这一事实也在此核对。",
+    "scope_en": "TypeScript reference implementation for the harness skeleton in Chapter 15 (Claude Code itself lives in the TS/Node ecosystem), and the check on the public types for streaming events and the message iterator in Chapter 13; also where we confirm the SDK version suffix tracks CLI 2.1.310."
+  },
+  "gh-book-repo": {
+    "title": "御舆:解码 Agent Harness(本书仓库)",
+    "title_en": "Yù Yú: Decoding Agent Harness (this repository)",
+    "authority": "lintsinghua/claude-code-book",
+    "kind": "github",
+    "url": "https://github.com/lintsinghua/claude-code-book",
+    "pinned": {
+      "commit": "12e29d6697993f5f6809c6d01755ff61dc6bb755"
+    },
+    "snapshot": "2026-10-25 读取(默认分支 main,HEAD commit 12e29d66,提交时间 2026-10-16T08:17:38Z)",
+    "snapshot_en": "2026-10-25 read (default branch main, HEAD commit 12e29d66, committed 2026-10-16T08:17:38Z)",
+    "accessed": "2026-10-25",
+    "license": "CC BY-NC-SA 4.0(文字);GitHub 未标注 SPDX",
+    "license_en": "CC BY-NC-SA 4.0 (text); no SPDX label on GitHub",
+    "scope": "自我引用锚点:钉住本书正文与「已核实来源」之间的对应版本,便于后续 diff 出哪些章节结论是在哪个提交上做出的;同时用于附录 A 源码导航地图与仓库目录结构的互查。",
+    "scope_en": "Self-reference anchor: pins the pairing between the book's text and its verified sources, so a later diff shows which commit each chapter conclusion was drawn against; also cross-checks Appendix A's source-navigation map against the repository layout."
+  },
+  "npm-claude-code": {
+    "title": "@anthropic-ai/claude-code(npm 发布元数据)",
+    "title_en": "@anthropic-ai/claude-code (npm registry metadata)",
+    "authority": "Anthropic(npm registry 官方包)",
+    "authority_en": "Anthropic (official npm registry package)",
+    "kind": "npm",
+    "url": "https://registry.npmjs.org/@anthropic-ai/claude-code",
+    "pinned": {
+      "version": "2.1.310",
+      "distTag": "latest",
+      "shasum": "b5029e530ac7794608227f2ef47d313b1af90460"
+    },
+    "snapshot": "2026-10-25 读取(registry API HTTP 200:dist-tags latest=2.1.310 / stable=2.1.308 / next=2.1.310;2.1.310 发布时间 2026-10-23T21:20:46.540Z;共 550 个已发布版本)",
+    "snapshot_en": "2026-10-25 read (registry API HTTP 200: dist-tags latest=2.1.310 / stable=2.1.308 / next=2.1.310; 2.1.310 published 2026-10-23T21:20:46.540Z; 550 published versions in total)",
+    "accessed": "2026-10-25",
+    "license": "包内 license 字段为 SEE LICENSE IN README.md(专有);仅引用元数据,不转载包内容",
+    "license_en": "The package's license field reads SEE LICENSE IN README.md (proprietary); only metadata is cited, never the package contents",
+    "scope": "第 5 章安装/发布渠道与第 13 章冷启动、包体大小、Node 版本要求(该包声明 engines.node >= 22)的事实来源;stable/latest/next 三个 dist-tag 的差异也是「用户实际拿到的版本」与「GitHub 最新版本」可能不一致的解释依据。",
+    "scope_en": "The factual source for installation and release channels in Chapter 5 and for cold start, package size and the Node requirement in Chapter 13 (the package declares engines.node >= 22). The gap between the stable/latest/next dist-tags also explains why the version a user actually gets may differ from the newest one on GitHub."
+  },
+  "mcp-spec-2026-07-28": {
+    "title": "Model Context Protocol Specification (2026-07-28)",
+    "authority": "Model Context Protocol 开源项目",
+    "authority_en": "The Model Context Protocol open-source project",
+    "kind": "spec",
+    "url": "https://modelcontextprotocol.io/specification/2026-07-28",
+    "pinned": {
+      "spec-version": "2026-07-28",
+      "released": "2026-07-28T16:47:49Z"
+    },
+    "snapshot": "2026-10-25 读取(规范页 HTTP 200;GitHub releases/latest 亦确认 2026-07-28 为最新定版,2025-11-25 为上一版)",
+    "snapshot_en": "2026-10-25 read (spec page HTTP 200; GitHub releases/latest also confirms 2026-07-28 as the current release and 2025-11-25 as the previous one)",
+    "accessed": "2026-10-25",
+    "license": "规范仓库许可以其仓库声明为准;链接引用,不转载条文",
+    "license_en": "Licensing follows the specification repository's own statement; linked, clauses never republished",
+    "scope": "第 12 章协议基线:tools / resources / prompts 三类原语、生命周期与版本协商、传输层(stdio 与 HTTP)、能力协商的术语定义,用来判定 Claude Code 的 MCP 集成「实现了什么、简化了什么」,并以 2025-11-25 旧版做差异对照。",
+    "scope_en": "Chapter 12's protocol baseline: the tools / resources / prompts primitives, lifecycle and version negotiation, transports (stdio and HTTP) and the vocabulary of capability negotiation. It is what lets us judge what Claude Code's MCP integration implements and what it simplifies, with 2025-11-25 as the diff reference."
+  },
+  "blog-building-effective-agents": {
+    "title": "Building Effective Agents(Anthropic 工程博客,2024-12-19)",
+    "title_en": "Building Effective Agents (Anthropic engineering blog, 2024-12-19)",
+    "authority": "Anthropic Engineering / Research",
+    "kind": "blog",
+    "url": "https://www.anthropic.com/research/building-effective-agents",
+    "pinned": {
+      "published": "2024-12-19"
+    },
+    "snapshot": "2026-10-25 读取(anthropic.com/engineering/building-effective-agents 301 跳转到 /research/ 同名页,HTTP 200,页面 datePublished=2024-12-19)",
+    "snapshot_en": "2026-10-25 read (anthropic.com/engineering/building-effective-agents 301-redirects to the same path under /research/, HTTP 200; datePublished=2024-12-19)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 博客内容版权所有;链接引用,不转载",
+    "license_en": "Copyright Anthropic; linked, never republished",
+    "scope": "第 1 章与附录 D 的概念底座:workflow 与 agent 的区分、「简单且可组合的模式优先」这一设计取向,为本书解释 Claude Code 为何把循环做小、把控制权交给模型提供作者自陈的依据。",
+    "scope_en": "Conceptual ground for Chapter 1 and Appendix D: the workflow/agent distinction and the preference for simple, composable patterns first — the authors' own account of why to keep the loop small and hand control to the model."
+  },
+  "blog-multi-agent-research": {
+    "title": "How we built our multi-agent research system(Anthropic 工程博客,2025-06-13)",
+    "title_en": "How we built our multi-agent research system (Anthropic engineering blog, 2025-06-13)",
+    "authority": "Anthropic Engineering",
+    "kind": "blog",
+    "url": "https://www.anthropic.com/research/multi-agent-research-system",
+    "pinned": {
+      "published": "2025-06-13"
+    },
+    "snapshot": "2026-10-25 读取(/engineering/ 路径 301 跳转到 /research/ 同名页,HTTP 200,datePublished=2025-06-13)",
+    "snapshot_en": "2026-10-25 read (the /engineering/ path 301-redirects to the same page under /research/, HTTP 200; datePublished=2025-06-13)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 博客内容版权所有;链接引用,不转载",
+    "license_en": "Copyright Anthropic; linked, never republished",
+    "scope": "第 9、10 章多智能体论证的官方一手材料:编排者—工作者分工、上下文隔离的收益与代价、并行调度与 token 成本放大、失败模式(分工模糊、状态不一致),用于校准本书对协调器模式利弊的判断,而非只凭代码结构反推。",
+    "scope_en": "First-party material for the multi-agent argument of Chapters 9 and 10: orchestrator–worker division of labour, the gains and costs of context isolation, parallel scheduling and token-cost amplification, failure modes (fuzzy division of work, inconsistent state). Used to calibrate the book's judgement rather than to rely on reverse engineering alone."
+  },
+  "blog-writing-tools-for-agents": {
+    "title": "Writing effective tools for AI agents(Anthropic 工程博客,2025-09-11)",
+    "title_en": "Writing effective tools for AI agents (Anthropic engineering blog, 2025-09-11)",
+    "authority": "Anthropic Engineering",
+    "kind": "blog",
+    "url": "https://www.anthropic.com/research/writing-tools-for-agents",
+    "pinned": {
+      "published": "2025-09-11"
+    },
+    "snapshot": "2026-10-25 读取(/engineering/ 路径 301 跳转到 /research/ 同名页,HTTP 200,datePublished=2025-09-11)",
+    "snapshot_en": "2026-10-25 read (the /engineering/ path 301-redirects to the same page under /research/, HTTP 200; datePublished=2025-09-11)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 博客内容版权所有;链接引用,不转载",
+    "license_en": "Copyright Anthropic; linked, never republished",
+    "scope": "第 3 章工具系统与第 12 章 MCP 工具暴露的设计准则:工具数量与选择准确率的张力、返回体瘦身、可组合性与评测驱动的迭代,用来解释 Claude Code 内置工具的粒度划分与 MCP 工具命名前缀的动机。",
+    "scope_en": "Design guidance for the tool system of Chapter 3 and MCP tool exposure in Chapter 12: the tension between tool count and selection accuracy, slimming result payloads, composability and evaluation-driven iteration. Explains the granularity of built-in tools and the motive behind MCP tool-name prefixes."
+  },
+  "blog-context-engineering": {
+    "title": "Effective context engineering for AI agents(Anthropic 工程博客,2025-09-29)",
+    "title_en": "Effective context engineering for AI agents (Anthropic engineering blog, 2025-09-29)",
+    "authority": "Anthropic Engineering",
+    "kind": "blog",
+    "url": "https://www.anthropic.com/research/effective-context-engineering-for-ai-agents",
+    "pinned": {
+      "published": "2025-09-29"
+    },
+    "snapshot": "2026-10-25 读取(/engineering/ 路径 301 跳转到 /research/ 同名页,HTTP 200,datePublished=2025-09-29)",
+    "snapshot_en": "2026-10-25 read (the /engineering/ path 301-redirects to the same page under /research/, HTTP 200; datePublished=2025-09-29)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 博客内容版权所有;链接引用,不转载",
+    "license_en": "Copyright Anthropic; linked, never republished",
+    "scope": "第 6、7 章上下文与记忆的立论依据:上下文预算与「退场」策略、compaction 的定位、长期记忆外置到文件的理由;本书讨论「为什么把记忆写成 CLAUDE.md 而不是数据库」时以该文为官方立场参照。",
+    "scope_en": "The basis for context and memory in Chapters 6 and 7: context budgets and exit strategies, the place of compaction, the case for keeping long-term memory in files. Where the book asks why memory is CLAUDE.md rather than a database, this is the official position."
+  },
+  "docs-how-it-works": {
+    "title": "How Claude Code works",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/how-claude-code-works",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 How Claude Code works;并出现在官方 sitemap 与 llms.txt 索引中)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title How Claude Code works; also listed in the official sitemap and the llms.txt index)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 1、2 章与前言的官方口径基线:agentic loop、内置工具族、与项目交互方式的定义。凡本书给「对话循环」下术语,都先与此页措辞比对,避免自造概念与官方命名冲突。",
+    "scope_en": "Official wording baseline for the preface and Chapters 1 and 2: the agentic loop, the built-in tool families, interaction with a project. Every term the book uses for the conversation loop is compared with this page first, so our coinages do not clash with official naming."
+  },
+  "docs-best-practices": {
+    "title": "Best practices for Claude Code",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/best-practices",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200;原工程博客 anthropic.com/engineering/claude-code-best-practices 现已 301 重定向到本页,故以本页为准登记)",
+    "snapshot_en": "2026-10-25 read (HTTP 200; the former engineering post anthropic.com/engineering/claude-code-best-practices now 301-redirects here, so this page is the registered location)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "前言、第 1 章与第 15 章的「官方推荐用法」来源:环境配置、并行会话扩展、把复杂任务拆给子智能体的实践建议。本书凡引用「Anthropic 建议这样做」的表述,以此页为最新版本落点(旧博客地址已并入此处)。",
+    "scope_en": "The official recommended usage behind the preface, Chapter 1 and Chapter 15: environment setup, scaling with parallel sessions, splitting complex work into subagents. Every 'Anthropic recommends …' statement registers here as the current location, since the old blog post merged into this page."
+  },
+  "docs-interactive-mode": {
+    "title": "Interactive mode",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/interactive-mode",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Interactive mode)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Interactive mode)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 2 章与第 14 章的输入侧事实来源:快捷键(含 Shift+Tab 切换权限/Plan 模式)、输入模式、斜杠命令与中断/追加输入的时机,用来核对本书描述的「循环在人机接口处如何被打断与续接」。",
+    "scope_en": "The input side of Chapters 2 and 14: keyboard shortcuts (Shift+Tab switches permission and Plan modes), input modes, slash commands, and when interruption or appended input takes effect — how the loop breaks and resumes at the human interface."
+  },
+  "docs-cli-reference": {
+    "title": "CLI reference",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/cli-reference",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 CLI reference)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title CLI reference)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 15 章与附录 C 的命令/旗标清单来源:子命令、旗标名称与默认值。本书所有写到具体命令行参数处以此页核对拼写与是否存在,防止把内部未公开参数当 CLI 参数写进正文。",
+    "scope_en": "The command and flag inventory behind Chapter 15 and Appendix C: subcommands, flag names and defaults. Every command-line parameter the book mentions is checked here for spelling and existence, so undocumented internals are never written up as CLI flags."
+  },
+  "docs-tools-reference": {
+    "title": "Tools reference",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/tools-reference",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Tools reference;llms.txt 描述含 permission requirements 与 per-tool behavior)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Tools reference; the llms.txt description covers permission requirements and per-tool behavior)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 3 章与附录 B「工具完整清单」的权威对照表:内置工具名录、每个工具的权限要求与行为差异。本书附录 B 的条目数、工具命名、哪些工具需要询问都以本页为准。",
+    "scope_en": "Authoritative cross-check for Chapter 3 and the complete tool list in Appendix B: the roster of built-in tools, each one's permission requirement and behaviour. Appendix B's entry count, tool naming and which tools prompt all follow this page."
+  },
+  "docs-permissions": {
+    "title": "Configure permissions",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/permissions",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Configure permissions;llms.txt 描述为 fine-grained permission rules, modes, and managed policies)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Configure permissions; the llms.txt description reads fine-grained permission rules, modes, and managed policies)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 4 章权限管线的规则语言来源:allow/deny 规则语法、规则与模式的优先级、受管策略。本书描述权限判定顺序、工具名匹配与 Bash 规则细粒度时必须与此页语义一致。",
+    "scope_en": "The rule language of Chapter 4's permission pipeline: allow/deny rule syntax, precedence between rules and modes, managed policies. The book's decision order, tool-name matching and Bash rule granularity must match this page's semantics."
+  },
+  "docs-permission-modes": {
+    "title": "Choose a permission mode",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/permission-modes",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Choose a permission mode;注:/docs/en/plan-mode 实测 404,Plan 模式在官方文档中并入本页)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Choose a permission mode; note that /docs/en/plan-mode returned 404 in testing — Plan mode is folded into this page)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 14 章 Plan 模式与第 4 章模式机的主要官方定义:模式集合、切换途径(Shift+Tab / IDE / 模式选择器)、各模式的读写边界。本书「Plan 模式是一种权限模式而非独立执行引擎」这一论断的证据来源。",
+    "scope_en": "Main official definition behind Plan mode in Chapter 14 and the mode machine in Chapter 4: the mode set, the switching paths (Shift+Tab / IDE / mode selector) and each mode's read–write boundary. It is the evidence for the claim that Plan mode is a permission mode rather than a separate execution engine."
+  },
+  "docs-settings": {
+    "title": "Settings files and precedence",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/settings",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Settings files and precedence;llms.txt 含 scope 归属与多文件冲突时取值的描述)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Settings files and precedence; llms.txt covers scope ownership and which value wins when files conflict)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 5 章配置分层的权威依据:用户/项目/本地/受管各层文件位置、优先级合并规则、生效自检方法;附录 A 描述配置目录布局时同样以本页核对。",
+    "scope_en": "Authority for configuration layering in Chapter 5: where user/project/local/managed files live, how precedence merges, how to verify what took effect. Appendix A's description of the configuration directory layout is checked here as well."
+  },
+  "docs-env-vars": {
+    "title": "Environment variables",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/env-vars",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Environment variables)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Environment variables)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 4、5、11、13 章与附录 C「功能标志速查表」的外部开关清单来源:哪些行为可由环境变量开启/关闭、变量名与作用域。本书把内部 feature flag 与用户可用开关区分表述时以本页为界。",
+    "scope_en": "The list of external switches for Chapters 4, 5, 11 and 13 and the feature-flag quick reference in Appendix C: which behaviours environment variables can turn on or off, their names and their scope. The boundary where the book separates internal feature flags from user-facing switches."
+  },
+  "docs-memory": {
+    "title": "How Claude remembers your project (CLAUDE.md / auto memory)",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/memory",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 How Claude remembers your project;llms.txt 提及 CLAUDE.md 或 AGENTS.md 与 auto memory)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title How Claude remembers your project; llms.txt mentions CLAUDE.md or AGENTS.md and auto memory)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 6 章记忆系统的规格来源:记忆文件层级与加载条件、导入语法、AGENTS.md 兼容、auto memory 的写入位置与失效边界。本书关于「记忆何时进入上下文」的所有叙述以此页加 changelog 核对。",
+    "scope_en": "Specification of the memory system in Chapter 6: the memory-file hierarchy and load conditions, import syntax, AGENTS.md compatibility, where auto memory writes and where it stops applying. Everything about when memory enters the context is checked here plus against the changelog."
+  },
+  "docs-context-window": {
+    "title": "Explore the context window",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/context-window",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Explore the context window;llms.txt 描述为上下文窗口填充过程的交互模拟)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Explore the context window; llms.txt describes an interactive simulation of how the window fills)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 7 章上下文预算的量化口径:自动加载项、单次文件读取与工具结果的成本、压缩阈值与余量。本书给出的 token 预算示意与「什么被挤出去」的判定顺序需与本页一致。",
+    "scope_en": "The quantitative frame for Chapter 7's context budget: automatic inclusions, the cost of a single file read or tool result, compaction thresholds and headroom. The book's token illustrations and its 'what gets pushed out' ordering must agree with this page."
+  },
+  "docs-hooks": {
+    "title": "Hooks reference",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/hooks",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,页面标题 Hooks reference;另有 /docs/en/hooks-guide 教程页,本版登记未单列)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, page title Hooks reference; a tutorial page exists at /docs/en/hooks-guide and is not registered separately in this edition)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 8 章钩子系统的核心规格:事件名(PreToolUse/PostToolUse 等)、输入 JSON 字段、退出码与返回字段的控制语义、matcher 规则、超时与并发。本书生命周期扩展点的状态机与拦截返回值表以此页逐字段核对。",
+    "scope_en": "Core specification of the hook system in Chapter 8: event names (PreToolUse/PostToolUse and friends), input JSON fields, exit codes and the control semantics of returned fields, matcher rules, timeout and concurrency. The lifecycle state machine and the interception-return table are checked field by field here."
+  },
+  "docs-sub-agents": {
+    "title": "Create custom subagents",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/sub-agents",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Create custom subagents)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Create custom subagents)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 9 章子智能体的官方定义来源:agent 描述文件字段、工具与权限收窄、模型选择、上下文隔离与调用方式;本书区分「子智能体」与「Fork」两种模式的边界以此页与 changelog 交叉确认。",
+    "scope_en": "Official definition for Chapter 9's subagents: the agent description-file fields, narrowing of tools and permissions, model selection, context isolation and invocation. The boundary between 'subagent' and 'fork' is confirmed here and against the changelog."
+  },
+  "docs-agent-teams": {
+    "title": "Orchestrate teams of Claude Code sessions",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/agent-teams",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Orchestrate teams of Claude Code sessions)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Orchestrate teams of Claude Code sessions)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 10 章协调器/多会话编排的官方形态:共享任务、跨会话消息、集中管理能力的命名与边界。本书讨论协调器模式时用它说明 Claude Code 已产品化的那一层,与内部编排机制区分开。",
+    "scope_en": "The official form of multi-session orchestration in Chapter 10: shared tasks, cross-session messages, and the naming and limits of centrally managed capability. Used to separate what Claude Code has already productised from the internal orchestration machinery."
+  },
+  "docs-skills": {
+    "title": "Extend Claude with skills",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/skills",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Extend Claude with skills;llms.txt 描述含自定义命令与内置技能)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Extend Claude with skills; the llms.txt description includes custom commands and built-in skills)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 11 章技能系统来源:SKILL.md 结构、frontmatter 字段、渐进式披露与按需加载、技能与自定义命令的关系。本书关于技能如何进入上下文、如何被触发的描述以此页核对。",
+    "scope_en": "Chapter 11's skill system: SKILL.md structure, frontmatter fields, progressive disclosure and on-demand loading, the relation between skills and custom commands. The book's account of how skills enter the context and get triggered is checked here."
+  },
+  "docs-plugins": {
+    "title": "Plugins overview",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/plugins",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Plugins overview;/docs/en/plugins/overview 为同一页别名,亦 200)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Plugins overview; /docs/en/plugins/overview is an alias of the same page and also returns 200)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 11 章插件打包与市场机制的官方口径:插件可携带哪些组件(技能、hooks、MCP、命令)、何时该用插件而非独立技能或 MCP 服务器、安装与加载路径。",
+    "scope_en": "Official account of plugin packaging and the marketplace in Chapter 11: which components a plugin may carry (skills, hooks, MCP, commands), when a plugin beats a standalone skill or MCP server, and the install and load paths."
+  },
+  "docs-mcp": {
+    "title": "Connect Claude Code to tools via MCP",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/mcp",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Connect Claude Code to tools via MCP)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Connect Claude Code to tools via MCP)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 12 章 MCP 集成的产品侧规格:服务器作用域与配置文件位置、工具命名与权限询问、资源与 prompts 的支持程度、认证方式。本书据此论证 Claude Code 对规范的取舍实现。",
+    "scope_en": "Product-side specification of the MCP integration in Chapter 12: server scopes and where configuration lives, tool naming and permission prompts, how far resources and prompts are supported, authentication methods. The book's argument about what Claude Code kept and dropped from the spec rests on it."
+  },
+  "docs-prompt-caching": {
+    "title": "How Claude Code uses prompt caching",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/prompt-caching",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 How Claude Code uses prompt caching;llms.txt 描述含模型切换触发未缓存回合与 /compact 成本)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title How Claude Code uses prompt caching; llms.txt mentions uncached turns on model switch and the cost of /compact)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 13 章流式与性能的官方成本模型:缓存断点位置、前缀稳定性要求、压缩与模型切换如何打断缓存、由此带来的首 token 延迟与计费差异;本书延迟数字的解释框架。",
+    "scope_en": "The official cost model behind streaming and performance in Chapter 13: cache breakpoint placement, prefix-stability requirements, how compaction and model switches break the cache, and the resulting time-to-first-token and billing differences. The frame for the book's latency numbers."
+  },
+  "docs-headless": {
+    "title": "Run Claude Code programmatically (headless)",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/headless",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Run Claude Code programmatically;llms.txt 描述为经 Agent SDK 从 CLI/Python/TypeScript 运行)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Run Claude Code programmatically; llms.txt describes running it from the CLI/Python/TypeScript through the Agent SDK)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 15 章构建 harness 的对外契约:非交互运行、输出格式(含结构化流式输出)、权限在无人值守下的处理。本书把它当作「官方最小 harness」的例子,与内部循环实现做对照。",
+    "scope_en": "The public contract for building a harness in Chapter 15: non-interactive runs, output formats including structured streaming output, and how permissions are handled with nobody watching. Treated as the example of a minimal official harness, contrasted with the internal loop."
+  },
+  "docs-agent-sdk-overview": {
+    "title": "Agent SDK overview",
+    "authority": "Anthropic (code.claude.com/docs/en/agent-sdk)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/agent-sdk/overview",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Agent SDK overview)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Agent SDK overview)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "第 9、13、15 章的「把 Claude Code 当库用」视角:SDK 暴露的循环、工具、hooks、子智能体、流式与权限接口。本书据此判断哪些内部机制已上升为公共 API、哪些仍是实现细节,避免把 SDK 能力当成 CLI 私有能力来写。",
+    "scope_en": "The 'use Claude Code as a library' perspective for Chapters 9, 13 and 15: the loop, tools, hooks, subagents, streaming and permission interfaces the SDK exposes. It lets the book judge which internal mechanisms became public API and which are still implementation detail."
+  },
+  "docs-glossary": {
+    "title": "Glossary",
+    "authority": "Anthropic (code.claude.com/docs)",
+    "kind": "docs",
+    "url": "https://code.claude.com/docs/en/glossary",
+    "pinned": {
+      "docs-revision": "2026-10-25 在线版"
+    },
+    "snapshot": "2026-10-25 读取(HTTP 200,标题 Glossary;llms.txt 描述覆盖 agentic loop、compaction、CLAUDE.md、hooks、subagents、MCP 等术语)",
+    "snapshot_en": "2026-10-25 read (HTTP 200, title Glossary; the llms.txt description covers agentic loop, compaction, CLAUDE.md, hooks, subagents, MCP and more)",
+    "accessed": "2026-10-25",
+    "license": "Anthropic 文档版权(专有/条款见站点声明);链接引用,不转载",
+    "license_en": "Anthropic documentation copyright (proprietary; terms per the site's own statement); linked, never republished",
+    "scope": "附录 D 术语表与全书术语一致性的对照源:官方对 agentic loop、compaction、subagent、harness 相关词的定义与用词习惯,本书自造译名时在此做映射与差异标注。",
+    "scope_en": "Cross-check for the glossary in Appendix D and term consistency throughout: official definitions and usage habits for agentic loop, compaction, subagent and harness-related words. Where the book coins its own translation, the mapping and the divergence are recorded here."
+  },
+  "ld-topic-1870593": {
+    "title": "【开源千星】《御舆:解码 Agent Harness——Claude Code 架构深度剖析》",
+    "title_en": "[Open source, 1,000+ stars] Yuyu: Decoding the Agent Harness",
+    "authority": "LINUX DO 社区(帖子 ID 1870593,板块:文档共建)",
+    "authority_en": "LINUX DO community (post 1870593, docs co-authoring board)",
+    "kind": "community",
+    "url": "https://linux.do/t/topic/1870593",
+    "pinned": {
+      "topic-id": "1870593",
+      "post-created": "2026-03-31/04-01"
+    },
+    "snapshot": "2026-10-25 尝试读取:站点 Cloudflare 反爬,直连与 Discourse JSON 接口均返回 403,正文未能抓取;主题标题、板块、发帖人与日期经搜索引擎索引多页结果交叉确认",
+    "snapshot_en": "2026-10-25 attempted read: the site sits behind Cloudflare anti-scraping, direct fetch and the Discourse JSON endpoint both returned 403 and the body could not be captured; topic title, board, author and date were cross-confirmed through search-engine indexes of several pages",
+    "accessed": "2026-10-25",
+    "license": "社区帖子版权归原发帖人;链接引用,不转载",
+    "license_en": "Copyright the original poster; linked, never republished",
+    "scope": "本书的社区首发与读者反馈索引(README.md:103 致谢 Linux.Do 的落点)。仅用于记录「本书在中文社区的首发时间与讨论语境」,属历史脉络,不作为任何技术事实的支撑来源。",
+    "scope_en": "The index of this book's community first publication and reader feedback, which is where the Linux.do acknowledgement in README.md lands. It records when the book appeared in the Chinese-language community and the discussion around it: historical context, and support for no technical claim.",
+    "normative": false
+  },
+  "ld-topic-1871393": {
+    "title": "[开源] 基于站内佬友 claude-code-book 项目构建的网页版 book",
+    "title_en": "[Open source] A web-edition book built on a forum member's claude-code-book project",
+    "authority": "LINUX DO 社区(帖子 ID 1871393,板块:开发调优)",
+    "authority_en": "LINUX DO community (post 1871393, development tuning board)",
+    "kind": "community",
+    "url": "https://linux.do/t/topic/1871393",
+    "pinned": {
+      "topic-id": "1871393",
+      "post-created": "2026-04-01"
+    },
+    "snapshot": "2026-10-25 尝试读取:直连返回 403(Cloudflare 反爬),正文未能抓取;标题、日期与被衍生项目名经搜索引擎索引确认",
+    "snapshot_en": "2026-10-25 attempted read: direct fetch returned 403 (Cloudflare anti-scraping) and the body could not be captured; title, date and the derived project's name were confirmed through search-engine indexes",
+    "accessed": "2026-10-25",
+    "license": "社区帖子版权归原发帖人;链接引用,不转载",
+    "license_en": "Copyright the original poster; linked, never republished",
+    "scope": "附录 A 之外的生态旁证:社区基于本书仓库衍生的在线阅读版,说明本书内容以仓库为唯一权威源、衍生站点不作为引用依据。仅作来源谱系记录,不支撑技术结论。",
+    "scope_en": "Ecosystem evidence beyond Appendix A: an online reading edition derived from this book's repository by a community member, which shows the repository is the single authoritative source and the derived site is not a citation target. Provenance record only; it supports no technical conclusion.",
+    "normative": false
+  }
+}
diff --git a/yuyu-book/subject-index.json b/yuyu-book/subject-index.json
new file mode 100644
index 00000000..8560e6f5
--- /dev/null
+++ b/yuyu-book/subject-index.json
@@ -0,0 +1,2554 @@
+[
+  {
+    "key": "ablation baseline",
+    "letter": "A",
+    "zh": {
+      "term": "Ablation Baseline",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "第 13 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Ablation Baseline",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "Ch 13"
+        }
+      ]
+    }
+  },
+  {
+    "key": "agent",
+    "letter": "A",
+    "zh": {
+      "term": "Agent",
+      "refs": [
+        {
+          "anchor": "ch01",
+          "label": "第 1 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Agent",
+      "refs": [
+        {
+          "anchor": "ch01",
+          "label": "Ch 1"
+        }
+      ]
+    }
+  },
+  {
+    "key": "agent harness",
+    "letter": "A",
+    "zh": {
+      "term": "Agent Harness",
+      "refs": [
+        {
+          "anchor": "ch01",
+          "label": "第 1 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Agent Harness",
+      "refs": [
+        {
+          "anchor": "ch01",
+          "label": "Ch 1"
+        }
+      ]
+    }
+  },
+  {
+    "key": "agent memory snapshot",
+    "letter": "A",
+    "zh": {
+      "term": "Agent Memory Snapshot",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "第 10 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Agent Memory Snapshot",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "Ch 10"
+        }
+      ]
+    }
+  },
+  {
+    "key": "agent trigger",
+    "letter": "A",
+    "zh": {
+      "term": "Agent Trigger",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Agent Trigger",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "always allow / always deny / always ask",
+    "letter": "A",
+    "zh": {
+      "term": "Always Allow / Always Deny / Always Ask",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "第 4 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Always Allow / Always Deny / Always Ask",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "Ch 4"
+        }
+      ]
+    }
+  },
+  {
+    "key": "api turn",
+    "letter": "A",
+    "zh": {
+      "term": "API Turn",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "API Turn",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "async generator",
+    "letter": "A",
+    "zh": {
+      "term": "Async Generator",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Async Generator",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "auto compact",
+    "letter": "A",
+    "zh": {
+      "term": "Auto Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Auto Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "auto mode",
+    "letter": "A",
+    "zh": {
+      "term": "Auto Mode",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Auto Mode",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "auto theme",
+    "letter": "A",
+    "zh": {
+      "term": "Auto Theme",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Auto Theme",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "away summary",
+    "letter": "A",
+    "zh": {
+      "term": "Away Summary",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Away Summary",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "background session (bg_sessions)",
+    "letter": "B",
+    "zh": {
+      "term": "Background Session (BG_SESSIONS)",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Background Session (BG_SESSIONS)",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "bash classifier",
+    "letter": "B",
+    "zh": {
+      "term": "Bash Classifier",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Bash Classifier",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "bridge mode",
+    "letter": "B",
+    "zh": {
+      "term": "Bridge Mode",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Bridge Mode",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "buddy",
+    "letter": "B",
+    "zh": {
+      "term": "Buddy",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Buddy",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "build tool (buildtool)",
+    "letter": "B",
+    "zh": {
+      "term": "Build Tool (buildTool)",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Build Tool (buildTool)",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "cache safe params",
+    "letter": "C",
+    "zh": {
+      "term": "Cache Safe Params",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Cache Safe Params",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "cached micro compact",
+    "letter": "C",
+    "zh": {
+      "term": "Cached Micro Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Cached Micro Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "channel",
+    "letter": "C",
+    "zh": {
+      "term": "Channel",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "第 6 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Channel",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "Ch 6"
+        }
+      ]
+    }
+  },
+  {
+    "key": "chicago_mcp",
+    "letter": "C",
+    "zh": {
+      "term": "CHICAGO_MCP",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "CHICAGO_MCP",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "claude.md",
+    "letter": "C",
+    "zh": {
+      "term": "CLAUDE.md",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "第 6 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "CLAUDE.md",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "Ch 6"
+        }
+      ]
+    }
+  },
+  {
+    "key": "cli (command line interface)",
+    "letter": "C",
+    "zh": {
+      "term": "CLI (Command Line Interface)",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "CLI (Command Line Interface)",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "compaction",
+    "letter": "C",
+    "zh": {
+      "term": "Compaction",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Compaction",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "companion sprite",
+    "letter": "C",
+    "zh": {
+      "term": "Companion Sprite",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Companion Sprite",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "concurrency safe",
+    "letter": "C",
+    "zh": {
+      "term": "Concurrency Safe",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Concurrency Safe",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "context collapse",
+    "letter": "C",
+    "zh": {
+      "term": "Context Collapse",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Context Collapse",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "context window",
+    "letter": "C",
+    "zh": {
+      "term": "Context Window",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Context Window",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "coordinator mode",
+    "letter": "C",
+    "zh": {
+      "term": "Coordinator Mode",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "第 10 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Coordinator Mode",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "Ch 10"
+        }
+      ]
+    }
+  },
+  {
+    "key": "coworker_type_telemetry",
+    "letter": "C",
+    "zh": {
+      "term": "COWORKER_TYPE_TELEMETRY",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "第 13 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "COWORKER_TYPE_TELEMETRY",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "Ch 13"
+        }
+      ]
+    }
+  },
+  {
+    "key": "cron tool",
+    "letter": "C",
+    "zh": {
+      "term": "Cron Tool",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Cron Tool",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "daemon",
+    "letter": "D",
+    "zh": {
+      "term": "Daemon",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Daemon",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "direct_connect",
+    "letter": "D",
+    "zh": {
+      "term": "DIRECT_CONNECT",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "DIRECT_CONNECT",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "dynamic tool registration",
+    "letter": "D",
+    "zh": {
+      "term": "Dynamic Tool Registration",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Dynamic Tool Registration",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "enhanced telemetry",
+    "letter": "E",
+    "zh": {
+      "term": "Enhanced Telemetry",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "第 13 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Enhanced Telemetry",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "Ch 13"
+        }
+      ]
+    }
+  },
+  {
+    "key": "extended thinking",
+    "letter": "E",
+    "zh": {
+      "term": "Extended Thinking",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "第 5 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Extended Thinking",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "Ch 5"
+        }
+      ]
+    }
+  },
+  {
+    "key": "extract memories",
+    "letter": "E",
+    "zh": {
+      "term": "Extract Memories",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "第 10 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Extract Memories",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "Ch 10"
+        }
+      ]
+    }
+  },
+  {
+    "key": "file persistence",
+    "letter": "F",
+    "zh": {
+      "term": "File Persistence",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "File Persistence",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "fork mode / fork subagent",
+    "letter": "F",
+    "zh": {
+      "term": "Fork Mode / Fork Subagent",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Fork Mode / Fork Subagent",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "fullscreen layout",
+    "letter": "F",
+    "zh": {
+      "term": "Fullscreen Layout",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Fullscreen Layout",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "globtool",
+    "letter": "G",
+    "zh": {
+      "term": "GlobTool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "GlobTool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "greptool",
+    "letter": "G",
+    "zh": {
+      "term": "GrepTool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "GrepTool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "hard fail",
+    "letter": "H",
+    "zh": {
+      "term": "Hard Fail",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Hard Fail",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "history snip",
+    "letter": "H",
+    "zh": {
+      "term": "History Snip",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "History Snip",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "hook",
+    "letter": "H",
+    "zh": {
+      "term": "Hook",
+      "refs": [
+        {
+          "anchor": "ch08",
+          "label": "第 8 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Hook",
+      "refs": [
+        {
+          "anchor": "ch08",
+          "label": "Ch 8"
+        }
+      ]
+    }
+  },
+  {
+    "key": "hook prompts",
+    "letter": "H",
+    "zh": {
+      "term": "Hook Prompts",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Hook Prompts",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "ide integration",
+    "letter": "I",
+    "zh": {
+      "term": "IDE Integration",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "IDE Integration",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "ink",
+    "letter": "I",
+    "zh": {
+      "term": "Ink",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Ink",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "job classifier",
+    "letter": "J",
+    "zh": {
+      "term": "Job Classifier",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Job Classifier",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "jwt (json web token)",
+    "letter": "J",
+    "zh": {
+      "term": "JWT (JSON Web Token)",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "JWT (JSON Web Token)",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "kairos",
+    "letter": "K",
+    "zh": {
+      "term": "KAIROS",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "第 6 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "KAIROS",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "Ch 6"
+        }
+      ]
+    }
+  },
+  {
+    "key": "kairos dream",
+    "letter": "K",
+    "zh": {
+      "term": "KAIROS Dream",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "第 6 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "KAIROS Dream",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "Ch 6"
+        }
+      ]
+    }
+  },
+  {
+    "key": "llm (large language model)",
+    "letter": "L",
+    "zh": {
+      "term": "LLM (Large Language Model)",
+      "refs": [
+        {
+          "anchor": "ch01",
+          "label": "第 1 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "LLM (Large Language Model)",
+      "refs": [
+        {
+          "anchor": "ch01",
+          "label": "Ch 1"
+        }
+      ]
+    }
+  },
+  {
+    "key": "lodestone",
+    "letter": "L",
+    "zh": {
+      "term": "Lodestone",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "第 10 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Lodestone",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "Ch 10"
+        }
+      ]
+    }
+  },
+  {
+    "key": "main loop",
+    "letter": "M",
+    "zh": {
+      "term": "Main Loop",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Main Loop",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "mcp (model context protocol)",
+    "letter": "M",
+    "zh": {
+      "term": "MCP (Model Context Protocol)",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "MCP (Model Context Protocol)",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "mcp skills",
+    "letter": "M",
+    "zh": {
+      "term": "MCP Skills",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "MCP Skills",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "memory",
+    "letter": "M",
+    "zh": {
+      "term": "Memory",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "第 6 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Memory",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "Ch 6"
+        }
+      ]
+    }
+  },
+  {
+    "key": "memory injection",
+    "letter": "M",
+    "zh": {
+      "term": "Memory Injection",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "第 6 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Memory Injection",
+      "refs": [
+        {
+          "anchor": "ch06",
+          "label": "Ch 6"
+        }
+      ]
+    }
+  },
+  {
+    "key": "message actions",
+    "letter": "M",
+    "zh": {
+      "term": "Message Actions",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Message Actions",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "micro compact",
+    "letter": "M",
+    "zh": {
+      "term": "Micro Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Micro Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "monitor tool",
+    "letter": "M",
+    "zh": {
+      "term": "Monitor Tool",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Monitor Tool",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "multi-tier settings",
+    "letter": "M",
+    "zh": {
+      "term": "Multi-tier Settings",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "第 5 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Multi-tier Settings",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "Ch 5"
+        }
+      ]
+    }
+  },
+  {
+    "key": "native client attestation",
+    "letter": "N",
+    "zh": {
+      "term": "Native Client Attestation",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Native Client Attestation",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "permission mode",
+    "letter": "P",
+    "zh": {
+      "term": "Permission Mode",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "第 4 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Permission Mode",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "Ch 4"
+        }
+      ]
+    }
+  },
+  {
+    "key": "permission pipeline",
+    "letter": "P",
+    "zh": {
+      "term": "Permission Pipeline",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "第 4 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Permission Pipeline",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "Ch 4"
+        }
+      ]
+    }
+  },
+  {
+    "key": "plan mode",
+    "letter": "P",
+    "zh": {
+      "term": "Plan Mode",
+      "refs": [
+        {
+          "anchor": "ch14",
+          "label": "第 14 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Plan Mode",
+      "refs": [
+        {
+          "anchor": "ch14",
+          "label": "Ch 14"
+        }
+      ]
+    }
+  },
+  {
+    "key": "power assertion",
+    "letter": "P",
+    "zh": {
+      "term": "Power Assertion",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Power Assertion",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "pretooluse / posttooluse",
+    "letter": "P",
+    "zh": {
+      "term": "PreToolUse / PostToolUse",
+      "refs": [
+        {
+          "anchor": "ch08",
+          "label": "第 8 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "PreToolUse / PostToolUse",
+      "refs": [
+        {
+          "anchor": "ch08",
+          "label": "Ch 8"
+        }
+      ]
+    }
+  },
+  {
+    "key": "proactive mode",
+    "letter": "P",
+    "zh": {
+      "term": "Proactive Mode",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "第 5 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Proactive Mode",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "Ch 5"
+        }
+      ]
+    }
+  },
+  {
+    "key": "prompt cache",
+    "letter": "P",
+    "zh": {
+      "term": "Prompt Cache",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Prompt Cache",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "prompt cache break detection",
+    "letter": "P",
+    "zh": {
+      "term": "Prompt Cache Break Detection",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Prompt Cache Break Detection",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "query engine",
+    "letter": "Q",
+    "zh": {
+      "term": "Query Engine",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Query Engine",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "quick search",
+    "letter": "Q",
+    "zh": {
+      "term": "Quick Search",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Quick Search",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "react",
+    "letter": "R",
+    "zh": {
+      "term": "React",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "React",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "reactive compact",
+    "letter": "R",
+    "zh": {
+      "term": "Reactive Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Reactive Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "readonly tool",
+    "letter": "R",
+    "zh": {
+      "term": "ReadOnly Tool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "ReadOnly Tool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "repl (read-eval-print loop)",
+    "letter": "R",
+    "zh": {
+      "term": "REPL (Read-Eval-Print Loop)",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "REPL (Read-Eval-Print Loop)",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "selector pattern",
+    "letter": "S",
+    "zh": {
+      "term": "Selector Pattern",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Selector Pattern",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "settings sync",
+    "letter": "S",
+    "zh": {
+      "term": "Settings Sync",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "第 10 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Settings Sync",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "Ch 10"
+        }
+      ]
+    }
+  },
+  {
+    "key": "skill",
+    "letter": "S",
+    "zh": {
+      "term": "Skill",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Skill",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "skill generator",
+    "letter": "S",
+    "zh": {
+      "term": "Skill Generator",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Skill Generator",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "slash command",
+    "letter": "S",
+    "zh": {
+      "term": "Slash Command",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Slash Command",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "snip compact",
+    "letter": "S",
+    "zh": {
+      "term": "Snip Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Snip Compact",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "ssh remote",
+    "letter": "S",
+    "zh": {
+      "term": "SSH Remote",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "第 11 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "SSH Remote",
+      "refs": [
+        {
+          "anchor": "ch11",
+          "label": "Ch 11"
+        }
+      ]
+    }
+  },
+  {
+    "key": "state management",
+    "letter": "S",
+    "zh": {
+      "term": "State Management",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "State Management",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "stop reason",
+    "letter": "S",
+    "zh": {
+      "term": "Stop Reason",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Stop Reason",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "stream",
+    "letter": "S",
+    "zh": {
+      "term": "Stream",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "第 13 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Stream",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "Ch 13"
+        }
+      ]
+    }
+  },
+  {
+    "key": "streaming tool executor",
+    "letter": "S",
+    "zh": {
+      "term": "Streaming Tool Executor",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Streaming Tool Executor",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "subagent",
+    "letter": "S",
+    "zh": {
+      "term": "Subagent",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Subagent",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "system prompt",
+    "letter": "S",
+    "zh": {
+      "term": "System Prompt",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "System Prompt",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "team memory (teammem)",
+    "letter": "T",
+    "zh": {
+      "term": "Team Memory (TEAMMEM)",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "第 10 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Team Memory (TEAMMEM)",
+      "refs": [
+        {
+          "anchor": "ch10",
+          "label": "Ch 10"
+        }
+      ]
+    }
+  },
+  {
+    "key": "templates",
+    "letter": "T",
+    "zh": {
+      "term": "Templates",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Templates",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "terminal panel",
+    "letter": "T",
+    "zh": {
+      "term": "Terminal Panel",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Terminal Panel",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "token",
+    "letter": "T",
+    "zh": {
+      "term": "Token",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Token",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "token budget",
+    "letter": "T",
+    "zh": {
+      "term": "Token Budget",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Token Budget",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "tool",
+    "letter": "T",
+    "zh": {
+      "term": "Tool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Tool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "tool interface",
+    "letter": "T",
+    "zh": {
+      "term": "Tool Interface",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Tool Interface",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "tool orchestration",
+    "letter": "T",
+    "zh": {
+      "term": "Tool Orchestration",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Tool Orchestration",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "tool permission context",
+    "letter": "T",
+    "zh": {
+      "term": "Tool Permission Context",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "第 4 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Tool Permission Context",
+      "refs": [
+        {
+          "anchor": "ch04",
+          "label": "Ch 4"
+        }
+      ]
+    }
+  },
+  {
+    "key": "tool registry",
+    "letter": "T",
+    "zh": {
+      "term": "Tool Registry",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Tool Registry",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "toolsearch tool",
+    "letter": "T",
+    "zh": {
+      "term": "ToolSearch Tool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "ToolSearch Tool",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "transcript classifier",
+    "letter": "T",
+    "zh": {
+      "term": "Transcript Classifier",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Transcript Classifier",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "transcription / transcript",
+    "letter": "T",
+    "zh": {
+      "term": "Transcription / Transcript",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Transcription / Transcript",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "tree-sitter",
+    "letter": "T",
+    "zh": {
+      "term": "Tree-sitter",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Tree-sitter",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  },
+  {
+    "key": "turn",
+    "letter": "T",
+    "zh": {
+      "term": "Turn",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "第 2 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Turn",
+      "refs": [
+        {
+          "anchor": "ch02",
+          "label": "Ch 2"
+        }
+      ]
+    }
+  },
+  {
+    "key": "uds (unix domain socket) inbox",
+    "letter": "U",
+    "zh": {
+      "term": "UDS (Unix Domain Socket) Inbox",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "UDS (Unix Domain Socket) Inbox",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "ultraplan",
+    "letter": "U",
+    "zh": {
+      "term": "Ultraplan",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "第 5 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Ultraplan",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "Ch 5"
+        }
+      ]
+    }
+  },
+  {
+    "key": "ultrathink",
+    "letter": "U",
+    "zh": {
+      "term": "Ultrathink",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "第 5 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Ultrathink",
+      "refs": [
+        {
+          "anchor": "ch05",
+          "label": "Ch 5"
+        }
+      ]
+    }
+  },
+  {
+    "key": "unattended retry",
+    "letter": "U",
+    "zh": {
+      "term": "Unattended Retry",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "第 13 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Unattended Retry",
+      "refs": [
+        {
+          "anchor": "ch13",
+          "label": "Ch 13"
+        }
+      ]
+    }
+  },
+  {
+    "key": "verification agent",
+    "letter": "V",
+    "zh": {
+      "term": "Verification Agent",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Verification Agent",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "voice mode",
+    "letter": "V",
+    "zh": {
+      "term": "Voice Mode",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "第 12 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Voice Mode",
+      "refs": [
+        {
+          "anchor": "ch12",
+          "label": "Ch 12"
+        }
+      ]
+    }
+  },
+  {
+    "key": "web browser tool",
+    "letter": "W",
+    "zh": {
+      "term": "Web Browser Tool",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "第 7 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Web Browser Tool",
+      "refs": [
+        {
+          "anchor": "ch07",
+          "label": "Ch 7"
+        }
+      ]
+    }
+  },
+  {
+    "key": "workflow scripts",
+    "letter": "W",
+    "zh": {
+      "term": "Workflow Scripts",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Workflow Scripts",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "worktree",
+    "letter": "W",
+    "zh": {
+      "term": "Worktree",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "第 9 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Worktree",
+      "refs": [
+        {
+          "anchor": "ch09",
+          "label": "Ch 9"
+        }
+      ]
+    }
+  },
+  {
+    "key": "zod schema",
+    "letter": "Z",
+    "zh": {
+      "term": "Zod Schema",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "第 3 章"
+        }
+      ]
+    },
+    "en": {
+      "term": "Zod Schema",
+      "refs": [
+        {
+          "anchor": "ch03",
+          "label": "Ch 3"
+        }
+      ]
+    }
+  }
+]
diff --git a/yuyu-book/tests/__init__.py b/yuyu-book/tests/__init__.py
new file mode 100644
index 00000000..2091226e
--- /dev/null
+++ b/yuyu-book/tests/__init__.py
@@ -0,0 +1,22 @@
+"""Contract suite for the whole-book builder (`yuyu-book/`).
+
+Single path bootstrap for the whole suite: discovery is started with the repo
+root as ``-t``, which puts the repo root on sys.path but not ``yuyu-book/``
+itself, so add it here exactly like build.py does for the CLI. Every module
+can then ``import yuyu_book.*``; the two-line guard at the top of each module
+only repeats this when a file is run directly.
+
+Run from the repo root:
+
+    python -m unittest discover -s yuyu-book/tests -t . -v
+
+Scope: pure Python contracts over the shipped config and the markdown source.
+No network, no browser, no PDF — render_all, pdf.build_pdf, cover_art.build
+and build_edition are never called.
+"""
+import sys
+from pathlib import Path
+
+BOOK_DIR = Path(__file__).resolve().parents[1]
+if str(BOOK_DIR) not in sys.path:
+    sys.path.insert(0, str(BOOK_DIR))
diff --git a/yuyu-book/tests/test_art.py b/yuyu-book/tests/test_art.py
new file mode 100644
index 00000000..553eb4a2
--- /dev/null
+++ b/yuyu-book/tests/test_art.py
@@ -0,0 +1,138 @@
+"""The illustration layer: one line drawing per unit, one per part, one plate.
+
+The book inlines every drawing into a single offline file 20+ times over, so
+the files themselves carry the guarantees that keep the build valid: no ids
+(the audit fails on a duplicate), nothing that reaches outside the document,
+and currentColor so one drawing serves paper, screen and dark mode. The cover
+plate is inlined once, so it gets a budget of its own.
+"""
+import re
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from yuyu_book import art  # noqa: E402
+from yuyu_book.core import (BOOK_DIR, BuildError, load_json,  # noqa: E402
+                            unit_stream)
+
+MANIFEST = load_json(BOOK_DIR / "manifest.json")
+UNIT_IDS = [u["id"] for u, _kind, _part in unit_stream(MANIFEST)]
+PART_IDS = [p["id"] for p in MANIFEST["parts"]]
+
+GOOD = (''
+        '')
+
+
+def _write(tmp: Path, name: str, text: str) -> Path:
+    (tmp / f"{name}.svg").write_text(text, encoding="utf-8")
+    return tmp
+
+
+class ShippedDrawings(unittest.TestCase):
+    @unittest.skipUnless(art.ART_DIR.exists(), "no illustrations yet")
+    def test_every_unit_and_part_has_its_own_drawing(self):
+        gaps = art.missing(UNIT_IDS + [f"part-{p}" for p in PART_IDS])
+        self.assertEqual(gaps, [], f"units without a drawing: {gaps}")
+
+    @unittest.skipUnless(art.ART_DIR.exists(), "no illustrations yet")
+    def test_every_shipped_drawing_passes_the_loader(self):
+        # a drawing that breaks the rules must break the build, not be skipped
+        for name in UNIT_IDS + [f"part-{p}" for p in PART_IDS]:
+            if (art.ART_DIR / f"{name}.svg").exists():
+                self.assertTrue(art._read(name), name)
+
+    @unittest.skipUnless(art.ART_DIR.exists(), "no illustrations yet")
+    def test_the_drawings_stay_small_enough_to_inline(self):
+        for path in sorted(art.ART_DIR.glob("*.svg")):
+            budget = art.PLATE_BYTES if path.stem == "cover" else art.MAX_BYTES
+            size = len(path.read_text(encoding="utf-8").encode("utf-8"))
+            self.assertLessEqual(size, budget, path.name)
+
+    @unittest.skipUnless(art.ART_DIR.exists(), "no illustrations yet")
+    def test_no_drawing_repeats_a_shape_id(self):
+        """24 files inlined into one page: a shared id would collide, and the
+        duplicate-id audit would fire far from the drawing that caused it."""
+        ids = set()
+        for path in sorted(art.ART_DIR.glob("*.svg")):
+            found = set(re.findall(r"\bid=\"([^\"]+)\"", path.read_text("utf-8")))
+            self.assertFalse(found, f"{path.name} declares {sorted(found)}")
+            ids |= found
+
+
+class Loader(unittest.TestCase):
+    def setUp(self):
+        self._dir = art.ART_DIR
+        self.tmp = Path(tempfile.mkdtemp())
+        art.ART_DIR = self.tmp
+
+    def tearDown(self):
+        art.ART_DIR = self._dir
+
+    def test_a_unit_without_a_file_simply_has_no_emblem(self):
+        self.assertEqual(art.emblem("ch99"), "")
+        self.assertEqual(art.missing(["ch99"]), ["ch99"])
+
+    def test_a_clean_drawing_becomes_hidden_markup(self):
+        _write(self.tmp, "ch01", GOOD)
+        out = art.emblem("ch01")
+        self.assertTrue(out.startswith('"
+        _write(self.tmp, "cover", GOOD.replace("", pad + ""))
+        self.assertTrue(art.cover_plate())
+        with self.assertRaises(BuildError):
+            art.emblem("cover")
+
+    def test_a_missing_cover_plate_leaves_the_page_typeset_alone(self):
+        self.assertEqual(art.cover_plate(), "")
+
+    def test_a_drawing_that_cannot_re_ink_is_rejected(self):
+        _write(self.tmp, "ch01", GOOD.replace("currentColor", "#8A6B3F"))
+        with self.assertRaises(BuildError):
+            art.emblem("ch01")
+
+    def test_a_drawing_with_an_id_is_rejected(self):
+        _write(self.tmp, "ch01", GOOD.replace("", ''),
+                    GOOD.replace("",
+                                 ''),
+                    GOOD.replace("",
+                                 '')):
+            _write(self.tmp, "ch01", bad)
+            with self.assertRaises(BuildError):
+                art.emblem("ch01")
+
+    def test_a_drawing_without_a_viewbox_is_rejected(self):
+        _write(self.tmp, "ch01", GOOD.replace('viewBox="0 0 64 64" ', ""))
+        with self.assertRaises(BuildError):
+            art.emblem("ch01")
+
+
+class Stylesheet(unittest.TestCase):
+    def test_the_emblem_slots_are_styled(self):
+        from yuyu_book.style import CSS
+        for cls in (".ch-emblem", ".pd-art", ".cover-art"):
+            self.assertIn(cls, CSS, cls)
+        # the emblem must not be allowed to stretch the head row
+        self.assertIn("margin-inline-start:auto",
+                      CSS[CSS.index(".ch-emblem"):CSS.index(".ch-emblem") + 200])
diff --git a/yuyu-book/tests/test_audit.py b/yuyu-book/tests/test_audit.py
new file mode 100644
index 00000000..6b611950
--- /dev/null
+++ b/yuyu-book/tests/test_audit.py
@@ -0,0 +1,104 @@
+"""The build gate itself: what audit_html refuses to ship."""
+import sys
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from yuyu_book import audit  # noqa: E402
+from yuyu_book.core import BuildError  # noqa: E402
+
+
+def page(n_units=1, svgs=0, extra=""):
+    """The smallest document that satisfies every structural contract."""
+    articles = "".join(
+        f'
' + f'

问题

' + f'

节

正文

' + for i in range(1, n_units + 1)) + figures = "".join(f'
' + for i in range(svgs)) + return (f'{articles}{figures}{extra}') + + +def check(html, n_units=1, expect_svgs=None): + audit.audit(html, n_units=n_units, expect_svgs=expect_svgs) + + +class Accepts(unittest.TestCase): + def test_a_clean_document_passes(self): + check(page(n_units=3, svgs=2)) + + def test_expected_svg_count_is_honoured(self): + check(page(n_units=1, svgs=4), expect_svgs=4) + + def test_source_mode_does_not_demand_svg(self): + check("" + '
' + '
' + '', + n_units=1, expect_svgs=None) + + +class Rejects(unittest.TestCase): + def assertRejects(self, html, needle, **kw): + with self.assertRaises(BuildError) as cm: + check(html, **kw) + self.assertIn(needle, str(cm.exception)) + + def test_duplicate_ids(self): + self.assertRejects(page() + page(), "duplicate ids", n_units=2) + + def test_duplicate_id_inside_a_diagram_counts_too(self): + html = page(svgs=1).replace('', + '' + '') + self.assertIn('id="node0"', html) + self.assertRejects(html, "duplicate ids") + + def test_broken_anchor(self): + self.assertRejects(page(extra='x'), "broken anchors") + + def test_javascript_is_not_shipped(self): + self.assertRejects(page(extra=''), "JS-free") + + def test_external_images_break_the_offline_promise(self): + self.assertRejects(page(extra=''), + "offline guarantee") + + def test_data_uri_images_are_allowed(self): + check(page(extra='')) + + def test_unresolved_mermaid_placeholder(self): + self.assertRejects(page(extra='

@@MM:0123456789ab@@

'), + "unresolved mermaid") + + def test_missing_diagram_is_an_error_not_a_gap(self): + self.assertRejects(page(extra='
缺失图
'), + "missing diagrams") + + def test_svg_shortfall(self): + self.assertRejects(page(svgs=1), "svg count", expect_svgs=2) + + def test_unclosed_fence_swallowing_the_rest_of_the_book(self): + self.assertRejects(page(extra="
x
"), + "nested inside
")
+
+    def test_stray_markdown_fence(self):
+        self.assertRejects(page(extra="\n```python\nx\n```"),
+                           "stray markdown fences")
+
+    def test_residual_md_link(self):
+        self.assertRejects(page(extra='t'),
+                           "residual .md links")
+
+    def test_a_unit_without_a_compass_is_incomplete(self):
+        html = page(n_units=2).replace('

问题

', + "", 1) + self.assertRejects(html, "compass cards", n_units=2) + + def test_unbalanced_close_tag(self): + self.assertRejects(page(extra=""), "unbalanced closing tags") + + +if __name__ == "__main__": + unittest.main() diff --git a/yuyu-book/tests/test_build_contracts.py b/yuyu-book/tests/test_build_contracts.py new file mode 100644 index 00000000..02dc31fc --- /dev/null +++ b/yuyu-book/tests/test_build_contracts.py @@ -0,0 +1,454 @@ +"""Typesetting and delivery contracts: tokens, contrast, print rules, and the +end-to-end no-browser build.""" +import re +import subprocess +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from yuyu_book.core import BOOK_DIR, REPO_ROOT # noqa: E402 +from yuyu_book.mermaid import BOOK_FAMILIES # noqa: E402 +from yuyu_book.style import CSS # noqa: E402 + +MIN_RATIO = 4.5 # WCAG 2.1 AA for body-size text + + +def _tokens(block: str) -> dict[str, str]: + return {k: v for k, v in re.findall(r"(--[\w-]+):\s*(#[0-9A-Fa-f]{6})", block)} + + +def _luminance(hex_: str) -> float: + def chan(i): + c = int(hex_.lstrip("#")[i:i + 2], 16) / 255 + return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 + r, g, b = (chan(i) for i in (0, 2, 4)) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def _ratio(fg: str, bg: str) -> float: + a, b = sorted([_luminance(fg), _luminance(bg)], reverse=True) + return (a + 0.05) / (b + 0.05) + + +LIGHT = _tokens(CSS.split("@media (prefers-color-scheme:dark)")[0]) +DARK = _tokens(re.search(r"body:not\(\.force-light\)\{(.*?)\}", CSS, re.S).group(1)) + + +def _rule(selector: str) -> str: + m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", CSS) + return m.group(1) if m else "" + + +class Palette(unittest.TestCase): + def test_the_book_defines_its_own_token_set(self): + for key in ("paper", "paper-2", "paper-3", "card", "ink", "ink-2", + "ink-3", "line", "line-2", "bronze", "bronze-2", "bronze-3", + "cinnabar", "jade", "sky", "code-bg", "code-fg", "serif", + "kai", "sans", "mono", "measure"): + self.assertIn(f"--{key}", CSS, key) + + def test_dark_mode_overrides_the_surface_and_ink_tokens(self): + for key in ("--paper", "--card", "--ink", "--ink-2", "--ink-3", + "--bronze", "--cinnabar", "--code-bg"): + self.assertIn(key, DARK, f"{key} has no dark override") + + def test_body_text_meets_wcag_aa_on_every_surface(self): + inks = ("--ink", "--ink-2", "--ink-3") + for mode, tokens in (("light", LIGHT), ("dark", DARK)): + for ink in inks: + for surface in ("--paper", "--card", "--paper-2", "--paper-3"): + fg, bg = tokens.get(ink), tokens.get(surface) + self.assertTrue(fg and bg, f"{mode}: missing {ink or surface}") + ratio = _ratio(fg, bg) + self.assertGreaterEqual( + round(ratio, 2), MIN_RATIO, + f"{mode}: {ink} on {surface} is {ratio:.2f}") + + def test_accent_colours_clear_aa_on_the_page(self): + for mode, tokens in (("light", LIGHT), ("dark", DARK)): + for ink in ("--bronze", "--bronze-3", "--cinnabar", "--jade", "--sky"): + ratio = _ratio(tokens[ink], tokens["--paper"]) + self.assertGreaterEqual(round(ratio, 2), MIN_RATIO, + f"{mode}: {ink} is {ratio:.2f} on paper") + + def test_diagram_families_keep_their_labels_readable(self): + """Re-inked plates are the one place a colour pair is invented in Python + rather than CSS, so the AA gate has to reach it too.""" + for name, (fill, _stroke, text) in BOOK_FAMILIES.items(): + ratio = _ratio(text, fill) + self.assertGreaterEqual(round(ratio, 2), MIN_RATIO, + f"diagram family {name} is {ratio:.2f}") + + def test_tertiary_ink_is_not_reused_for_decorative_rules_only(self): + """--ink-3 carries figure captions, folios and colophon copy, so it + must clear AA on the page background in both modes.""" + self.assertGreaterEqual(round(_ratio(LIGHT["--ink-3"], LIGHT["--paper"]), 2), + MIN_RATIO) + self.assertGreaterEqual(round(_ratio(DARK["--ink-3"], DARK["--paper"]), 2), + MIN_RATIO) + + def test_gold_is_reserved_for_ornament_not_small_copy(self): + self.assertLess(_ratio(LIGHT["--bronze-2"], LIGHT["--paper"]), MIN_RATIO, + "bronze-2 is intentionally decorative") + for selector in (".toc-n", ".reg-no", ".rk-n", ".fig-no", ".cc-src-t", + ".pd-no", ".page-ref", ".fig-cap"): + self.assertNotIn("bronze-2", _rule(selector), + f"{selector} sets small copy in gold") + + +class Typography(unittest.TestCase): + def test_serif_for_prose_sans_for_furniture(self): + self.assertIn("font:400 17px/1.8 var(--serif)", CSS) + self.assertIn("h1,h2,h3,h4{font-family:var(--sans)", CSS) + + def test_cjk_prose_is_justified_with_strict_line_breaking(self): + self.assertIn(".chapter-body p,.chapter-body li{text-align:justify}", CSS) + self.assertIn("line-break:strict", CSS) + self.assertIn("hyphens:auto", CSS) + + def test_a_standalone_block_keeps_the_alignment_its_container_set(self): + # the epigraph is a right-aligned block; the prose rule used to win on + # its paragraphs, so a two-line English quote justified a river under a + # short last line instead of hanging flush right like the rest of it. + self.assertIn(".chapter-body blockquote p{text-align:inherit}", CSS) + + def test_a_source_chip_shows_its_whole_label(self): + # chips were capped with an ellipsis, which cut + # "Building Effective Agents(Anthropic 工程博客,2024-12-19)" mid-date; + # the chip row already wraps, so the label may simply get taller. + chip = _rule(".source-chip") + self.assertTrue(chip, "no .source-chip rule") + self.assertNotIn("text-overflow", chip) + self.assertNotIn("white-space:nowrap", chip) + + def test_no_web_font_is_downloaded(self): + self.assertNotIn("@font-face", CSS) + self.assertNotIn("@import", CSS) + for url in re.findall(r"url\(([^)]*)\)", CSS): + self.assertNotIn("http", url, url) + + def test_every_component_the_audit_counts_has_a_style(self): + for cls in ("chapter", "unit", "appendix", "compass", "compass-card", + "callout", "callout-tip", "callout-warning", "callout-info", + "callout-concept", "diagram", "diagram-frame", "table-scroll", + "codehilite", "page-ref", "xref", "source-chip", "rail", + "part-divider", "front-page", "colophon", "toc-group", + "fig-hint"): + self.assertIn(cls, CSS, cls) + + +class ColumnWidths(unittest.TestCase): + """Two widths, one column: sentences stay at the reading measure while + plates, tables and code take the sheet. A build that quietly drops either + half turns into a wall of 100-character lines or a page of thumbnails.""" + + @staticmethod + def _var(name: str) -> float: + m = re.search(rf"--{name}:(\d+(?:\.\d+)?)rem", CSS) + assert m, f"--{name} is not a rem value" + return float(m.group(1)) * 16 + + @staticmethod + def _capped_selectors() -> list[str]: + block = re.search(r"^\.chapter-body>p,", CSS, re.M) + assert block, "the prose cap rule is missing" + head = CSS[block.start(): CSS.index("{", block.start())] + return [s.strip() for s in head.split(",")] + + def test_the_measure_is_a_reading_column_and_the_furniture_is_wider(self): + measure, furniture = self._var("measure"), self._var("furniture") + self.assertGreaterEqual(measure, 40 * 16, "a 40em column is too narrow") + self.assertLessEqual(measure, 46 * 16, "sentences run past a comfortable line") + self.assertGreater(furniture, measure, "the furniture column is redundant") + self.assertLessEqual(furniture, 66 * 16, "code lines would need scrolling") + + def test_a_wide_sheet_gives_the_furniture_its_own_width(self): + block = CSS[CSS.index("@media (min-width:1360px)"):] + self.assertIn(".sheet{max-width:var(--furniture)}", block) + + def test_prose_blocks_are_capped_and_centred(self): + body = CSS[CSS.index(".chapter-body>p,.chapter-body>ul"):] + body = body[: body.index("}") + 1] + self.assertIn("max-width:var(--measure)", body) + # centred, because a column pinned to the left of a wide sheet leaves a + # dead gutter the reader reads as a broken page + self.assertIn("margin-inline:auto", body) + for sel in (".chapter-body>p", ".chapter-body .callout", ".chapter-notes"): + self.assertIn(sel, self._capped_selectors(), sel) + + def test_the_chapter_furniture_keeps_the_sheet_width(self): + # an inset lead band sitting over full-width compass cards reads as a + # misaligned page, not as a hierarchy + capped = " ".join(self._capped_selectors()) + for cls in (".lead-band", ".new-terms", ".compass"): + self.assertNotIn(cls, capped, f"{cls} should span the sheet") + + def test_the_wide_blocks_are_left_out_of_the_cap(self): + capped = " ".join(self._capped_selectors()) + for cls in ("figure.diagram", ".table-scroll", ".codehilite"): + self.assertNotIn(cls, capped, f"{cls} should use the full sheet width") + + def test_latin_gets_a_shorter_measure_than_cjk(self): + """46rem of Chinese is ~43 characters; the same box in 17px Palatino is + a 90-character line, which is the classic 'no one can track the return' + failure. English overrides the token instead of the font size.""" + m = re.search(r"body\.lang-en\{--measure:(\d+(?:\.\d+)?)rem", CSS) + self.assertTrue(m, "the English column does not set its own measure") + en = float(m.group(1)) * 16 + self.assertLess(en, self._var("measure"), "English is not narrower") + # ~8.2px per character at 17px Palatino; 80 characters is already long + self.assertLessEqual(round(en / 8.2), 80) + + def test_the_gloss_bubble_is_demoted_before_it_can_hang_off_screen(self): + """A centred bubble only fits beside the column if the screen can hold + the whole measure plus the bubble on the side the term sits at. The + worst case is a term at the very end of the 46rem column, so the float + becomes a bottom caption below measure + bubble width -- a 720px screen + used to clip 100px of it and make the document scroll sideways.""" + bubble = re.search( + r"\.gloss-pop\{[^}]*max-width:min\((\d+(?:\.\d+)?)rem", CSS) + assert bubble, "the gloss bubble has no rem cap" + cap = re.search( + r"@media \(max-width:([\d.]+)rem\)\s*\{\s*\.gloss-pop\{" + r"position:fixed", CSS) + assert cap, "the bubble is never demoted to a caption" + self.assertGreaterEqual( + float(cap.group(1)) * 16, + self._var("measure") + float(bubble.group(1)) * 16, + "a term at the end of the measure overflows the viewport") + # pinned to the viewport rather than to the term: an absolutely placed + # caption would be written off the bottom of the page the reader left + demo = CSS[cap.start():] + demo = demo[: demo.index("}") + 1] + self.assertIn("left:50%", demo) + self.assertIn("92vw", demo) + + def test_a_bubble_inside_a_scrolling_sheet_is_pinned(self): + # a table scrolls sideways and clips everything painted inside it, so a + # floating bubble whose term sits near the sheet edge hovers into empty + # space; at 1360px 13 of the book's tables hid their bubbles outright + sheet = re.search(r"\.table-scroll \.gloss-pop\{([^}]*)\}", CSS) + assert sheet, "table bubbles still hang inside the box that clips them" + self.assertIn("position:fixed", sheet.group(1)) + cap = re.search(r"@media \(max-width:([\d.]+)rem\)\s*\{\s*\.gloss-pop\{" + r"([^}]*)\}", CSS) + assert cap, "the narrow-screen caption rule moved" + # one caption, two triggers: if the two drift apart the bubble jumps as + # the reader resizes the window + self.assertEqual(" ".join(cap.group(2).split()), + " ".join(sheet.group(1).split())) + + def test_the_cap_stands_down_on_paper(self): + # @page margins are 17mm each side of 210mm; rem is the 16px root, so a + # printed measure wider than the page box means paper is unaffected + box_px = (210 - 2 * 17) / 25.4 * 96 + m = re.search(r"@page\{size:A4;margin:21mm (\d+)mm", CSS) + self.assertTrue(m, "@page margins moved out from under this test") + print_block = CSS[CSS.index("@media print{"):] + pm = re.search(r"body,body\.lang-en\{--measure:(\d+(?:\.\d+)?)rem", + print_block) + self.assertTrue(pm, "paper does not release the reading-column cap") + self.assertGreaterEqual(float(pm.group(1)) * 16, box_px) + for name in ("--measure", "--furniture"): + self.assertLess(self._var(name[2:]), box_px * 1.6, + f"{name} is wider than a page for no reason") + + +class PrintContract(unittest.TestCase): + def test_print_block_exists_with_a4_geometry(self): + self.assertIn("@media print{", CSS) + self.assertIn("@page{size:A4", CSS) + + def test_units_and_parts_start_new_pages(self): + self.assertIn("article.chapter,article.unit{", CSS) + self.assertIn("break-before:page", CSS) + self.assertIn("section.front-page{break-before:page}", CSS) + self.assertIn(".part-divider", CSS) + + def test_orphan_and_widow_control(self): + self.assertIn("p{orphans:2;widows:2}", CSS) + self.assertIn("h1,h2,h3{break-after:avoid}", CSS) + self.assertIn(".toc-group{break-inside:avoid}", CSS) + self.assertIn("figure.diagram", CSS) + # a registry row is a bordered card, so a page break may not cut it + self.assertRegex(CSS, r"\.reg-list li\{[^}]*break-inside:avoid") + + def test_rail_disappears_on_paper_and_narrow_screens_use_the_column(self): + self.assertIn("nav.rail,.no-print{display:none!important}", CSS) + self.assertRegex(CSS, r"@media \(min-width:1360px\)") + + def test_the_wide_screen_rail_rule_outranks_its_own_default(self): + """`nav.rail{display:none}` and the media query's `nav.rail{display:block}` + are equally specific, so whichever is written last wins. With the query + first, every screen 1360px and up reserved a 17.5rem gutter and left it + empty — a rail the reader never sees.""" + hidden = CSS.index("nav.rail{display:none") + shown = CSS.index("nav.rail{display:block}") + self.assertLess(hidden, shown) + + def test_an_index_row_never_loses_its_leader(self): + """A long identifier used to squeeze the dotted leader down to a stub. + The term breaks anywhere rather than eat the dots, so every entry still + reads as term — leader — page.""" + self.assertRegex(CSS, r"\.idx-term\{[^}]*overflow-wrap:anywhere") + self.assertRegex(CSS, r"\.idx-row \.leader\{[^}]*min-width") + + +class RunningHeads(unittest.TestCase): + def test_every_bookmarked_section_opens_its_own_head(self): + """The PDF pass reads its section map out of the bookmarks. Part ids + were once filtered out, which left each divider page wearing the head + of the chapter that ended before it.""" + import build + from yuyu_book.core import load_configs + manifest, _ = load_configs() + outline = [(1, p["no"]["zh"] + " · " + p["title"]["zh"], p["id"]) + for p in manifest["parts"]] + outline.append((2, "第 1 章", "ch01")) + meta = build.pdf_meta(manifest, outline, [], "zh") + self.assertEqual([a for a, _label in meta["heads"]], + [a for _lvl, _t, a in outline]) + self.assertEqual(meta["lang"], "zh") + self.assertTrue(meta["title"]) + + def test_the_screen_floor_for_wide_plates_is_dropped_on_paper(self): + # render.py pins a min-width onto plates too wide to read at column + # width; on A4 that must give back the whole page width or the + # overflow-x frame would clip the drawing. + print_block = CSS[CSS.index("@media print{"):] + self.assertIn("min-width:0!important", print_block) + self.assertIn(".fig-hint{display:none}", print_block) + # ...and the cap is a per-plate variable, so a tall plate can stand up + self.assertIn("max-height:var(--plate-cap,150mm)", print_block) + + def test_a_wide_plate_gets_a_landscape_sheet_of_its_own(self): + print_block = CSS[CSS.index("@media print{"):] + self.assertIn("@page plate-landscape{size:A4 landscape", print_block) + rule = re.search(r"figure\.landscape\{[^}]*\}", print_block).group(0) + self.assertIn("break-before:page", rule) + # the named page already ends the sheet; asking for a break afterwards + # left one empty landscape page behind every fold-out + self.assertNotIn("break-after", rule) + + def test_a_capped_plate_is_printed_without_its_screen_frame(self): + # the height cap makes a plate narrower than the column, so a bordered + # frame would print as a wide empty box around a small drawing. The box + # stands down on paper; it may NOT shrink-to-fit instead, because every + # plate root is width="100%" and a fit-content frame collapses it. + print_block = CSS[CSS.index("@media print{"):] + self.assertIn(".diagram-frame{background:none;border:0", print_block) + self.assertNotIn("fit-content", print_block) + self.assertIn(".diagram-frame::before,.diagram-frame::after{display:none}", + print_block) + + def test_part_and_unit_openers_own_their_whole_sheet(self): + # a part plate taller than the text column drops its last chapter onto + # the next sheet as a page of one line. + print_block = CSS[CSS.index("@media print{"):] + self.assertIn(".part-divider{padding:26mm 0 24mm}", print_block) + self.assertIn(".tier-legend{break-inside:avoid", print_block) + self.assertIn(".tl-list{display:grid;grid-template-columns:repeat(2", + print_block) + self.assertIn(".route-parts li{padding:.3rem 0}", print_block) + + def test_wide_tables_wrap_on_paper_instead_of_losing_a_column(self): + # .table-scroll is a screen device; on paper its overflow-x would + # silently cut the last column of the appendix flag/glossary tables. + print_block = CSS[CSS.index("@media print{"):] + self.assertIn("th{white-space:normal;word-break:keep-all;overflow-wrap:break-word}", + print_block) + self.assertIn("table td{overflow-wrap:break-word}", print_block) + # `anywhere` feeds min-content width, which is exactly what let the + # layout squeeze a column below the width of one word. + self.assertNotIn("overflow-wrap:anywhere", print_block) + self.assertRegex(CSS, r"@media \(max-width:860px\)\{\.compass") + + def test_a_cut_off_table_advertises_the_rest_on_screen(self): + # a phone paints no scrollbar until the reader drags it, so an appendix + # table running 493px past a 390px screen read as a complete table with + # one narrow last column. Naming the bar keeps it in layout. + at = CSS.index(".table-scroll{overflow-x:auto") + base = CSS[at: CSS.index("}", at)] + self.assertIn("scrollbar-width:thin", base) + self.assertIn("scrollbar-color:", base) + self.assertRegex(CSS, r"\.table-scroll::-webkit-scrollbar\{height:\d+px\}") + # `tab-index` is not a CSS property at all; the focus ring under it only + # works because render.py writes a real tabindex on the wrapper + self.assertNotIn("tab-index", CSS) + # code plates get the same stop, and their ring has to sit inside the + # frame because the frame is what clips it + self.assertRegex(CSS, r"\.codehilite pre:focus-visible\{outline:2px solid " + r"var\(--bronze-2\);outline-offset:-2px") + + def test_the_plate_hint_wraps_instead_of_leaving_the_page_on_phones(self): + # the hint chip is nowrap and the caption is a centered flex row, so on + # a 380px screen the row overflows sideways unless it may wrap. + self.assertIn("@media (max-width:44rem){figcaption{flex-wrap:wrap", CSS) + + def test_the_printed_cover_frame_is_the_page_not_a_card_at_the_top(self): + print_block = CSS[CSS.index("@media print{"):] + self.assertIn(".front-page.cover{min-height:250mm", print_block) + self.assertIn(".cover-inner{margin:0;width:100%", print_block) + + def test_the_printed_subject_index_keeps_two_columns(self): + print_block = CSS[CSS.index("@media print{"):] + self.assertIn(".idx-cols{columns:2", print_block) + + def test_a_unit_closes_on_the_page_it_ends_on(self): + print_block = CSS[CSS.index("@media print{"):] + # break-before:avoid is what the ruts used to carry, and Chromium does + # not implement it -- the keep has to sit on the wrapper instead + self.assertIn(".ch-close{break-inside:avoid}", print_block) + self.assertRegex(print_block, r"\.ch-rut\{[^}]*margin-top:[0-9.]+rem") + self.assertIn(".chapter-body .ch-close", + CSS[:CSS.index("@media print{")]) + + def test_no_unit_padding_spills_onto_a_sheet_of_its_own(self): + print_block = CSS[CSS.index("@media print{"):] + # 2.2rem of screen padding under a line that ends 15pt from the box edge + # buys an empty page: Chromium keeps the sheet to paint the padding, and + # the next unit's forced break makes it permanent. + self.assertIn("article.chapter,article.unit{padding-bottom:0}", + print_block) + + def test_pdf_bookmarks_are_driven_by_css_not_injected_markers(self): + self.assertIn("bookmark-level:1", CSS) + self.assertIn("bookmark-label:content()", CSS) + self.assertIn("bookmark-level:none", CSS) + + +class Deliverables(unittest.TestCase): + def test_requirements_pin_the_tools_the_build_needs(self): + text = (BOOK_DIR / "requirements.txt").read_text(encoding="utf-8") + for pkg in ("Markdown", "Pygments", "playwright", "PyMuPDF", + "Pillow", "fonttools"): + self.assertIn(pkg, text, pkg) + + def test_package_json_pins_the_mermaid_version_used(self): + pkg = (REPO_ROOT / "package.json").read_text(encoding="utf-8") + self.assertRegex(pkg, r'"mermaid"\s*:\s*"[^"]+"') + + def test_repository_docs_exist(self): + for name in ("CREDITS.md", "STRUCTURE.md", "CONTRIBUTING.md", "LICENSE"): + self.assertTrue((REPO_ROOT / name).exists(), name) + self.assertGreater((REPO_ROOT / name).stat().st_size, 200, name) + + +class EndToEndNoBrowser(unittest.TestCase): + """The whole pipeline, with diagrams degraded to source listings: this is + the assertion that a machine without Edge still produces a legal book.""" + + def test_check_mode_passes_for_both_languages(self): + proc = subprocess.run( + [sys.executable, str(BOOK_DIR / "build.py"), "--check", + "--mermaid=source"], + cwd=str(REPO_ROOT), capture_output=True, text=True, encoding="utf-8") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertIn("CHECK OK", proc.stdout) + self.assertIn("20 units", proc.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/yuyu-book/tests/test_crossrefs.py b/yuyu-book/tests/test_crossrefs.py new file mode 100644 index 00000000..be640a13 --- /dev/null +++ b/yuyu-book/tests/test_crossrefs.py @@ -0,0 +1,117 @@ +"""Cross-reference autolinking: 第N章 / Chapter N become clickable.""" +import re +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from yuyu_book import render # noqa: E402 +from yuyu_book.core import (REPO_ROOT, expected_ids, load_configs, load_unit, # noqa: E402 + unit_stream) + +MAP = {i: i for i in expected_ids(load_configs()[0])} + + +def link(text, current="ch01", lang="zh"): + return render.RefLinker(MAP, current, lang).transform(text) + + +class ChineseReferences(unittest.TestCase): + def test_plain_mention_becomes_a_link(self): + out = link("

这一机制在第 3 章展开。

") + self.assertIn('第 3 章', out) + + def test_space_variants_all_match(self): + for text in ("第3章", "第 3 章", "第 3 章"): + self.assertIn('href="#ch03"', link(f"

{text}

"), text) + + def test_appendix_letters_match_too(self): + self.assertIn('href="#apxd"', link("

详见附录 D。

")) + + def test_only_one_link_per_target_per_text_run(self): + out = link("

第 4 章讲过,第 4 章还讲过。

") + self.assertEqual(out.count('href="#ch04"'), 1) + + def test_a_mention_of_the_current_chapter_is_not_linked(self): + out = link("

本章与第 1 章不同。

", current="ch01") + self.assertNotIn("xref", out) + + def test_chapters_outside_the_book_are_left_alone(self): + for text in ("第 16 章", "第 0 章"): + self.assertNotIn("xref", link(f"

{text}

")) + + def test_code_and_pre_subtrees_are_never_rewritten(self): + doc = ('
第 3 章

读 第 3 章 与' + '第 4 章。

') + out = link(doc) + self.assertIn("
第 3 章
", out) + self.assertIn("第 3 章", out) + self.assertIn("第 4 章", out) + + def test_existing_anchors_and_svg_are_skipped(self): + doc = ('第 3 章

第 5 章

' + '第 6 章') + out = link(doc) + self.assertIn('第 3 章', out) + self.assertIn("第 6 章", out) + self.assertIn('href="#ch05"', out) + + def test_nesting_recovers_from_a_closed_skip_tag(self): + doc = "

x第 3 章

" + self.assertIn('href="#ch03"', link(doc)) + + +class EnglishReferences(unittest.TestCase): + def test_chapter_and_appendix_mentions(self): + self.assertIn('href="#ch12"', link("

see Chapter 12

", lang="en")) + self.assertIn('href="#apxa"', link("

Appendix A lists it

", lang="en")) + + def test_plurals_and_prose_without_a_number_stay_plain(self): + for text in ("Chapters 3 and 4", "the chapter explains"): + self.assertNotIn("xref", link(f"

{text}

", lang="en"), text) + + def test_word_boundaries_prevent_midword_matches(self): + self.assertNotIn("xref", link("

NonChapter 3 material

", lang="en")) + + +class WholeBookCrossReferences(unittest.TestCase): + @classmethod + def setUpClass(cls): + manifest, _sources = load_configs() + cls.html = {} + for lang in ("zh", "en"): + # render every unit the way the book does, so the assertion is + # about real prose rather than a fixture + parts = [] + for unit_def, kind, _part in unit_stream(manifest): + u = load_unit(unit_def, kind, lang, REPO_ROOT) + diagrams = {b.uid: ("图", "") for b in u.blocks} + parts.append(render.render_unit(u, MAP, diagrams=diagrams)["html"]) + cls.html[lang] = "\n".join(parts) + cls.manifest = manifest + + def test_every_generated_href_resolves_to_a_real_unit(self): + for lang, doc in self.html.items(): + for href in re.findall(r'") for b in u.blocks} + off = render.render_unit(u, MAP, diagrams=diagrams, autolink=False)["html"] + on = render.render_unit(u, MAP, diagrams=diagrams)["html"] + self.assertLess(off.count('class="xref"'), on.count('class="xref"')) + + +if __name__ == "__main__": + unittest.main() diff --git a/yuyu-book/tests/test_manifest.py b/yuyu-book/tests/test_manifest.py new file mode 100644 index 00000000..779ce6f3 --- /dev/null +++ b/yuyu-book/tests/test_manifest.py @@ -0,0 +1,274 @@ +"""The shipped config must describe the book that is actually on disk. + +Everything structural (book order, anchors, compass text, source anchors) is +derived from manifest.json; this module locks the manifest to the repository: +paths exist, nothing on disk is unregistered, ids stay in part order, and +validate_configs accepts the real files (and rejects the classic mistakes). +""" +import copy +import re +import sys +import unittest +from pathlib import Path + +if not __package__: # direct run: see tests/__init__.py for the real bootstrap + sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from yuyu_book.core import (GUIDE_FIELDS, REPO_ROOT, BuildError, expected_ids, + load_configs, load_unit, unit_stream, + validate_configs) + +ZH_DIR_RX = re.compile(r"^(第[一二三四五六七八九十]+部分-.+|附录)$") +EN_DIR_RX = re.compile(r"^(Part-\d-.+|Appendices)$") + + +def disk_markdown(lang: str) -> set[str]: + """Every book markdown file on disk for one language. + + The builder has no discovery of its own (the manifest is the registry), so + this walks the part/appendix directories — plus the foreword — and returns + repo-relative posix paths. A new part directory is picked up here, which is + exactly how an unregistered chapter would slip into the book. + """ + if lang == "zh": + dirs = [p for p in REPO_ROOT.iterdir() + if p.is_dir() and ZH_DIR_RX.match(p.name)] + roots = [(p, p) for p in dirs] + foreword = [p for p in REPO_ROOT.glob("00-*.md")] + else: + en = REPO_ROOT / "en" + dirs = [p for p in en.iterdir() if p.is_dir() and EN_DIR_RX.match(p.name)] + roots = [(en, p) for p in dirs] + foreword = list(en.glob("00-*.md")) + found = {p.relative_to(REPO_ROOT).as_posix() for _r, d in roots + for p in d.glob("*.md")} + found |= {p.relative_to(REPO_ROOT).as_posix() for p in foreword} + return found + + +class ShippedConfigIsSelfConsistent(unittest.TestCase): + @classmethod + def setUpClass(cls): + # load_configs() runs validate_configs() and raises on any drift, so + # reaching the tests at all is the "real files are accepted" proof. + cls.manifest, cls.sources = load_configs() + + def test_validate_configs_accepts_the_shipped_files(self): + validate_configs(self.manifest, self.sources) + self.assertEqual(self.manifest["front"]["id"], "front") + self.assertGreaterEqual(len(self.sources), 30) + + def test_every_manifest_path_exists_on_disk_in_both_languages(self): + for unit_def, _kind, _part in unit_stream(self.manifest): + for lang in ("zh", "en"): + rel = unit_def[lang] + path = REPO_ROOT / rel + self.assertTrue(path.is_file(), f"{unit_def['id']}.{lang}: {rel}") + self.assertTrue(path.read_text(encoding="utf-8").lstrip(), + f"{unit_def['id']}.{lang}: empty file {rel}") + + def test_no_markdown_on_disk_is_unregistered(self): + registered = {u[lang] for lang in ("zh", "en") + for u, _k, _p in unit_stream(self.manifest)} + for lang in ("zh", "en"): + on_disk = disk_markdown(lang) + self.assertTrue(on_disk, f"no {lang} book markdown found") + self.assertEqual(sorted(on_disk), sorted(registered & on_disk), + f"unregistered {lang} markdown") + + def test_expected_ids_follow_part_order(self): + m = self.manifest + manual = ([m["front"]["id"]] + + [u["id"] for p in m["parts"] for u in p["units"]] + + [a["id"] for a in m["appendices"]]) + self.assertEqual(expected_ids(m), manual) + + def test_book_order_is_foreword_then_15_chapters_then_four_appendices(self): + ids = expected_ids(self.manifest) + self.assertEqual(len(ids), 20) + self.assertEqual(ids[0], "front") + self.assertEqual(ids[1:16], [f"ch{n:02d}" for n in range(1, 16)]) + self.assertEqual(ids[16:], ["apxa", "apxb", "apxc", "apxd"]) + + def test_unit_stream_matches_expected_ids_and_kinds(self): + stream = [(u["id"], k) for u, k, _p in unit_stream(self.manifest)] + self.assertEqual([i for i, _k in stream], expected_ids(self.manifest)) + kinds = {i: k for i, k in stream} + self.assertEqual(kinds["front"], "front") + self.assertEqual(kinds["ch07"], "chapter") + self.assertEqual(kinds["apxd"], "appendix") + + def test_parts_cover_every_chapter_and_no_appendix(self): + apx = {a["id"] for a in self.manifest["appendices"]} + for part in self.manifest["parts"]: + self.assertTrue(part["units"]) + for unit_def in part["units"]: + self.assertNotIn(unit_def["id"], apx) + self.assertRegex(unit_def["id"], r"^ch\d{2}$") + + def test_every_unit_has_a_non_empty_compass_in_both_languages(self): + guides = self.manifest["guides"] + self.assertEqual(sorted(guides), sorted(expected_ids(self.manifest))) + for uid in expected_ids(self.manifest): + for field in GUIDE_FIELDS: + entry = guides[uid].get(field) + self.assertIsInstance(entry, dict, f"{uid}.{field}") + for lang in ("zh", "en"): + self.assertTrue((entry.get(lang) or "").strip(), + f"{uid}.{field}.{lang} is empty") + + def test_compass_fields_are_single_sentences_not_paragraphs(self): + # the compass card is a fixed-width sidebar; the real data tops out at + # 163 chars (apxb.deliverable), so 200 is headroom without prose + for uid, guide in self.manifest["guides"].items(): + for field in GUIDE_FIELDS: + for lang in ("zh", "en"): + text = guide[field][lang] + self.assertNotIn("\n", text, f"{uid}.{field}.{lang}") + self.assertLessEqual(len(text), 200, f"{uid}.{field}.{lang}") + + def test_every_guide_source_id_resolves_in_sources_json(self): + for uid, guide in self.manifest["guides"].items(): + self.assertTrue(guide.get("sources"), f"{uid} has no source anchors") + for sid in guide["sources"]: + self.assertIn(sid, self.sources, f"{uid} -> unknown source {sid}") + + def test_part_titles_mottos_and_goals_exist_in_both_languages(self): + for part in self.manifest["parts"]: + for field in ("no", "title", "motto", "goal"): + for lang in ("zh", "en"): + self.assertTrue((part[field][lang] or "").strip(), + f"{part['id']}.{field}.{lang} is empty") + + def test_top_level_book_metadata_is_present(self): + for key in ("title", "subtitle", "epigraph"): + for lang in ("zh", "en"): + self.assertTrue(self.manifest[key][lang].strip(), key) + self.assertTrue(self.manifest["license"]["url"].startswith("https://")) + self.assertRegex(self.manifest["version"], r"^\d+\.\d+\.\d+$") + self.assertRegex(self.manifest["date"], r"^\d{4}-\d{2}-\d{2}$") + + +class DerivedUnitLabels(unittest.TestCase): + """The ids/labels the TOC, PDF bookmarks and folios are built from.""" + + @classmethod + def setUpClass(cls): + cls.manifest, _ = load_configs() + cls.by_id = {u["id"]: u for u, _k, _p in unit_stream(cls.manifest)} + + def unit(self, uid, lang): + kind = ("front" if uid == "front" + else "appendix" if uid.startswith("apx") else "chapter") + return load_unit(self.by_id[uid], kind, lang, REPO_ROOT) + + def test_zh_display_uses_the_ideographic_space(self): + u = self.unit("ch02", "zh") + self.assertEqual(u.no, "第 2 章") + self.assertEqual(u.display, f"第 2 章 {u.title}") + self.assertEqual(u.eyebrow, "第 2 章") + + def test_en_display_uses_a_colon(self): + u = self.unit("ch02", "en") + self.assertEqual(u.no, "Chapter 2") + self.assertEqual(u.display, f"Chapter 2: {u.title}") + + def test_no_plain_is_a_two_digit_gutter_label(self): + self.assertEqual(self.unit("ch01", "zh").no_plain, "01") + self.assertEqual(self.unit("ch15", "zh").no_plain, "15") + self.assertEqual(self.unit("ch09", "en").no_plain, "09") + + def test_no_plain_for_appendices_is_the_letter(self): + self.assertEqual(self.unit("apxa", "zh").no_plain, "A") + self.assertEqual(self.unit("apxd", "en").no_plain, "D") + + def test_foreword_has_no_number_and_a_localised_eyebrow(self): + zh = self.unit("front", "zh") + en = self.unit("front", "en") + self.assertEqual(zh.no, "") + self.assertEqual(zh.display, zh.title) + self.assertEqual(zh.eyebrow, "前言") + self.assertEqual(en.eyebrow, "Foreword") + + def test_anchor_is_the_unit_id_for_every_unit(self): + for uid in ("front", "ch07", "apxc"): + for lang in ("zh", "en"): + self.assertEqual(self.unit(uid, lang).anchor, uid) + + def test_unit_id_is_unique_across_the_book(self): + ids = expected_ids(self.manifest) + self.assertEqual(len(ids), len(set(ids))) + + +class ConfigValidationRejects(unittest.TestCase): + """Negative cases over an in-memory copy of the real config.""" + + @classmethod + def setUpClass(cls): + cls.manifest, cls.sources = load_configs() + + def check(self, manifest, sources=None, message=""): + with self.assertRaises(BuildError) as ctx: + validate_configs(manifest, sources or self.sources) + self.assertIn(message, str(ctx.exception)) + + def test_missing_guide_for_a_unit(self): + m = copy.deepcopy(self.manifest) + del m["guides"]["ch07"] + self.check(m, message="guides missing for: ch07") + + def test_guide_for_a_unit_that_does_not_exist(self): + m = copy.deepcopy(self.manifest) + m["guides"]["ch99"] = copy.deepcopy(m["guides"]["ch07"]) + self.check(m, message="guides for unknown units: ch99") + + def test_guide_pointing_at_an_unknown_source_id(self): + m = copy.deepcopy(self.manifest) + m["guides"]["ch07"]["sources"] = ["does-not-exist"] + self.check(m, message="unknown source id 'does-not-exist'") + + def test_unit_file_missing_from_disk(self): + m = copy.deepcopy(self.manifest) + m["parts"][1]["units"][2]["en"] = "en/Part-2-Core-Systems/99-Ghost.md" + self.check(m, message="unit ch07: missing en file") + + def test_empty_compass_field_in_one_language(self): + m = copy.deepcopy(self.manifest) + m["guides"]["ch07"]["deliverable"]["en"] = " " + self.check(m, message="guide ch07.deliverable.en is empty") + + def test_duplicate_unit_ids(self): + m = copy.deepcopy(self.manifest) + m["parts"][0]["units"].append(copy.deepcopy(m["appendices"][0])) + self.check(m, message="duplicate unit ids") + + def test_source_missing_a_required_field(self): + s = copy.deepcopy(self.sources) + del s["cc-changelog"]["license"] + self.check(self.manifest, sources=s, + message="source cc-changelog: field license missing/empty") + + def test_source_url_that_is_not_https(self): + s = copy.deepcopy(self.sources) + s["cc-changelog"]["url"] = "http://github.com/anthropics/claude-code" + self.check(self.manifest, sources=s, message="source cc-changelog: url must be https") + + def test_github_source_without_a_pin(self): + s = copy.deepcopy(self.sources) + s["gh-book-repo"]["pinned"] = {} + self.check(self.manifest, sources=s, + message="source gh-book-repo: github refs must pin tag or commit") + + def test_community_source_claiming_normative_status(self): + s = copy.deepcopy(self.sources) + s["ld-topic-1870593"]["normative"] = True + self.check(self.manifest, sources=s, + message="source ld-topic-1870593: community refs need") + + def test_the_unmutated_copy_still_passes(self): + validate_configs(copy.deepcopy(self.manifest), + copy.deepcopy(self.sources)) + + +if __name__ == "__main__": + unittest.main() diff --git a/yuyu-book/tests/test_mermaid.py b/yuyu-book/tests/test_mermaid.py new file mode 100644 index 00000000..956ae94a --- /dev/null +++ b/yuyu-book/tests/test_mermaid.py @@ -0,0 +1,472 @@ +"""The fence-aware scanner behind all 314 diagram plates (157 per language). + +Markdown is the read-only source of truth, so the only thing that may cut a +document into structure is a real (unfenced) line. extract_mermaid also has to +leave the document balanced: a closing fence re-emitted after the placeholder +unmatched every following fence and swallowed whole chapters into a
, so
+"no fence residue, one placeholder per block, placeholder alone on its line"
+are pinned here together with the caption/cache/fallback contracts.
+"""
+import re
+import sys
+import unittest
+from pathlib import Path
+
+if not __package__:  # direct run: see tests/__init__.py for the real bootstrap
+    sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from yuyu_book import mermaid as mm
+from yuyu_book import render
+from yuyu_book.core import (REPO_ROOT, BuildError, Unit, expected_ids,
+                            extract_mermaid, load_configs, load_unit,
+                            normalize_h1, read_text, unit_stream)
+
+PLACEHOLDER = re.compile(r"@@MM:[0-9a-f]{12}@@")
+FENCE_LINE = re.compile(r"^\s*`{3,}")
+
+CH11 = "第三部分-高级模式篇/11-技能系统与插件架构.md"
+APXB = "附录/B-工具完整清单.md"
+
+
+def pipeline(text, lang="zh", file="t.md"):
+    """What load_unit does to one file, exposed for inline fixtures."""
+    _no, _title, body = normalize_h1(text, lang, file)
+    return extract_mermaid(body, file)
+
+
+class FenceAwareExtraction(unittest.TestCase):
+    def test_hash_inside_a_fence_is_never_document_structure(self):
+        text = ("# 第 1 章 标题\n\n正文。\n\n"
+                "```bash\n# 这是注释\n# 也是注释\nls\n```\n\n尾部。\n")
+        no, title, _body = normalize_h1(text, "zh", "t.md")
+        self.assertEqual((no, title), ("第 1 章", "标题"))
+        body, blocks, langs = pipeline(text)
+        self.assertEqual(blocks, [])
+        self.assertEqual(langs, ["bash"])
+        self.assertIn("# 这是注释", body)
+        self.assertIsNone(PLACEHOLDER.search(body))
+
+    def test_a_fence_never_opens_a_heading_for_the_scanner(self):
+        # strip the real H1: the four `# ` lines in ch11 are all inside fenced
+        # samples, so a fence-aware scanner must find NO heading at all
+        text = read_text(REPO_ROOT / CH11)
+        body = "\n".join(text.split("\n")[1:])
+        with self.assertRaises(BuildError) as ctx:
+            normalize_h1(body, "zh", CH11)
+        self.assertIn("no top-level H1", str(ctx.exception))
+
+    def test_chapter_11_regression_keeps_its_four_fenced_comments(self):
+        unit_def, kind = next((u, k) for u, k, _p in unit_stream(load_configs()[0])
+                              if u["id"] == "ch11")
+        u = load_unit(unit_def, kind, "zh", REPO_ROOT)
+        self.assertEqual(u.no, "第 11 章")
+        self.assertEqual(u.title, "技能系统与插件架构")
+        self.assertEqual(len(u.blocks), 7)
+        for comment in ("# 技能正文", "# API 端点生成器", "# SKILL.md", "# 技能内容"):
+            self.assertIn(f"{comment}\n", u.md, comment)
+
+    def test_longer_outer_fence_contains_a_shorter_inner_fence(self):
+        text = ("# 第 1 章 标题\n\n示例:\n\n"
+                "````markdown\n```mermaid\ngraph LR\nA-->B\n```\n````\n\n"
+                "真图:\n\n```mermaid\nA-->B\n```\n")
+        body, blocks, langs = pipeline(text)
+        self.assertEqual(langs[0], "markdown")          # the example is code
+        self.assertEqual(len(blocks), 1)                # only the real diagram
+        self.assertEqual(body.count("```mermaid"), 1)   # the example stays literal
+        self.assertIn("graph LR", body)
+
+    def test_tilde_fence_and_backtick_fence_do_not_cross_close(self):
+        text = ("# 第 1 章 标题\n\n~~~\n```not-a-fence\n~~~\n\n"
+                "```mermaid\nA-->B\n```\n")
+        body, blocks, langs = pipeline(text)
+        self.assertEqual(len(blocks), 1)
+        self.assertEqual(langs, [""])
+        self.assertIn("```not-a-fence", body)
+
+    def test_unclosed_fence_raises_and_names_the_file(self):
+        with self.assertRaises(BuildError) as ctx:
+            pipeline("# 第 1 章 标题\n\n```mermaid\nA-->B\n")
+        self.assertIn("t.md: unclosed code fence at line", str(ctx.exception))
+
+    def test_unclosed_plain_fence_names_the_line_it_opened_on(self):
+        with self.assertRaises(BuildError) as ctx:
+            pipeline("# 第 1 章 标题\n\n```python\nx = 1\n")
+        self.assertIn("unclosed code fence at line 3", str(ctx.exception))
+
+    def test_block_line_numbers_are_lines_of_the_markdown_file(self):
+        _body, blocks, _langs = pipeline("# 第 1 章 标题\n\n```mermaid\nA-->B\n```\n")
+        self.assertEqual(blocks[0].line, 3)
+
+    def test_two_blocks_get_distinct_stable_uids(self):
+        text = ("# 第 1 章 标题\n\n```mermaid\nA-->B\n```\n\n"
+                "```mermaid\nA-->B\n```\n")
+        _body, first, _l = pipeline(text)
+        body2, second, _l = pipeline(text)
+        self.assertEqual([b.uid for b in first], [b.uid for b in second])
+        self.assertEqual(len({b.uid for b in first}), 2)   # same src, two places
+        self.assertEqual(len({b.src for b in first}), 1)
+        self.assertEqual(first[0].file, "t.md")
+        self.assertEqual([b.line for b in first], [3, 7])
+
+    def test_uid_moves_with_the_diagram_but_the_svg_cache_does_not(self):
+        same_src = mm.MermaidBlock("x", "graph LR\nA-->B", "ch01.md", 10)
+        moved = mm.MermaidBlock("y", "graph LR\nA-->B", "ch02.md", 99)
+        self.assertNotEqual(same_src.uid, moved.uid)
+        self.assertEqual(mm._cache_key(same_src.src, "11.17.2", "edge"),
+                         mm._cache_key(moved.src, "11.17.2", "edge"))
+
+    def test_placeholder_always_stands_alone_on_its_own_line(self):
+        text = "# 第 1 章 标题\n\n段落紧贴图:\n```mermaid\nA-->B\n```\n紧跟的段落。\n"
+        body, blocks, _langs = pipeline(text)
+        uid = blocks[0].uid
+        self.assertIn(f"\n\n@@MM:{uid}@@\n\n", body)
+        for line in body.split("\n"):
+            if "@@MM:" in line:
+                self.assertEqual(line.strip(), f"@@MM:{uid}@@")
+
+    def test_no_closing_fence_is_re_emitted_after_the_placeholder(self):
+        # the catastrophic case: an extra ``` after the placeholder re-opened a
+        # fence and swallowed every following paragraph into a 
+        text = ("# 第 1 章 标题\n\n```mermaid\nA-->B\n```\n\n"
+                "之后的正文必须还是段落。\n\n## 下一节\n\n又一段。\n")
+        body, blocks, _langs = pipeline(text)
+        self.assertEqual(len(blocks), 1)
+        self.assertNotIn("`", body)
+        self.assertEqual(FENCE_LINE.findall(body), [])
+        self.assertIn("之后的正文必须还是段落。", body)
+
+    def test_code_langs_are_the_info_strings_in_document_order(self):
+        text = ("# 第 1 章 标题\n\n```python\n1\n```\n\n"
+                "```\nbare\n```\n\n```mermaid\nA-->B\n```\n\n"
+                "```typescript title=x\n1\n```\n")
+        _body, blocks, langs = pipeline(text)
+        self.assertEqual(langs, ["python", "", "typescript"])
+        self.assertEqual(len(blocks), 1)
+
+
+class Captions(unittest.TestCase):
+    def captions(self, text, file="t.md"):
+        _no, _t, body = normalize_h1(text, "zh", file)
+        _body, blocks, _langs = extract_mermaid(body, file)
+        # build.py passes the file as shipped; block.line is a line of it
+        return mm.captions_for(text, blocks), blocks
+
+    def test_caption_is_the_nearest_preceding_h2(self):
+        caps, blocks = self.captions(
+            "# 第 1 章 标题\n\n## 第一节\n\n```mermaid\nA-->B\n```\n\n"
+            "## 第二节\n")
+        self.assertEqual(caps[blocks[0].uid], "第一节")
+
+    def test_h3_is_not_a_caption_source(self):
+        caps, blocks = self.captions(
+            "# 第 1 章 标题\n\n## 大节\n\n### 小节\n\n```mermaid\nA-->B\n```\n")
+        self.assertEqual(caps[blocks[0].uid], "大节")
+
+    def test_heading_inside_a_fence_does_not_become_a_caption(self):
+        caps, blocks = self.captions(
+            "# 第 1 章 标题\n\n## 真标题\n\n```bash\n## 假标题\n```\n\n"
+            "```mermaid\nA-->B\n```\n")
+        self.assertEqual(caps[blocks[0].uid], "真标题")
+
+    def test_each_block_in_a_unit_gets_its_own_section_caption(self):
+        caps, blocks = self.captions(
+            "# 第 1 章 标题\n\n## A 节\n\n```mermaid\nA-->B\n```\n\n"
+            "## B 节\n\n```mermaid\nC-->D\n```\n")
+        self.assertEqual([caps[b.uid] for b in blocks], ["A 节", "B 节"])
+
+    def test_heading_directly_above_the_fence_is_the_caption(self):
+        caps, blocks = self.captions(
+            "# 第 1 章 标题\n\n## 上一节\n\n正文。\n## 应有的标题\n"
+            "```mermaid\nA-->B\n```\n")
+        self.assertEqual(caps[blocks[0].uid], "应有的标题")
+
+    def test_diagram_with_no_preceding_heading_uses_the_neutral_fallback(self):
+        caps, blocks = self.captions(
+            "# 附录 B:清单\n\n前言一段。\n\n```mermaid\nA-->B\n```\n\n"
+            "## 最后一节\n")
+        self.assertEqual(caps[blocks[0].uid], "架构示意")
+
+    def test_shipped_appendix_b_first_plate_uses_the_neutral_fallback(self):
+        # observed in the real book (zh + en 附录 B): the first plate sits
+        # above every H2, so it must not inherit the document's last heading
+        unit_def, kind = next((u, k) for u, k, _p in unit_stream(load_configs()[0])
+                              if u["id"] == "apxb")
+        u = load_unit(unit_def, "appendix", "zh", REPO_ROOT)
+        caps = mm.captions_for(read_text(REPO_ROOT / APXB), u.blocks)
+        first = min(u.blocks, key=lambda b: b.line)
+        self.assertEqual(caps[first.uid], "架构示意")
+        self.assertEqual(first.line, 14)
+
+
+class CacheKeyAndFallback(unittest.TestCase):
+    def setUp(self):
+        self.src = "graph LR\nA-->B"
+        self.base = mm._cache_key(self.src, "11.17.2", "edge-140")
+
+    def test_key_is_a_svg_path_inside_the_mermaid_cache_dir(self):
+        self.assertEqual(self.base.parent, mm.MM_DIR)
+        self.assertEqual(self.base.suffix, ".svg")
+        self.assertRegex(self.base.stem, r"^[0-9a-f]{24}$")
+
+    def test_key_changes_when_the_source_changes(self):
+        self.assertNotEqual(self.base,
+                            mm._cache_key("graph LR\nA-->C", "11.17.2", "edge-140"))
+
+    def test_key_changes_when_the_mermaid_version_changes(self):
+        self.assertNotEqual(self.base,
+                            mm._cache_key(self.src, "11.18.0", "edge-140"))
+
+    def test_key_changes_when_the_browser_version_changes(self):
+        self.assertNotEqual(self.base,
+                            mm._cache_key(self.src, "11.17.2", "edge-141"))
+
+    def test_key_is_stable(self):
+        self.assertEqual(self.base, mm._cache_key(self.src, "11.17.2", "edge-140"))
+
+    def test_key_changes_when_the_book_palette_changes(self):
+        """A cached SVG is a picture of one palette. Editing a token must not
+        keep serving the picture drawn under the old one."""
+        original = mm._STYLE_VER
+        try:
+            mm._STYLE_VER = "deadbeef00"
+            self.assertNotEqual(self.base,
+                                mm._cache_key(self.src, "11.17.2", "edge-140"))
+        finally:
+            mm._STYLE_VER = original
+
+    def test_style_version_fingerprints_both_theme_and_families(self):
+        self.assertEqual(mm._style_fingerprint(), mm._STYLE_VER)
+        saved = mm.BOOK_FAMILIES["info"]
+        try:
+            mm.BOOK_FAMILIES["info"] = ("#000001", "#000002", "#000003")
+            self.assertNotEqual(mm._style_fingerprint(), mm._STYLE_VER)
+        finally:
+            mm.BOOK_FAMILIES["info"] = saved
+        saved_theme = mm.THEME["themeVariables"]["primaryColor"]
+        try:
+            mm.THEME["themeVariables"]["primaryColor"] = "#000004"
+            self.assertNotEqual(mm._style_fingerprint(), mm._STYLE_VER)
+        finally:
+            mm.THEME["themeVariables"]["primaryColor"] = saved_theme
+
+    def test_source_fallback_escapes_the_original_source(self):
+        block = mm.MermaidBlock("a" * 12, 'graph LR\nA -->|x'))
+        self.assertTrue(html.endswith("
")) + self.assertIn("A -->|x<y & "q"| B", html) + self.assertNotIn("B", "ch01.md", i) + for i in range(5)] + out = mm.source_fallback(blocks, {}) + self.assertEqual(sorted(out), sorted(b.uid for b in blocks)) + self.assertEqual(out["000000000000"][0], "") # caption-less is fine + + +class AuthorColoursReInked(unittest.TestCase): + """The ~900 classDef/style hexes the manuscript brings to the 314 plates. + + Mermaid's ``theme: base`` only reaches nodes the author left alone, so an + untreated Material-blue box sits in the middle of a bronze-and-parchment + book and reads as a screenshot from another product. These tests pin the + build-time re-inking: role preserved, palette replaced, and nothing outside + a directive line touched. + """ + + def reink(self, decls: str) -> str: + return mm.reink_colors(f"graph LR\nclassDef x {decls}").splitlines()[1] + + def test_hue_decides_the_family_not_the_exact_hex(self): + cases = { + "fill:#eff6ff,stroke:#3b82f6,color:#1e3a8a": mm.BOOK_FAMILIES["info"], + "fill:#e8f4f8,stroke:#2196F3,color:#1565C0": mm.BOOK_FAMILIES["info"], + "fill:#d4edda,stroke:#27ae60,color:#1b5e20": mm.BOOK_FAMILIES["good"], + "fill:#fff3cd,stroke:#f39c12,color:#664d03": mm.BOOK_FAMILIES["warn"], + "fill:#f8d7da,stroke:#c0392b,color:#842029": mm.BOOK_FAMILIES["danger"], + "fill:#f3e5f5,stroke:#7b1fa2,color:#4a148c": mm.BOOK_FAMILIES["plum"], + } + for decls, (fill, stroke, text) in cases.items(): + with self.subTest(decls=decls): + out = self.reink(decls) + self.assertIn(f"fill:{fill}", out) + self.assertIn(f"stroke:{stroke}", out) + self.assertIn(f"color:{text}", out) + + def test_the_fill_decides_the_family_when_the_stroke_disagrees(self): + """A pale-blue box with a purple border is a blue box; the border is + the weaker signal and both halves still have to come from one family.""" + out = self.reink("fill:#e3f2fd,stroke:#7b1fa2,color:#4a148c") + fill, stroke, text = mm.BOOK_FAMILIES["info"] + self.assertEqual(out, f"classDef x fill:{fill},stroke:{stroke}," + f"color:{text}") + + def test_a_diagram_that_distinguishes_two_groups_still_does(self): + block = ("classDef core fill:#4a90d9,stroke:#2c5f8a,color:#fff\n" + "classDef tool fill:#8fbc8f,stroke:#5a8a5a,color:#fff") + out = mm.reink_colors(block) + self.assertIn(mm.BOOK_FAMILIES["info"][0], out) + self.assertIn(mm.BOOK_FAMILIES["good"][0], out) + + def test_a_mid_tone_fill_becomes_its_hue_tint_not_a_dark_box(self): + """The manuscript's darkest fills are #1abc9c / #27ae60; they read as + teal and green here, and the white labels they carried stay legible.""" + for hexc, family in (("#27ae60", "good"), ("#1abc9c", "good"), + ("#0d47a1", "info")): + with self.subTest(hexc=hexc): + fill, _, text = mm.BOOK_FAMILIES[family] + out = self.reink(f"fill:{hexc},stroke:#333,color:#fff") + self.assertIn(f"fill:{fill}", out) + self.assertIn(f"color:{text}", out) + + def test_properties_without_a_colour_are_left_alone(self): + out = self.reink("fill:#2196F3,stroke:#333,stroke-width:2px," + "font-weight:bold") + self.assertIn("stroke-width:2px", out) + self.assertIn("font-weight:bold", out) + + def test_only_directive_lines_are_rewritten(self): + src = ("sequenceDiagram\n" + " Keychain-->>Server: hit #1 (32ms) fill:#000\n" + ' A["style: keep this fill:#e8f4f8 text"]\n' + " style A fill:#2196F3,stroke:#333") + out = mm.reink_colors(src) + self.assertIn("hit #1 (32ms) fill:#000", out) + self.assertIn('A["style: keep this fill:#e8f4f8 text"]', out) + self.assertIn(f"style A fill:{mm.BOOK_FAMILIES['info'][0]}", out) + + def test_reinking_is_idempotent(self): + src = ("classDef a fill:#27ae60,stroke:#1b5e20,color:#fff\n" + "style B fill:#ffcdd2,stroke:#d32f2f") + once = mm.reink_colors(src) + self.assertEqual(once, mm.reink_colors(once)) + + def test_no_shipped_directive_keeps_an_off_book_hex(self): + allowed = {c.lower() for fam in mm.BOOK_FAMILIES.values() for c in fam} + checked = 0 + for path in sorted(REPO_ROOT.glob("**/*.md")): + if "node_modules" in path.parts: + continue + for block in re.findall(r"```mermaid\n(.*?)```", + read_text(path), re.S): + for line in mm.reink_colors(block).splitlines(): + if not re.match(r"\s*(classDef|style)\s", line): + continue + for prop, hexc in re.findall( + r"\b(fill|stroke|color)\s*:\s*(#[0-9a-fA-F]{3,8})", + line): + with self.subTest(path=path.name, prop=prop): + self.assertIn(hexc.lower(), allowed) + checked += 1 + self.assertGreater(checked, 2000) + + +class FigureMarkup(unittest.TestCase): + def test_zh_and_en_caption_words_and_figure_id(self): + zh = render.mermaid_figure("ab" * 6, "", "7-1", "循环", "zh") + en = render.mermaid_figure("ab" * 6, "", "7-1", "loop", "en") + self.assertIn('
', zh) + self.assertIn('图 7-1', zh) + self.assertIn('Figure 7-1', en) + self.assertIn('
', zh) + + def test_diagram_frame_holds_the_svg_and_no_block_element_is_nested(self): + html = render.mermaid_figure("a" * 12, '', + "1-1", "x", "zh") + self.assertEqual(html.count("", html) + + WIDE = ('') + NARROW = ('') + + def test_wide_plates_are_floored_and_advertise_the_scroll(self): + zh = render.mermaid_figure("w" * 12, self.WIDE, "C-15", "依赖关系", "zh") + self.assertIn("min-width:2600px;", zh) # capped, so the drag stays sane + self.assertIn('
可横向拖动', zh) + en = render.mermaid_figure("w" * 12, self.WIDE, "C-15", "deps", "en") + self.assertIn("drag sideways", en) + self.assertNotIn("可横向", en) + + def test_plates_that_already_fit_the_column_are_untouched(self): + for vw in ("795", "1100"): + svg = self.NARROW.replace("0 0 795 160", f"0 0 {vw} 160") + html = render.mermaid_figure("n" * 12, svg, "1-1", "x", "zh") + self.assertNotIn("min-width", html) + self.assertNotIn("fig-hint", html) + self.assertIn('
' + '') + out, floor = render.floor_wide_svg(svg) + self.assertEqual(floor, "1700px") + self.assertIn('', out) + self.assertIn("", out) + self.assertIn('', out) + + +class DiagramsSurviveRendering(unittest.TestCase): + """End-to-end for one synthetic unit: placeholder -> figure, no
."""
+
+    def render(self, text, lang="zh", uid="ch01", kind="chapter"):
+        _no, _t, body = normalize_h1(text, lang, "t.md")
+        body, blocks, langs = extract_mermaid(body, "t.md")
+        caps = mm.captions_for(text, blocks)
+        unit = Unit(id=uid, kind=kind, lang=lang, no=_no, title=_t,
+                    md=body, blocks=blocks, code_langs=langs, path="t.md",
+                    anchor=uid)
+        mapping = {i: i for i in expected_ids(load_configs()[0])}
+        return render.render_unit(unit, mapping,
+                                  diagrams=mm.source_fallback(blocks, caps))
+
+    def test_prose_after_a_diagram_is_not_swallowed_by_a_pre(self):
+        payload = self.render(
+            "# 第 1 章 标题\n\n```mermaid\ngraph LR\nA-->B\n```\n\n"
+            "这段是普通段落,不该在 pre 里。\n")
+        html = payload["html"]
+        self.assertEqual(payload["n_diagrams"], 1)
+        self.assertNotIn("@@MM:", html)
+        self.assertNotIn("```", html)
+        self.assertIn("

这段是普通段落,不该在 pre 里。

", html) + pre = re.search(r"]*>(.*?)
", html, re.S) + self.assertIn("graph LR", pre.group(1)) # the mermaid source + self.assertNotIn("普通段落", pre.group(1)) + + def test_missing_diagram_is_marked_not_dropped(self): + payload = self.render("# 第 1 章 标题\n\n```mermaid\nA-->B\n```\n") + unit = payload["unit"] + html = render.render_unit(unit, {}, diagrams={})["html"] + self.assertIn('class="mermaid-missing"', html) + self.assertNotIn("@@MM:", html) + + def test_figures_are_numbered_by_chapter_and_run(self): + payload = self.render( + "# 第 7 章 上下文\n\n```mermaid\nA-->B\n```\n\n```mermaid\nC-->D\n```\n") + self.assertEqual(payload["n_diagrams"], 2) + self.assertIn(">图 7-1<", payload["html"]) + self.assertIn(">图 7-2<", payload["html"]) + + def test_appendix_figures_are_numbered_with_the_letter(self): + payload = self.render("# 附录 A:地图\n\n```mermaid\nA-->B\n```\n", + uid="apxa", kind="appendix") + self.assertIn(">图 A1<", payload["html"]) + + def test_diagram_uids_do_not_collide_across_the_two_languages(self): + manifest, _sources = load_configs() + seen = {} + for lang in ("zh", "en"): + for unit_def, kind, _part in unit_stream(manifest): + u = load_unit(unit_def, kind, lang, REPO_ROOT) + for b in u.blocks: + seen.setdefault(b.uid, set()).add((lang, u.id)) + self.assertEqual(sum(1 for v in seen.values() if len(v) > 1), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/yuyu-book/tests/test_pages.py b/yuyu-book/tests/test_pages.py new file mode 100644 index 00000000..a05437d0 --- /dev/null +++ b/yuyu-book/tests/test_pages.py @@ -0,0 +1,435 @@ +"""Structure pages: cover, imprint, compass, contents, registry, index, shell.""" +import re +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from yuyu_book import pages, render # noqa: E402 +from yuyu_book.core import (BOOK_DIR, REPO_ROOT, expected_ids, load_configs, # noqa: E402 + load_json, load_unit, unit_stream) +from yuyu_book.style import CSS, ink_mark, page, trace, wheel # noqa: E402 + + +class Config(unittest.TestCase): + """Shared fixtures. It must be a TestCase itself: a plain mixin silently + drops every subclass out of discovery, and the pages went untested.""" + @classmethod + def setUpClass(cls): + cls.manifest, cls.sources = load_configs() + cls.units = {u["id"]: load_unit(u, k, "zh", REPO_ROOT) + for u, k, _p in unit_stream(cls.manifest)} + + +class Ornaments(Config): + def test_each_wheel_gets_its_own_spoke_id(self): + """The wheel is inlined 10+ times per book; shared ids would break + every instance after the first.""" + a, b = wheel(24), wheel(24) + ida = re.search(r'id="(spoke\d+)"', a).group(1) + idb = re.search(r'id="(spoke\d+)"', b).group(1) + self.assertNotEqual(ida, idb) + self.assertIn(f'href="#{ida}"', a) + self.assertNotIn('href="#spoke"', a + b) + + def test_wheel_is_self_coloured_and_accessible(self): + self.assertIn('aria-hidden="true"', wheel()) + self.assertIn("currentColor", wheel()) + + +class HandInked(unittest.TestCase): + """The drawn marks and the chapter wire. Both are inlined dozens of times, + so they answer to the same rules as a file in illustrations/.""" + + KINDS = ("tip", "warning", "info", "concept", "note") + + def test_every_callout_kind_is_inked(self): + from yuyu_book.render import CALLOUT_LABELS + self.assertEqual(set(CALLOUT_LABELS.values()) - set(self.KINDS), set()) + for kind in self.KINDS: + self.assertIn("currentColor", ink_mark(kind), kind) + + def test_nothing_the_builder_inlines_carries_an_id_or_a_reach_outside(self): + marks = [ink_mark(k) for k in self.KINDS] + [trace("ch01"), trace("apxb")] + for text in marks: + for banned in ("id=", "href=", "url(", "a


b

") + self.assertIn('class="hr-rule"', out) + self.assertIn("currentColor", out) + self.assertNotIn("", 1)[0]) + + def test_the_road_has_a_ring_for_every_chapter_and_a_dot_for_the_siding(self): + chapters = sum(len(p["units"]) for p in self.manifest["parts"]) + self.assertEqual(self.svg.count('r="3.6"'), chapters, chapters) + self.assertEqual(self.svg.count('r="2.7"'), + len(self.manifest["appendices"])) + + def test_the_wheel_at_the_head_carries_the_foreword_number(self): + self.assertEqual(self.svg.count(f'>{pages.FRONT_NO}'), 1) + self.assertEqual(self.svg.count(f'>{self.nos["ch01"]}'), 1) + + def test_the_map_is_the_same_map_every_build(self): + again = pages.route_map_page(self.manifest, "zh", self.nos) + self.assertEqual(again, self.html) + + def test_the_list_under_the_map_links_every_leg(self): + want = [self.manifest["front"]["id"]] + \ + [p["id"] for p in self.manifest["parts"]] + \ + [self.manifest["appendices"][0]["id"]] + got = re.findall(r'
  • ", html), "no bullet in revision list") + + def test_revision_notes_are_typeset_not_left_in_markdown(self): + """The notes quote the builder's own names; a row of raw backticks + around one reads as a typo in a book that sets code spans properly.""" + entries = load_json(BOOK_DIR / "revisions.json")["entries"] + for lang in ("zh", "en"): + html = pages.revisions_page(entries, lang) + self.assertNotIn("`", html, "a backtick reached the page") + self.assertIn("pdf_meta()", html) + self.assertNotIn("", html) + + def test_a_note_naming_a_long_flag_still_fits_a_narrow_screen(self): + """Front matter never runs the unit pipeline, so the break points have + to be put in over the assembled page: `getFeatureValue_CACHED_MAY_BE_STALE` + in a revision note pushed the document 37px past a 390px screen.""" + entries = load_json(BOOK_DIR / "revisions.json")["entries"] + for lang in ("zh", "en"): + out = render.soften_long_names(pages.revisions_page(entries, lang)) + self.assertIn("", out, "no break point in a long name") + self.assertNotIn("", out) + self.assertIn("getFeatureValue_CACHED_MAY_" + "BE_STALE", out) + + def test_reading_routes_link_to_the_first_chapter_of_each_part(self): + html = pages.how_to_read(self.manifest, "zh") + self.assertIn('id="how-to-read"', html) + for part in self.manifest["parts"]: + self.assertIn(f'href="#{part["units"][0]["id"]}"', html) + self.assertEqual(html.count("route-card"), len(self.manifest["start_reading"])) + + def test_part_divider_lists_its_own_chapters(self): + part = self.manifest["parts"][1] + rows = [(u["id"], u["id"][-2:], u["id"].upper()) for u in part["units"]] + html = pages.part_divider(part, "zh", 2, rows) + self.assertEqual(f'id="{part["id"]}"', re.search(r'id="(p\d)"', html).group(0)) + for anchor, no, label in rows: + self.assertIn(f'href="#{anchor}"', html) + self.assertIn(f'data-no="{no}">{label}', html) + self.assertIn("Ⅱ", pages._ROMAN[2]) + + def test_part_divider_draws_the_road_before_its_stops(self): + """The strip above the list is the part's itinerary: one ring for every + chapter the divider then names, so the count is not decoration.""" + part = self.manifest["parts"][1] + rows = [(u["id"], u["id"][-2:], u["id"].upper()) for u in part["units"]] + html = pages.part_divider(part, "zh", 2, rows) + self.assertEqual(html.count('r="3.3"'), len(rows)) + self.assertLess(html.index("pd-route"), html.index("pd-list")) + + +class Compass(Config): + def test_three_cards_and_cited_source_chips(self): + guide = self.manifest["guides"]["ch07"] + html = pages.compass(self.units["ch07"], guide, self.sources, "zh") + for key in ("question", "deliverable", "boundary"): + self.assertIn(f'compass-card {key}', html) + self.assertIn(guide[key]["zh"], html) + self.assertEqual(html.count("source-chip"), len(guide["sources"])) + for sid in guide["sources"]: + self.assertIn(f'href="#source-{sid}"', html) + + def test_english_compass_uses_english_copy(self): + guide = self.manifest["guides"]["ch01"] + html = pages.compass(self.units["ch01"], guide, self.sources, "en") + self.assertIn(guide["question"]["en"], html) + self.assertNotIn(guide["question"]["zh"], html) + + def test_chips_point_at_real_registry_entries(self): + registry = pages.source_registry(self.sources, "zh") + for uid, guide in self.manifest["guides"].items(): + for sid in guide.get("sources", []): + self.assertIn(f'id="source-{sid}"', registry, uid) + + +class Contents(Config): + def _entries(self): + entries = [{"kind": "row", "anchor": "front", "no": "00", "title": "前言"}] + for i, part in enumerate(self.manifest["parts"], 1): + entries.append({"kind": "part", "anchor": part["id"], + "no": pages._ROMAN[i], "title": part["title"]["zh"]}) + for u in part["units"]: + unit = self.units[u["id"]] + entries.append({"kind": "row", "anchor": u["id"], + "no": unit.no_plain, "title": unit.title}) + entries.append({"kind": "hr"}) + for apx in self.manifest["appendices"]: + unit = self.units[apx["id"]] + entries.append({"kind": "row", "anchor": apx["id"], + "no": unit.no_plain, "title": unit.title}) + return entries + + def test_every_entry_gets_a_page_ref_placeholder(self): + html = pages.toc_html(self._entries(), "zh") + for e in self._entries(): + if e["kind"] == "hr": + continue + self.assertIn(f'data-ref="{e["anchor"]}"', html, e["anchor"]) + self.assertIn('class="page-ref', html) + + def test_parts_and_their_rows_group_so_a_page_break_cannot_strand_a_heading(self): + html = pages.toc_html(self._entries(), "zh") + groups = re.findall(r'

    (.*?)
    \s*(?=
    |)', + html, re.S) + self.assertGreaterEqual(len(groups), 5) + for group in groups: + parts = re.findall(r'class="toc-part"', group) + self.assertLessEqual(len(parts), 1, "one heading per group") + + def test_appendix_gutter_labels_are_letters_in_both_languages(self): + for apx in self.manifest["appendices"]: + for lang in ("zh", "en"): + u = load_unit(apx, "appendix", lang, REPO_ROOT) + self.assertEqual(u.no_plain, apx["id"][-1].upper(), + f"{apx['id']} {lang}: {u.no_plain}") + + def test_chapter_gutter_labels_are_zero_padded_numbers(self): + for part in self.manifest["parts"]: + for u in part["units"]: + unit = self.units[u["id"]] + self.assertRegex(unit.no_plain, r"^\d{2}$", u["id"]) + + +class BackMatter(Config): + def test_registry_groups_by_kind_and_counts_every_source(self): + html = pages.source_registry(self.sources, "zh") + self.assertIn('id="sources"', html) + for sid in self.sources: + self.assertIn(f'id="source-{sid}"', html) + self.assertEqual(html.count('class="reg-list"'), + len({s["kind"] for s in self.sources.values()})) + + def test_registry_urls_are_https_and_rel_noopener(self): + html = pages.source_registry(self.sources, "en") + for href in re.findall(r'([^<]*(?:[^<]*)*)', + html) + self.assertIsNotNone(row, "no underscore term reached the index") + self.assertIn("_", html) + self.assertNotIn("", html) + self.assertNotIn("<wbr>", html, "the break tag got escaped away") + + def test_index_entries_are_sorted_and_lettered(self): + entries = load_json(BOOK_DIR / "subject-index.json") + letters = [e["letter"] for e in entries] + self.assertEqual(letters, sorted(letters)) + for e in entries[:20]: + self.assertTrue(e["zh"]["refs"] or e["en"]["refs"]) + self.assertTrue(e["key"].strip()) + + +class Shell(unittest.TestCase): + def test_page_shell_is_js_free_and_offline(self): + html = page("zh", "御舆", CSS, "lang-zh", "

    x

    ", "", "") + self.assertIn('', html) + self.assertIn(" + + +{rail} +
    +{content} +{colophon} +
    + +""" From 50c5622654f24c4e8eab6f12ca01af87d9f31c6a Mon Sep 17 00:00:00 2001 From: zhuguang-ZFG <168892777+zhuguang-ZFG@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:24:01 +0800 Subject: [PATCH 02/21] docs: record where the book's claims and drawings come from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CONTRIBUTING.md | 201 ++++++++++++++++++++++++++++ CREDITS.md | 122 +++++++++++++++++ LICENSE | 339 ++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 42 ++++++ README.md | 29 ++++- STRUCTURE.md | 200 ++++++++++++++++++++++++++++ en/README.md | 29 ++++- 7 files changed, 958 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 CREDITS.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 STRUCTURE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..16826d9b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,201 @@ +# 参与修订 + +先读 [STRUCTURE.md](STRUCTURE.md) 了解仓库的两层契约:**Markdown 书稿是唯一内容源,构建器只读不写**;锚点、目录、图号、脚注、书签、页码都在构建时派生。本文讲环境、命令、改动流程和门禁。 + +## 1. 环境 + +```bash +# Node 22+(package.json 的 engines 要求 >=22),用于 mermaid 解析与图板渲染 +npm ci # 安装 mermaid 11.17.2 + jsdom 26.1.0 + +# Python 侧依赖(Markdown / Pygments / playwright / PyMuPDF / fonttools / Pillow) +python -m pip install -r yuyu-book/requirements.txt +``` + +渲染 Mermaid 图版和打印 PDF 需要一个 Chromium 系浏览器。构建器按 **msedge → chrome → Playwright 自带 chromium** 的顺序自动探测,装了前两者之一就直接可用;都没有时: + +```bash +python -m playwright install chromium # 注意是 Python 包提供的 CLI,不是 npx +``` + +也可以显式指定:`--browser "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"`。**一个坑**:走自动探测分支时构建器会带上 `--allow-file-access-from-files`,而用 `--browser` 指定可执行文件时不会带;如果图板报"harness failed to load",先去掉 `--browser` 让它自动探测。只想跑通构建而不渲染图版,可以用 `--mermaid source`(完全不启动浏览器,图源以代码块排版)。 + +## 2. 常用命令 + +```bash +python yuyu-book/build.py --check # 装配两语版并跑审计,输出进临时目录,不落 dist/、不出 PDF +python yuyu-book/build.py --lang zh # 只出中文版 HTML 到 yuyu-book/dist/ +python yuyu-book/build.py --lang en --pdf # 英文版 HTML + PDF +python yuyu-book/build.py --regen-index # 从附录 D 重建 subject-index.json(随后仍会继续完整构建) +``` + +仓库根 `Makefile` 把常用组合包了一层: + +```bash +make # 列出可用目标 +make book # 双语 HTML +make book-pdf # 双语 HTML + PDF +make check # build.py --check + 两套单测 + scripts/check_book.py + npm run check:diagrams +make index # --regen-index +make clean # 删除 yuyu-book/dist +``` + +两个变量可以覆盖:`make PYTHON=python3`(Linux CI 用)、`make BOOK_LANG=zh`(只出一语;刻意不叫 `LANG`,那是 shell 已经在导出的 locale 变量)。 + +改动前后都建议跑一遍既有内容门: + +```bash +python scripts/check_book.py # 相对链接落点、围栏闭合、Mermaid 里的字面量 \n +npm run check:diagrams # 用锁定的 mermaid 解析器逐图 parse +python -m unittest discover -s tests # 仓库现有单测(star history 等) +python -m unittest discover -s yuyu-book/tests # 构建契约:manifest/来源/围栏与图题/交叉引用/审计/版式 WCAG +``` + +`yuyu-book/tests` 不需要浏览器、不渲染图、不写书稿;它把 `--check` 抓不到的东西钉死:附录 D 表格到索引的映射、`MermaidBlock.line` 必须仍是文件里的行号(图题靠它对位)、`第 N 章` 在 `/
    /` 子树里不许被加链、配色 token 的 WCAG 4.5:1 对比度。Windows 控制台若因中文断言报编码错误,加 `PYTHONIOENCODING=utf-8`。
    +
    +## 3. 新增或修改一章
    +
    +1. **写 Markdown**,中英文两份都要动。
    +2. **首行 H1 必须能被构建器识别**(`core.normalize_h1()` 的模板,围栏内不算):
    +   - 中文:`# 前言` / `# 第 2 章:对话循环 -- Agent 的心跳` / `# 附录 A:源码导航地图`
    +   - 英文:`# Foreword` / `# Chapter 2: The Dialog Loop -- The Agent's Heartbeat` / `# Appendix A: Architecture Navigation Map`
    +   - 冒号可省;正文里的 ` -- `(两侧空格)会被规整成破折号 `—`,连续空格会被压成一个。
    +   - 找不到顶层 H1,或第一个 H1 不匹配上面任一模板,直接 `BuildError`。
    +3. **在 `yuyu-book/manifest.json` 登记**:`parts[].units[]`(或 `appendices[]`)里加 `{id, zh, en}`;`id` 同时就是成品里的锚点(`#ch09`、`#apxd`),改 `id` 等于改全书链接目标。两个语言路径都必须存在,否则 `validate_configs()` 报错。
    +4. **补罗盘条目** `guides[]`:`question` / `deliverable` / `boundary` 三个字段,每个都要 **zh 与 en 双语非空**。校验要求 guides 恰好覆盖所有单元(多一个少一个都不行),`audit` 还要求成品里的罗盘卡数量不少于单元数。
    +5. **挂来源** `guides[].sources`:填 `sources.json` 里已有的 id,写错 id 会被 `unknown source id` 拒绝。这些 id 在书里渲染成"来源锚点"chip,指向 `#source-`。
    +6. **补通俗层**(可选但强烈建议):`guides[].plain` 是一句章首导读带,`guides[].analogy` 是紧跟其后的"打个比方"。两者都要 zh / en 双语,且必须同时给 `guides[].plain_evidence` —— 一组 `文件:行号` 指针,指回书稿里真正支撑这句话的行。指针落点不能在代码围栏内,那一行还必须含书里真实出现过的词形;导读带要求中英两侧都有证据。写不出证据的白话,就是还没读懂正文,先别写。
    +7. **画一枚章徽**:`yuyu-book/illustrations/.svg`(新篇再加 `part-<篇id>.svg`)。不画不会失败——构建器只统计"磁盘上有的图是否都上了页面"——但每个单元的标题行就空着一格。规格见第 5.2 节。
    +8. 顺手更新 `yuyu-book/revisions.json` 的版本说明(zh/en 各一份)。
    +
    +## 4. 新增一条来源(`yuyu-book/sources.json`)
    +
    +以来源 id 为键的字典条目。id 必须匹配 `^[a-z0-9][a-z0-9-]*$`。
    +
    +**必填非空字段**:`title`、`authority`、`kind`、`url`、`snapshot`、`accessed`、`license`、`scope`。
    +
    +**门禁**(`core.validate_configs()`,不满足即构建失败):
    +
    +- `url` 必须 `https://` 开头。
    +- `kind: github` → `pinned` 里必须有 `tag` 或 `commit` 之一。
    +- `kind: npm` → `pinned.version` 必填。
    +- `kind: community` → 必须 `normative: false` 且有 `accessed`;若给了 `pinned.commit`,必须是完整 40 位 SHA(长度校验目前只对 community 生效)。
    +
    +**约定**(构建器不检查,但请遵守):
    +
    +- GitHub 条目按现有做法写完整 40 位 SHA,而不是分支名——`main` 会移动,叙述就无法回溯。现有 6 条 github 条目全是完整 SHA。
    +- `snapshot` 写清读取时间与核对方式(例:"2026-10-25 读取该 commit 下的 CHANGELOG.md,HTTP 200,首条版本号 2.1.310");`accessed` 写日期;`scope` 写这条来源用来核对全书的哪类叙述。**只登记书目元数据,不转载来源正文**——这也是 `license` 字段一律写"链接引用,不转载"的原因。
    +- Anthropic 的源码、文档、博客与 changelog 一律链接引用,不复制进书稿或成品。
    +- 不要把 GPL 系许可的资产(图、字体、代码片段)打包进书的单文件成品:本书文字是 CC BY-NC-SA 4.0,非商业 + 相同方式共享的条款与 copyleft 强传染条款混装会两头都不干净。
    +
    +## 5. 书稿行文规范
    +
    +这些规则决定了成品能不能过审计、以及排版是否成立。
    +
    +**中文排版**
    +
    +- 全角标点,中英文之间留半角空格;术语首次出现给中英对照。
    +- 正文不使用 emoji。图注编号、罗盘卡与提示框的图形标记(✦ ⚠ ↗ ◎ ※)由渲染器注入,不需要也不应该在 Markdown 里手写。
    +- 表格里的排版符号(✓ ✗ ★ ☆)可以用。
    +
    +**代码围栏**
    +
    +- 必须闭合,未闭合会报 `unclosed code fence at line N`,并且审计会以"`
    `/`` 内嵌块级元素"再抓一次。
    +- **声明语言**:` ```json `、` ```typescript `、` ```bash ` 等。围栏 info 串会成为代码板右上角的语言角标(`data-lang`);不写语言的围栏拿不到角标,且按纯文本高亮。围栏数量与渲染出的代码块数量必须一一对应,否则报"rendered code blocks but N source fences"。
    +- Mermaid 围栏固定写 ` ```mermaid `。图题取自动态扫描的**最近上层 H2**,所以每张图前要有一个 H2 标题;没有就回落成"架构示意"。图内换行用 `
    `,写字面量 `\n` 会被 `scripts/check_book.py` 拦下。 +- **图的宽度**:Mermaid 按自己的坐标排版,正文栏只有 635px。viewBox 宽超过 1150 单位的图版会被 `floor_wide_svg()` 钉上 `min-width`(最宽 2600px),屏幕上横向拖动阅读、图题带"可横向拖动"提示,打印时归零回到整页可见。所以画超大依赖图前先想清楚:横向铺开 = 读者要拖,宁可拆成两张 H2 下的小图。 +- **图在纸面上的尺寸**:纸面没有滚动条,只有两块可用地——竖版版心 176×245mm(665×926 CSS px)与横版折页 170mm 高(993×642 CSS px)。构建时 `plate_cap()` 给"很高、按 6pt 字号底线缩放后正好还放得进栏宽"的图写一条 `--plate-cap`(150–245mm),让它独占版面而不是被压小——成品里中文版抬了 6 张、英文版 9 张;`plate_landscape()` 把"横过来才读得清"的图(竖着不足 6pt、横着能到 5pt 且好上一成半)挂上 `landscape` 类,独占一张 `@page plate-landscape` 横向 A4——中文版 15 张、英文版 22 张,一张图一张纸。折页**不写** `--plate-cap`:横版纸面只有 170mm 高,图版吃满 165mm 就会把图题挤到下一张纸上。两头都够不着的极端长条仍留在正文流里,纸面上就是小:中文版还剩 1 张(3592×1330 单位,约 2.5pt),英文版 3 张。**HTML 才是全保真产物**,这类图请引导读者在屏幕上横向拖开看。 +- **三个会把整本书的图打崩的写法**:别给 `.diagram-frame` 或图版容器加 `width:fit-content`——mermaid 的 SVG 根节点写的是 `width="100%"`,百分比宽度在 fit-content 父级里塌成 0,157 张图会集体消失(`test_build_contracts` 里有一条负向断言盯着它);别加 `filter:`——打印引擎会把滤镜子树光栅化,图里的字退化成像素(PDF 里选不中、搜不到),文件体积还能翻倍;也别在 `figure.landscape` 上写 `break-after:page`——命名页面本身就会结束这一张纸,两个都写会在每个折页后面多出一张空白。 +- **图的颜色**:优先不写颜色,`mermaid.py` 的 `THEME` 会把无主节点排成书色。已经写了 `classDef`/`style` 的图也不必回改正文——构建时 `reink_colors()` 按色相把 fill/stroke/color 重染进 `BOOK_FAMILIES` 七组书色(蓝→sky、绿→jade、橙黄→brass、红→cinnabar、紫→plum、近白→card、其余→neutral),角色区分保留,色板统一。写新图时按语义选色即可,别指望某个具体十六进制出现在成品里。 + +**引用与链接** + +- 跨章引用直接写"第 9 章""附录 B"(英文写 `Chapter 9` / `Appendix B`),构建时自动加链(覆盖第 1–15 章与附录 A–D);调试时可加 `--no-autolink`。 +- 不要在正文里写指向 `.md` 的相对链接——书稿之间的互指应通过"第 N 章"表达,否则审计报 `residual .md links`。 +- 不要手写锚点、图号、脚注编号:它们全部派生自 Markdown 标题与链接顺序。 +- 外链写成普通 Markdown 链接即可,构建器会自动加上标编号并在单元末尾生成"引用链接"清单。 + +**提示框(callout)** + +以 `> **标签:** 正文` 开头的引用块会被识别成对应色板的 callout,标签必须命中内置映射(括号说明会被剥掉后比较): + +| 标签 | 类型 | +|------|------| +| 最佳实践 / 正确做法 / 推荐做法 | tip | +| 反模式警告 / 危险 / 注意 | warning | +| 交叉引用 / 交叉引用提示 | info | +| 设计洞察 / 原理 | concept | +| best practice / correct approach | tip | +| anti-pattern warning / warning | warning | +| cross-reference / extended reflection / practical application scenarios | info | +| memories that should be saved | tip | +| memories that should not be saved | warning | +| design insight | concept | +| learning objectives / reference answer(s) / reference answer hints | note(硬编码兜底列表) | + +命中不了的标签会保持成普通引用块,不会报错——所以想真正出框,请照抄上表的标签写法。 + +**双语同步** + +`en/` 下的英文镜像与中文逐单元对应,章节标题模板不同但结构一致;只改中文不改英文,虽然不会立刻失败,但会让两语版的图数、脚注数、索引词条数对不上,评审时请一并说明差异。 + +### 5.1 通俗层:白话注、导读带、阅读路线 + +通俗层是构建时叠在书稿上的一层,**从不回写 Markdown**。三个落点: + +**术语白话注**(`yuyu-book/glosses.json`)一条一个术语:`term.{zh,en}`、`forms.{zh,en}`(正文里要匹配的表面写法,大小写与连字符照抄)、`plain.{zh,en}`(一句能懂的话)、`analogy`(可选比方)、`priority`、`evidence`。只有 `priority: "core"` 的词会在正文首现处画虚线;只想进章首面板的词写 `"extra"`。一个单元的面板最多 22 行,超了构建失败——那时把最通用的词降成 `extra`,而不是删掉解释。 + +**导读带与比方**在 `guides[].plain` / `.analogy`,配 `guides[].plain_evidence`。长度上限(超出即 `BuildError`):导读带 zh 60 字 / en 170 字符,比方 zh 52 / en 160;浮窗里的白话 zh 40 / en 130,比方 zh 44 / en 130。 + +**阅读路线**(`yuyu-book/reading-paths.json`)按单元存一个小节顺序的位置列表,取值 `start` / `core` / `deep`(成品里渲染成"起步 / 主干 / 进阶")。长度必须等于该单元实际渲染出的 `

    ` 数:加了小节忘了同步,构建会直接报错,而不是把徽盖到别的小节上。 + +整层可以 `--no-plain` 关掉,用来对比开合前后的成品。 + +**底线:通俗不能以说错为代价。** 每一句白话都要能在书稿里指出行号;`validate_plain()` 会逐条核对证据指针落在代码围栏之外、那一行确实含登记过的词形,core 词条与每条导读带要求中英两侧都有证据。写不出证据的白话,就是还没读懂正文——先别写。 + +### 5.2 插图:章徽、篇题刻与扉页车图 + +- 位置与命名:`yuyu-book/illustrations/.svg`(20 枚章徽)、`illustrations/part-.svg`(4 幅题刻)、`illustrations/cover.svg`(扉页那幅四马单辕古车,1000×470)。 +- 章徽是 64×64 的单线小图、描边 2.1;题刻是 200×140 的小版画、描边 1.5;车图铺满整个版面、按层次分档描线(轮与辕 6.5/6,马身 4,辐与细部 1.2–2)。屏幕上题刻约 216px 宽、章徽 74px,三者不该用同一档笔画粗细。 +- 硬约束(`art.py` 的装载器逐条把关,违反即构建失败):只用 `currentColor` 上色——一张图同时管屏幕、暗色模式与纸面,不需要第二份文件;不得出现 `id=`(同一份图会被内联进单文件几十次,一个 id 就是一次重复 id,审计会在离图很远的地方炸)、`href=`、`url(`、`