Per-subsystem generated cordis-surface regions
按子系统生成的 cordis-surface 区块
One subsystem's documentation was split across three homes: its hand-written subsystems page (introduction, data structures, verbs), its `ctx.<key>` slice of the flat generated `docs/cordis-catalog/services.md`, and its event scope's slice of the flat `docs/cordis-catalog/events.md`. A reader of shell.md had to open two more documents to see the service interface and events the page was describing, and nothing tied t
English
Problem
One subsystem's documentation was split across three homes: its hand-written subsystems page (introduction, data structures, verbs), its ctx.<key> slice of the flat generated docs/cordis-catalog/services.md, and its event scope's slice of the flat docs/cordis-catalog/events.md. A reader of shell.md had to open two more documents to see the service interface and events the page was describing, and nothing tied the three views together beyond hand-maintained links. The flat catalogs also sat outside the bilingual corpus (excluded from pairing because generated output is English-only), so the reference surface had no Chinese route at all.
The generated-catalog decision — generate from source, @mode tags cross-checked, fail-closed type-link coverage, the ts cordis-catalog fence — is not in question; what changed is WHERE the generated output lands.
Decision
gen-cordis-catalog.ts injects each subsystem's service and event reference INTO its own page, between <!-- BEGIN GENERATED cordis-surface … --> / <!-- END GENERATED cordis-surface --> markers, and the flat services/events catalogs are deleted. One page per subsystem now carries introduction, data structures, and the generated wiring surface.
- Curated fail-loud partition.
SERVICE_PAGEmaps every discoveredctx.<key>to exactly one page;EVENT_SCOPE_PAGEmaps every event scope. The generator hard-errors in both directions — an unmapped discovered service/scope, and a mapped key/scope the walk no longer discovers — so the partition cannot drift from the source surface. Independent AST scans of everydeclare module 'cordis'merge block underpackages/*/*/src/**backstop the projection's blind spots for services AND events: a declared Context key or Events member the projection cannot render must carry a namedSERVICE_WALK_EXEMPTIONS/EVENT_WALK_EXEMPTIONSreason, stale exemptions hard-error, and everything rendered must also be visible to the scan (events-backstop decision owns the scan contract); aTODO(cordis-catalog-interface-services)marks teaching the projection to render the interface-typed entries. - Byte-identical regions across the pair. The generator writes the SAME English region bytes into
foo.mdandfoo.zh.md, extending the existing rule that verbatim code fences match across a pair.verify-translation-pairinggained a dedicated region-identity check (partitionGeneratedRegionsintranslation-pairing.tsowns the marker grammar) that names a divergent or malformed region precisely; the whole-document structural signature still covers the region content a second time. - Guarded pair auto-record. A regeneration that changes region bytes would leave every touched pair out-of-sync, so the generator re-records a pair's
.i18n.yamlitself — but ONLY when the write is region-confined: both sides' recorded blob hashes must match the pre-write bytes, and the region-STRIPPED content must be unchanged on both sides. Human-prose drift leaves the record stale so the pairing gate still forces the normal translation flow; a brand-new pair is never auto-recorded (the author's reviewed--writeowns that). This keeps.i18n.yamlas plaingit hash-objectvalues — no stripped-hash semantics change. - The inherited tier moved, not died. The vendor
ctxmembers andinternal/*/loader/hmr/timer events render todocs/cordis-api/inherited.md, next to the relocated Cordis core API pages (docs/cordis-catalog/core/→docs/cordis-api/). Framework surface lives under a framework home; the harness pages stay repository-owned vocabulary. - In-page links. Signature
Types:lines link sibling pages (core.md,shell.md); a type whose primary page is the rendering page is dropped from the line instead of self-linking. Pages reference their own region with#cordis-surfaceor a#ctx<key>--<class>anchor — every generated heading is preceded by an explicit<a id>carrying the GitHub slug (the historical flat-catalog anchor), so the fragments resolve identically on GitHub and the VitePress site, whose own slugger treats the punctuation-heavy headings differently.
Alternatives considered
- Keep the flat catalogs alongside the regions, both generated — rejected: every JSDoc edit would produce double diff noise, and the scattering (one subsystem, three documents) this change exists to remove would survive.
- Generator-owned whole pages with hand-written intros in fragment files — rejected: the narrative prose is the majority of every existing page and belongs in the reviewed document itself; markers cost one grammar rule and keep authors editing the real file.
- Localized regions (generator emits Chinese too) — deferred, same status as the i18n README's long-standing note for the remaining generated docs: teaching the generator zh output means translating source JSDoc, which is machinery this change does not need. English regions inside zh pages match the existing status quo of English JSDoc inside verbatim fences.
- Hashing region-stripped content in
.i18n.yaml— rejected: the record would stop beinggit hash-objectof the file, breaking the recover-last-confirmed-text property and every consumer that recomputes hashes.
Consequences
- A subsystem's whole story is one page:
docs/subsystems/<name>.md(and its pair) carries introduction, data structures/verbs, and the generated service/event surface;docs/cordis-catalog/no longer exists. - A new service or event scope cannot ship undocumented or unmapped: the generator fails until
SERVICE_PAGE/EVENT_SCOPE_PAGEnames its owning page, and the page must already exist with markers in both language sides. - Regeneration after a source-JSDoc change touches the affected pages in both languages plus (when region-confined) their pair records — a mechanical, reviewable diff; prose edits keep demanding the translation flow because the auto-record guard refuses them.
- The website's subsystem nav lists every page (38 routes per locale: 35 translated pairs plus the three still-English-mirrored goal/terminal/commands), replacing the two flat catalog nav entries; the Cordis API section gained
inherited.md. packages/typert/generator/tests/cordis-catalog-contract.spec.tspins the region renderer (renderPageRegion), the same-page link-drop rule, and the fail-loud JSDoc/type-link validation;scripts/translation-pairing.spec.tspins the marker grammar and blob-hash primitive;scripts/gen-cordis-catalog-record.spec.tsproves the auto-record guard refuses every invalid state (stale record, malformed or renamed-key sidecar, extra entries, prose drift, missing record, missing snapshot).
中文
问题
一个子系统的文档过去分散在三个归属:手写的 subsystems 页面(介绍、数据结构、动词)、平铺生成的 docs/cordis-catalog/services.md 中属于它的 ctx.<key> 切片,以及平铺的 docs/cordis-catalog/events.md 中属于其事件作用域的切片。shell.md 的读者必须再打开两份文档,才能看到该页面正在描述的服务接口与事件;除了手工维护的链接,没有任何机制把这三个视图联系在一起。平铺目录还游离在双语语料之外(生成输出只有英文,故被排除在配对之外),因此,这套参考内容完全没有中文入口。
生成式目录决策本身(从源码生成、@mode 标签交叉校验、失败关闭的类型链接覆盖、ts cordis-catalog 围栏)不在质疑之列;改变的只是生成输出「落在哪里」。
决策
gen-cordis-catalog.ts 把每个子系统的服务与事件参考注入到该子系统自己的页面内部,置于 <!-- BEGIN GENERATED cordis-surface … --> / <!-- END GENERATED cordis-surface --> 标记之间;平铺的 services/events 目录随之删除。现在,每个子系统由一个页面同时承载介绍、数据结构和生成的接线接口参考。
- 人工维护、异常时明确报错的划分。
SERVICE_PAGE把发现的每个ctx.<key>映射到恰好一个页面;EVENT_SCOPE_PAGE映射每个事件作用域。生成器在两个方向上都会直接报错(既有被发现却未映射的服务或作用域,也有已映射但遍历不再发现的键或作用域),因此,划分不会与源码中的接口范围脱节。独立的 AST 扫描读取packages/*/*/src/**下每一个declare module 'cordis'merge 块,为投影在服务与事件两侧的盲区兜底:投影渲染不了的已声明 Context key 或 Events 成员必须在SERVICE_WALK_EXEMPTIONS/EVENT_WALK_EXEMPTIONS中带着点名理由,陈旧豁免直接报错,且投影渲染的一切也必须对扫描可见(扫描约定归事件兜底决定所有);教会投影渲染接口类型条目的后续工作由TODO(cordis-catalog-interface-services)标记。 - 区块在配对两侧按字节一致。 生成器把同一份英文区块字节写入
foo.md和foo.zh.md,是对「围栏代码块在配对两侧逐字节一致」这一既有规则的延伸。verify-translation-pairing新增了专门的区块一致性检查(标记语法归translation-pairing.ts中的partitionGeneratedRegions所有),能精确点名出现分歧或格式错误的区块;整篇文档的结构签名仍会把区块内容再覆盖一遍。 - 带防护的配对自动记录。 一次改变区块字节的重新生成会让每个被触及的配对失去同步,因此生成器会自行重新记录配对的
.i18n.yaml,但仅限本次写入完全限定在区块内的情况:两侧记录的 blob hash 必须与写入前的字节相符,且两侧剥离区块后的内容必须没有变化。人工行文若有漂移,记录就保持陈旧,配对门禁因此仍会强制走正常翻译流程;全新的配对绝不自动记录(那归作者经评审的--write所有)。这样.i18n.yaml保持为纯粹的git hash-object值:不引入任何「剥离后 hash」的语义变化。 - 继承层搬了家,而非消亡。 vendor 的
ctx成员与internal/*/loader/hmr/timer 事件渲染到docs/cordis-api/inherited.md,紧邻迁移后的 Cordis 核心 API 页面(docs/cordis-catalog/core/→docs/cordis-api/)。框架表面落在框架自己的归属之下;harness 页面仍是仓库自有的词汇。 - 页内链接。 签名的
Types:行链接到兄弟页面(core.md、shell.md);若某个类型的主要页面就是正在渲染的页面,该类型会从该行去掉,而不是链接到自身。页面用#cordis-surface或#ctx<key>--<class>锚点引用自己的区块:每个生成标题前都有一个显式<a id>,携带 GitHub slug(即平铺目录时期的历史锚点),因此这些片段在 GitHub 与 VitePress 站点上解析一致——后者自带的 slugger 对含大量标点的标题会得出不同结果。
曾考虑的替代方案
- 平铺目录与区块并存、两者都生成:否决。每次 JSDoc 编辑都会产生双份 diff 噪音,而本次变更本要消除的分散状况(一个子系统、三份文档)也将延续。
- 整页归生成器所有、手写介绍放进片段文件:否决。叙述性行文占每个现有页面的大部分,应当留在被评审的文档本身;标记只需增加一条语法规则,同时还能让作者继续编辑真实文件。
- 本地化区块(生成器同时输出中文):推迟,与 i18n README 中针对其余生成文档的长期备注同属一个状态:教会生成器输出中文意味着要翻译源码 JSDoc,而那是本次变更并不需要的机制。zh 页面里的英文区块,与「英文 JSDoc 出现在逐字节一致的围栏代码块内」这一既有现状相符。
- 在
.i18n.yaml中对剥离区块后的内容做 hash:否决。记录将不再是文件的git hash-object,这会破坏「还原上次确认文本」的性质,也会破坏每个自行重算 hash 的消费方。
后果
- 一个子系统的完整说明集中在一个页面上:
docs/subsystems/<name>.md(及其配对文件)承载介绍、数据结构/动词,以及生成的服务/事件接口参考;docs/cordis-catalog/不复存在。 - 新的服务或事件作用域无法在未记录、未映射的状态下落地:在
SERVICE_PAGE/EVENT_SCOPE_PAGE点名其所属页面之前,生成器一直失败,而且该页面必须已经存在,并在两个语言侧都带有标记。 - 源码 JSDoc 变更后的重新生成会触及两种语言的受影响页面,外加(当写入限定在区块内时)它们的配对记录:一份机械、可评审的 diff。行文编辑仍然要走翻译流程,因为自动记录防护会拒绝它们。
- 网站的子系统导航列出每个页面(每个 locale 38 条路由:35 个已翻译配对,加上仍为英文镜像的 goal/terminal/commands 三页),取代两个平铺目录导航项;Cordis API 一节新增
inherited.md。 packages/typert/generator/tests/cordis-catalog-contract.spec.ts固定区块渲染器(renderPageRegion)、同页链接去除规则,以及异常时明确报错的 JSDoc 与类型链接校验;scripts/translation-pairing.spec.ts固定标记语法与 blob hash 原语;scripts/gen-cordis-catalog-record.spec.ts证明自动重录守卫拒绝每一种非法状态(陈旧记录、格式错误或键被改名的伴随记录、多余条目、行文漂移、记录缺失、快照缺失)。