feat(doc-gates): backstop every declared cordis event; deepen the Context scan to src/**

The rendering projection walks only files reachable from host-face package
exports, so client-face Events merges vanished silently (12 events) and the
services backstop's top-level glob missed 13 nested client Context keys.
Events now get the exact mirror of the service backstop (EVENT_WALK_EXEMPTIONS,
name-keyed because client events share scopes with rendered host events), both
scans read packages/*/*/src/**/*.ts, and the partition judgment is the pure
walkPartitionProblems() so every acceptance path is unit-provable.
This commit is contained in:
Tianyi Cui
2026-08-09 02:39:12 +08:00
parent 0543a6f79d
commit 0fb062d85e
6 changed files with 401 additions and 43 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.md
2026-08-09-cordis-event-walk-backstop.md: 59aeb06149925dc93839df64145555f27257d7be
2026-08-09-cordis-event-walk-backstop.zh.md: 4549e9d382e5ff17ddff30eafad8d6c7e66d61ed

View File

@@ -0,0 +1,38 @@
# Agent Note: An independent Events backstop closes the cordis-surface exhaustiveness gap
Status: implemented
English | [中文](2026-08-09-cordis-event-walk-backstop.zh.md)
## Problem
`gen-cordis-catalog` renders every service and event the Typert host-face projection discovers, and fail-closed page maps (`SERVICE_PAGE`, `EVENT_SCOPE_PAGE`) guarantee each discovered key or scope lands on exactly one `docs/subsystems/` page. Discovery itself was only backstopped for services: an independent AST scan read every `declare module 'cordis'` Context merge and demanded each declared key be rendered or carry a named `SERVICE_WALK_EXEMPTIONS` reason.
Events had no such backstop. The projection walks only files reachable from host-face package exports, so an `interface Events` merge in client-face code — or in any file the host analyzer cannot reach — vanished with no trace: 12 declared events (`slash/input-*`, `theme/change`, `locale/change`, and the client runtime's `*/changed` invalidation signals) were documented nowhere generated and nothing would ever notice a thirteenth. The services scan also globbed only `packages/*/*/src/*.ts`, so 13 client-face Context keys declared in nested files (`src/client/**`) were invisible to the very scan meant to prevent silent vanishing.
## Decision
Events get the exact mirror of the services backstop, and both scans read the full package source tree.
`scripts/cordis-walk.ts` gains `eventNameList` (every member name of an `interface Events` merge, read from method and property members alike so a shape the projector would reject still enters the scan) and its merge-file prefilter matches the `declare module` heads instead of the literal text `interface Context`, so an Events-only merge file is not skipped. The scan glob in `gen-cordis-catalog` deepens from `packages/*/*/src/*.ts` to `packages/*/*/src/**/*.ts`.
A new curated `EVENT_WALK_EXEMPTIONS` map names every declared event the projection cannot see, with the reason and the package README that owns its surface. Keys are full event names, not scopes: client-face events share scopes with rendered host events (`commands/changed` beside the host `commands/*` family), so a scope-level exemption would mask a host-face regression. The partition check is fail-closed in both directions, exactly like the service maps: an unexempted invisible event, an exemption for an event that renders, and an exemption no merge declares are all hard errors.
The partition judgment moved out of `computeOutputs` into the pure `walkPartitionProblems(input, maps)` so every acceptance path is provable by unit test without running the Typert projection; `computeOutputs` feeds it the rendered model plus the independent scan and keeps aggregating page-splice errors as before.
The audit that motivated this found the host face already complete: 48 rendered services + 10 walk exemptions covered all 58 host-visible Context keys, all 49 host events rendered, and every type name in every rendered signature is classified by the existing fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` check. The 25 findings (12 events, 13 keys) were all client-face; each now carries a named exemption pointing at its owning README, consistent with the existing `appShell`/`connection` precedent.
## Verification
`scripts/gen-cordis-catalog-partition.spec.ts` proves each acceptance path: the green partition, an invisible unexempted event (named with its declaring file), a stale rendered-event exemption, a stale never-declared exemption, the service mirror of each, unmapped rendered surface in both page maps, and the scan reaching a nested Events-only merge file. Deleting one live exemption from the real tree makes `gen-cordis-catalog` fail loud with the event's name and declaring file; restoring it returns the generator to a byte-identical no-op regeneration (85 artifacts, 0 written), which also proves the new exemptions exactly cover today's surface. `verify-cordis-catalog` in doc-sync executes the partition on every run.
## Alternatives considered
- **Render the client face instead of exempting it.** Analyzing `faces: ['host', 'client']` and giving client services/events generated regions is the real fix for the underlying blind spot, but it changes what the subsystems catalog IS (host-tier reference) and requires page decisions for browser-only surfaces; the existing `TODO(cordis-catalog-interface-services)` already tracks widening the projection. The backstop is the guarantee; rendering is an upgrade behind it.
- **Scope-level event exemptions.** Smaller map, but `commands/changed` (client) shares the `commands` scope with rendered host events, so exempting a scope would swallow a future host-face event silently — the exact failure mode this note removes.
- **Deriving exhaustiveness from Typert instead of a raw AST scan.** The projection and the backstop must fail independently: a Typert reachability bug is precisely what the backstop exists to catch, so the scan deliberately stays a plain `ts.createSourceFile` walk with no shared machinery.
- **Gating the transitive type closure of rendered signatures.** Measured before deciding: every type name reachable in rendered signatures is already classified, and deeper field-of-field types are owned by the pages' hand-curated `type-equiv` pastes and package READMEs; a closure gate would force page homes for internals without a reader-facing need.
## Consequences
A new cordis event — host or client, any file depth — must either render onto a subsystems page or name itself in `EVENT_WALK_EXEMPTIONS` with its documentation owner; deleting one must retire its exemption. The same now holds for Context keys declared anywhere under `src/`. The curated maps grew by 25 client-face entries whose reasons all point at package READMEs, keeping the subsystems catalog a host-tier reference. `walkPartitionProblems` is the single home of the partition judgment; future backstop dimensions (e.g. rendering the client face, schema surfaces) extend it and its spec rather than re-inlining checks into `computeOutputs`.

View File

@@ -0,0 +1,38 @@
# Agent Noteagent 决策记录):独立的 Events 兜底扫描补上 cordis 表面完备性缺口
Status: implemented
[English](2026-08-09-cordis-event-walk-backstop.md) | 中文
## Problem
`gen-cordis-catalog` 渲染 Typert host face 投影发现的每个服务与事件fail-closed 的页面映射(`SERVICE_PAGE``EVENT_SCOPE_PAGE`)保证每个被发现的 key 或 scope 恰好落在一个 `docs/subsystems/` 页面上。但"发现"本身此前只对服务有兜底:一条独立的 AST 扫描读取每个 `declare module 'cordis'` Context merge要求每个声明的 key 要么被渲染、要么在 `SERVICE_WALK_EXEMPTIONS` 中给出具名理由。
事件没有这样的兜底。投影只遍历从 host face 包导出可达的文件,因此 client face 代码——或 host 分析器无法触及的任何文件——里的 `interface Events` merge 会无声消失12 个已声明事件(`slash/input-*``theme/change``locale/change`,以及 client runtime 的 `*/changed` 失效信号)不出现在任何生成文档中,而且再多一个也不会有任何机制察觉。服务扫描的 glob 也只有 `packages/*/*/src/*.ts`,于是声明在嵌套文件(`src/client/**`)中的 13 个 client face Context key 恰恰对这条为防止无声消失而存在的扫描不可见。
## Decision
事件获得与服务兜底完全对称的机制,且两条扫描都读取完整的包源码树。
`scripts/cordis-walk.ts` 新增 `eventNameList``interface Events` merge 的每个成员名,方法与属性成员一并读取,使投影器会拒绝的形状也进入扫描),其 merge 文件预过滤改为匹配 `declare module` 头部而非字面文本 `interface Context`,从而不再跳过只含 Events 的 merge 文件。`gen-cordis-catalog` 的扫描 glob 从 `packages/*/*/src/*.ts` 加深为 `packages/*/*/src/**/*.ts`
新的人工维护映射 `EVENT_WALK_EXEMPTIONS` 为投影看不到的每个已声明事件命名,附理由与拥有其表面的包 README。键是完整事件名而非 scopeclient face 事件与已渲染的 host 事件共享 scope`commands/changed` 与 host 的 `commands/*` 家族并存scope 级豁免会无声吞掉未来的 host face 回归。分区检查与服务映射一样双向 fail-closed未豁免的不可见事件、已渲染事件的豁免、无任何 merge 声明的豁免,皆为硬错误。
分区判定从 `computeOutputs` 中提取为纯函数 `walkPartitionProblems(input, maps)`,使每条验收路径都能以单元测试证明而无需运行 Typert 投影;`computeOutputs` 向它馈送渲染模型加独立扫描结果,页面拼接错误的聚合方式保持不变。
促成本决定的审计发现 host face 本已完备48 个渲染服务 + 10 条 walk 豁免覆盖全部 58 个 host 可见 Context key49 个 host 事件全部渲染,且每个渲染签名中的每个类型名都已被既有的 fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` 检查分类。25 条发现12 事件、13 key全部在 client face现在每条都带指向其所属 README 的具名豁免,与既有的 `appShell`/`connection` 先例一致。
## Verification
`scripts/gen-cordis-catalog-partition.spec.ts` 证明每条验收路径:绿色分区、不可见且未豁免的事件(报出声明文件)、已渲染事件的陈旧豁免、从未声明的陈旧豁免、服务侧的对称路径、两个页面映射中未映射的已渲染表面,以及扫描触达嵌套的仅含 Events 的 merge 文件。在真实源码树上删除一条现役豁免会让 `gen-cordis-catalog` 以事件名与声明文件大声失败;恢复后生成器回到字节相同的 no-op 再生成85 个 artifact0 写入这同时证明新豁免恰好覆盖当下表面。doc-sync 中的 `verify-cordis-catalog` 每次运行都会执行该分区检查。
## Alternatives considered
- **渲染 client face 而非豁免。** 以 `faces: ['host', 'client']` 分析并给 client 服务/事件生成区块才是对盲区的根治但它改变子系统目录的定位host 层参考),并要求为纯浏览器表面做页面归属决策;既有的 `TODO(cordis-catalog-interface-services)` 已跟踪拓宽投影。兜底是保证,渲染是其上的升级。
- **scope 级事件豁免。** 映射更小,但 `commands/changed`client与已渲染的 host 事件共享 `commands` scope豁免整个 scope 会无声吞掉未来的 host face 事件——正是本决定要消除的失败模式。
- **用 Typert 推导完备性而非原始 AST 扫描。** 投影与兜底必须独立失败Typert 的可达性 bug 恰是兜底要捕获的对象,因此扫描刻意保持为不共享机制的朴素 `ts.createSourceFile` 遍历。
- **对渲染签名的传递类型闭包设门。** 决定前先测量:渲染签名中可达的每个类型名都已分类,更深的字段套字段类型由页面手工维护的 `type-equiv` 粘贴与包 README 拥有;闭包门会在没有读者需求的情况下强迫内部类型认领页面。
## Consequences
新的 cordis 事件——host 或 client、任意文件深度——必须渲染到某个子系统页面或在 `EVENT_WALK_EXEMPTIONS` 中以其文档所有者具名;删除事件时必须一并退役其豁免。声明在 `src/` 下任意位置的 Context key 现在同样如此。人工维护映射增加了 25 条 client face 条目,理由全部指向包 README使子系统目录保持 host 层参考的定位。`walkPartitionProblems` 是分区判定的唯一居所;未来的兜底维度(如渲染 client face、schema 表面)应扩展它及其 spec而非把检查重新内联进 `computeOutputs`