fix(doc-gates): review findings — scan every merge block, any quote style, .tsx too; self-check the scan

ds-review-bot round: the backstop stopped at a file's first declare-module
block while the projection it guards reads them all; the textual prefilter
matched only single quotes; .tsx sources escaped the glob. All three are the
silent-vanish class the scan exists to prevent. contextMergeFiles now yields
one entry per block (quote-agnostic prefilter, ts+tsx patterns), and a third
partition direction requires everything rendered to be scan-visible, so a
future scan regression is a hard error. Spec fixture made projection-
consistent; the partially superseded 2026-07-28 regions note is rewritten to
the current mechanism and cross-linked both ways.
This commit is contained in:
Tianyi Cui
2026-08-09 10:29:13 +08:00
parent 0fb062d85e
commit c9205901ec
9 changed files with 123 additions and 34 deletions

View File

@@ -2,5 +2,5 @@
# 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
2026-08-09-cordis-event-walk-backstop.md: 1d833858f9471c98f54b3793edd12e4710b8c0fc
2026-08-09-cordis-event-walk-backstop.zh.md: 0cb0f3bd738b6cee703cee71f1c24741554df87a

View File

@@ -6,7 +6,7 @@ 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.
`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 ([per-subsystem regions decision](../process/2026-07-28-per-subsystem-cordis-surface-regions.md) owns the page-region mechanism). 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.
@@ -14,7 +14,7 @@ Events had no such backstop. The projection walks only files reachable from host
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`.
`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); the scan yields every `declare module 'cordis'` block in a file (the Typert analyzer reads them all, so stopping at the first would hide a second block's face), and its quote-agnostic prefilter matches the `declare module` heads instead of the literal text `interface Context`, so an Events-only or double-quoted merge file is not skipped. The scan glob in `gen-cordis-catalog` deepens from `packages/*/*/src/*.ts` to `packages/*/*/src/**/*.{ts,tsx}` (two patterns). A third partition direction guards the scan itself: every rendered service key and event name must also be visible to the scan, so a scan regression (glob, prefilter, block walk) is a hard error rather than a silent backstop decay.
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.
@@ -24,7 +24,7 @@ The audit that motivated this found the host face already complete: 48 rendered
## 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.
`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, rendered surface the scan cannot see (the third direction), and the scan reaching nested Events-only merges, every block of a multi-block file, double-quoted heads, and `.tsx` sources. 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

View File

@@ -6,7 +6,7 @@ Status: implemented
## 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` 中给出具名理由。
`gen-cordis-catalog` 渲染 Typert host face 投影发现的每个服务与事件,fail-closed 的页面映射(`SERVICE_PAGE`、`EVENT_SCOPE_PAGE`)保证每个被发现的 key 或 scope 恰好落在一个 `docs/subsystems/` 页面上(页面区块机制归[按子系统区块决定](../process/2026-07-28-per-subsystem-cordis-surface-regions.md)所有)。但"发现"本身此前只对服务有兜底:一条独立的 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 恰恰对这条为防止无声消失而存在的扫描不可见。
@@ -14,7 +14,7 @@ Status: implemented
事件获得与服务兜底完全对称的机制,且两条扫描都读取完整的包源码树。
`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`。
`scripts/cordis-walk.ts` 新增 `eventNameList`(`interface Events` merge 的每个成员名,方法与属性成员一并读取,使投影器会拒绝的形状也进入扫描);扫描产出文件中每一个 `declare module 'cordis'` 块(Typert 分析器读取全部块,止步于第一个会藏起第二个块的表面),其对引号风格不敏感的预过滤匹配 `declare module` 头部而非字面文本 `interface Context`,从而不再跳过只含 Events 或使用双引号的 merge 文件。`gen-cordis-catalog` 的扫描 glob 从 `packages/*/*/src/*.ts` 加深为 `packages/*/*/src/**/*.{ts,tsx}`(两个 pattern)。分区新增第三个方向守住扫描自身:投影渲染的每个服务 key 与事件名也必须对扫描可见,使扫描回归(glob、预过滤、块遍历)成为硬错误而非兜底的无声退化。
新的人工维护映射 `EVENT_WALK_EXEMPTIONS` 为投影看不到的每个已声明事件命名,附理由与拥有其表面的包 README。键是完整事件名而非 scope:client face 事件与已渲染的 host 事件共享 scope(`commands/changed` 与 host 的 `commands/*` 家族并存),scope 级豁免会无声吞掉未来的 host face 回归。分区检查与服务映射一样双向 fail-closed:未豁免的不可见事件、已渲染事件的豁免、无任何 merge 声明的豁免,皆为硬错误。
@@ -24,7 +24,7 @@ Status: implemented
## Verification
`scripts/gen-cordis-catalog-partition.spec.ts` 证明每条验收路径:绿色分区、不可见且未豁免的事件(报出声明文件)、已渲染事件的陈旧豁免、从未声明的陈旧豁免、服务侧的对称路径、两个页面映射中未映射的已渲染表面,以及扫描触达嵌套的仅含 Events 的 merge 文件。在真实源码树上删除一条现役豁免会让 `gen-cordis-catalog` 以事件名与声明文件大声失败;恢复后生成器回到字节相同的 no-op 再生成(85 个 artifact,0 写入),这同时证明新豁免恰好覆盖当下表面。doc-sync 中的 `verify-cordis-catalog` 每次运行都会执行该分区检查。
`scripts/gen-cordis-catalog-partition.spec.ts` 证明每条验收路径:绿色分区、不可见且未豁免的事件(报出声明文件)、已渲染事件的陈旧豁免、从未声明的陈旧豁免、服务侧的对称路径、两个页面映射中未映射的已渲染表面、扫描看不到的已渲染表面(第三方向),以及扫描触达嵌套的仅含 Events 的 merge、多块文件的每个块、双引号头部与 `.tsx` 源文件。在真实源码树上删除一条现役豁免会让 `gen-cordis-catalog` 以事件名与声明文件大声失败;恢复后生成器回到字节相同的 no-op 再生成(85 个 artifact,0 写入),这同时证明新豁免恰好覆盖当下表面。doc-sync 中的 `verify-cordis-catalog` 每次运行都会执行该分区检查。
## Alternatives considered