mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/process/2026-07-28-per-subsystem-cordis-surface-regions.md
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.md: ef65bfc4c7dadd7dafe1a38f41656e6ecc61ea50
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.zh.md: 1df18d7260800b89c95a9d4aeb0301adb0ee56f6
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.md: f6d4494d4195ba27f2898eecb27af32b433af88b
|
||||
2026-07-28-per-subsystem-cordis-surface-regions.zh.md: ee2a3c9730e6259e9f53d1b722b04b99819af65b
|
||||
|
||||
@@ -14,7 +14,7 @@ The [generated-catalog decision](../../archived/process/2026-06-20-generated-cor
|
||||
|
||||
`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_PAGE` maps every discovered `ctx.<key>` to exactly one page; `EVENT_SCOPE_PAGE` maps 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. An independent scan of EVERY `declare module 'cordis'` Context merge backstops the rendering walk's blind spot (it only sees a root `index.ts` with a same-named service class): a declared key the walk cannot render must carry a named `SERVICE_WALK_EXEMPTIONS` reason (today: the `ctx.agent` DX accessor, plus the interface-typed or non-index-declared lsp/apiProxy/appShell/tuiPrompt/tuiResumeHost), and stale exemptions hard-error; a `TODO(cordis-catalog-interface-services)` marks teaching the walk to render them.
|
||||
- **Curated fail-loud partition.** `SERVICE_PAGE` maps every discovered `ctx.<key>` to exactly one page; `EVENT_SCOPE_PAGE` maps 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 every `declare module 'cordis'` merge block under `packages/*/*/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 named `SERVICE_WALK_EXEMPTIONS`/`EVENT_WALK_EXEMPTIONS` reason, stale exemptions hard-error, and everything rendered must also be visible to the scan ([events-backstop decision](../architecture/2026-08-09-cordis-event-walk-backstop.md) owns the scan contract); a `TODO(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.md` and `foo.zh.md`, extending the existing rule that verbatim code fences match across a pair. `verify-translation-pairing` gained a dedicated region-identity check (`partitionGeneratedRegions` in `translation-pairing.ts` owns 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.yaml` itself — 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 `--write` owns that). This keeps `.i18n.yaml` as plain `git hash-object` values — no stripped-hash semantics change.
|
||||
- **The inherited tier moved, not died.** The vendor `ctx` members and `internal/*`/loader/hmr/timer events render to `docs/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.
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
`gen-cordis-catalog.ts` 把每个子系统的服务与事件参考注入到该子系统自己的页面内部,置于 `<!-- BEGIN GENERATED cordis-surface … -->` / `<!-- END GENERATED cordis-surface -->` 标记之间;平铺的 services/events 目录随之删除。现在每个子系统由一个页面同时承载介绍、数据结构和生成的接线表面。
|
||||
|
||||
- **人工维护、响亮失败的划分。** `SERVICE_PAGE` 把发现的每个 `ctx.<key>` 映射到恰好一个页面;`EVENT_SCOPE_PAGE` 映射每个事件作用域。生成器在两个方向上都会直接报错(既有被发现却未映射的服务或作用域,也有已映射但遍历不再发现的键或作用域),因此划分不可能相对源码表面发生漂移。一个独立扫描会读取每一处 `declare module 'cordis'` 的 Context 合并,为渲染遍历的盲区(它只看得到根 `index.ts` 中同名服务类)兜底:遍历渲染不了的已声明键必须在 `SERVICE_WALK_EXEMPTIONS` 中带着点名理由(目前是 `ctx.agent` 这一 DX 访问器字段,加上接口类型或不在 index 声明的 lsp/apiProxy/appShell/tuiPrompt/tuiResumeHost),陈旧豁免同样直接报错;教会遍历渲染它们的后续工作由 `TODO(cordis-catalog-interface-services)` 标记。
|
||||
- **人工维护、响亮失败的划分。** `SERVICE_PAGE` 把发现的每个 `ctx.<key>` 映射到恰好一个页面;`EVENT_SCOPE_PAGE` 映射每个事件作用域。生成器在两个方向上都会直接报错(既有被发现却未映射的服务或作用域,也有已映射但遍历不再发现的键或作用域),因此划分不可能相对源码表面发生漂移。独立的 AST 扫描读取 `packages/*/*/src/**` 下每一个 `declare module 'cordis'` merge 块,为投影在服务与事件两侧的盲区兜底:投影渲染不了的已声明 Context key 或 Events 成员必须在 `SERVICE_WALK_EXEMPTIONS`/`EVENT_WALK_EXEMPTIONS` 中带着点名理由,陈旧豁免直接报错,且投影渲染的一切也必须对扫描可见(扫描契约归[事件兜底决定](../architecture/2026-08-09-cordis-event-walk-backstop.md)所有);教会投影渲染接口类型条目的后续工作由 `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 页面仍是仓库自有的词汇。
|
||||
|
||||
@@ -9,41 +9,53 @@ import { globSync, readFileSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
/** Cheap textual prefilter for a cordis module merge, quote-style agnostic
|
||||
* (the AST match below reads `stmt.name.text` and never sees the quotes). */
|
||||
const MERGE_HEAD = /declare module ['"](?:cordis|\.\/context\.ts)['"]/
|
||||
|
||||
/**
|
||||
* Parse every file matching `pattern` (repo-relative, sorted, `/`-normalized)
|
||||
* that textually contains a cordis module merge, yielding each file's
|
||||
* module-merge body. Files without a merge are skipped.
|
||||
* @param scanRoot - Repository root the pattern is resolved against.
|
||||
* @param pattern - Glob selecting the TypeScript files to scan.
|
||||
* @returns One entry per file with a cordis module merge, in path order.
|
||||
* Parse every file matching `patterns` (repo-relative, sorted, `/`-normalized)
|
||||
* that textually contains a cordis module merge, yielding one entry per merge
|
||||
* BLOCK — a file may legally hold several `declare module 'cordis'` blocks
|
||||
* (the Typert analyzer reads them all), so the exhaustiveness scan must too.
|
||||
* Files without a merge are skipped.
|
||||
* @param scanRoot - Repository root the patterns are resolved against.
|
||||
* @param patterns - Glob(s) selecting the TypeScript files to scan.
|
||||
* @returns One entry per cordis module block, in path then source order.
|
||||
*/
|
||||
export function contextMergeFiles(
|
||||
scanRoot: string,
|
||||
pattern: string,
|
||||
patterns: string | readonly string[],
|
||||
): { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] {
|
||||
const out: { rel: string; sf: ts.SourceFile; text: string; body: ts.ModuleBlock }[] = []
|
||||
for (const rel of globSync(pattern, { cwd: scanRoot }).map(s => s.split(sep).join('/')).sort()) {
|
||||
const rels = [...new Set(globSync(patterns as string | string[], { cwd: scanRoot }).map(s => s.split(sep).join('/')))].sort()
|
||||
for (const rel of rels) {
|
||||
const abs = resolve(scanRoot, rel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
if (!text.includes("declare module 'cordis'") && !text.includes("declare module './context.ts'")) continue
|
||||
if (!MERGE_HEAD.test(text)) continue
|
||||
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true)
|
||||
const body = cordisModuleBody(sf)
|
||||
if (!body) continue
|
||||
out.push({ rel, sf, text, body })
|
||||
for (const body of cordisModuleBodies(sf)) out.push({ rel, sf, text, body })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** The body of the cordis module merge in `sf`: `declare module 'cordis'`
|
||||
* (harness packages) or `declare module './context.ts'` (vendor core), or
|
||||
* null when the file has neither. */
|
||||
export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
|
||||
/** Every cordis module-merge body in `sf`: `declare module 'cordis'` (harness
|
||||
* packages) or `declare module './context.ts'` (vendor core), in source order. */
|
||||
export function cordisModuleBodies(sf: ts.SourceFile): ts.ModuleBlock[] {
|
||||
const bodies: ts.ModuleBlock[] = []
|
||||
for (const stmt of sf.statements) {
|
||||
if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue
|
||||
if (stmt.name.text !== 'cordis' && stmt.name.text !== './context.ts') continue
|
||||
if (stmt.body && ts.isModuleBlock(stmt.body)) return stmt.body
|
||||
if (stmt.body && ts.isModuleBlock(stmt.body)) bodies.push(stmt.body)
|
||||
}
|
||||
return null
|
||||
return bodies
|
||||
}
|
||||
|
||||
/** The FIRST cordis module-merge body in `sf`, or null without one — for the
|
||||
* vendor core-API renderer whose input files carry exactly one merge; the
|
||||
* exhaustiveness scan uses {@link cordisModuleBodies} to read them all. */
|
||||
export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null {
|
||||
return cordisModuleBodies(sf)[0] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,12 +61,33 @@ describe('walkPartitionProblems', () => {
|
||||
|
||||
it('rejects an event exemption whose event the projection renders', () => {
|
||||
const { input, maps } = baseline()
|
||||
const rendered = { ...input, renderedEventNames: new Set(['llm/request', 'theme/change']) }
|
||||
expect(walkPartitionProblems(rendered, maps)).toEqual([
|
||||
// A projection that renders theme/change necessarily renders the theme
|
||||
// scope too; the fixture models that and maps the scope so the only
|
||||
// violation is the stale exemption.
|
||||
const rendered = {
|
||||
...input,
|
||||
renderedScopes: new Set(['llm', 'theme']),
|
||||
renderedEventNames: new Set(['llm/request', 'theme/change']),
|
||||
}
|
||||
const mapped = { ...maps, eventScopePage: { llm: 'llm-streaming.md', theme: 'client-modules.md' } }
|
||||
expect(walkPartitionProblems(rendered, mapped)).toEqual([
|
||||
expect.stringContaining("event 'theme/change' is rendered by the projection but still listed in EVENT_WALK_EXEMPTIONS"),
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects rendered surface the independent scan cannot see, naming the scan as the defect', () => {
|
||||
const { input, maps } = baseline()
|
||||
const blind = {
|
||||
...input,
|
||||
declaredKeys: new Map([['theme', 'packages/client/ui-theme/src/client/index.ts']]),
|
||||
declaredEvents: new Map([['theme/change', 'packages/client/ui-theme/src/client/index.ts']]),
|
||||
}
|
||||
expect(walkPartitionProblems(blind, maps)).toEqual([
|
||||
expect.stringContaining('ctx.llm is rendered by the projection but the independent scan finds no Context merge declaring it'),
|
||||
expect.stringContaining("event 'llm/request' is rendered by the projection but the independent scan finds no Events merge declaring it"),
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects an event exemption no Events merge declares', () => {
|
||||
const { input, maps } = baseline()
|
||||
const stale = { ...maps, eventWalkExemptions: { ...maps.eventWalkExemptions, 'gone/away': 'nothing owns this' } }
|
||||
@@ -123,6 +144,49 @@ describe('cordis-walk scan reach', () => {
|
||||
expect([...contextKeyMap(only.body, only.sf).keys()]).toEqual([])
|
||||
})
|
||||
|
||||
it('yields every merge block of a multi-block file, double-quoted heads, and .tsx sources', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'cordis-walk-'))
|
||||
roots.push(root)
|
||||
const dir = join(root, 'packages/client/ui-x/src')
|
||||
mkdirSync(dir, { recursive: true })
|
||||
// The Typert analyzer reads every cordis module block in a file; the
|
||||
// backstop must not stop at the first one, skip the double-quoted legal
|
||||
// form, or ignore .tsx sources.
|
||||
writeFileSync(join(dir, 'split.ts'), [
|
||||
"declare module 'cordis' {",
|
||||
' interface Context {',
|
||||
' first: FirstService',
|
||||
' }',
|
||||
'}',
|
||||
'declare module "cordis" {',
|
||||
' interface Events {',
|
||||
" 'second/changed'(): void",
|
||||
' }',
|
||||
'}',
|
||||
'export {}',
|
||||
'',
|
||||
].join('\n'))
|
||||
writeFileSync(join(dir, 'view.tsx'), [
|
||||
"declare module 'cordis' {",
|
||||
' interface Context {',
|
||||
' fromTsx: TsxService',
|
||||
' }',
|
||||
'}',
|
||||
'export {}',
|
||||
'',
|
||||
].join('\n'))
|
||||
const merges = contextMergeFiles(root, ['packages/*/*/src/**/*.ts', 'packages/*/*/src/**/*.tsx'])
|
||||
expect(merges.map(m => m.rel)).toEqual([
|
||||
'packages/client/ui-x/src/split.ts',
|
||||
'packages/client/ui-x/src/split.ts',
|
||||
'packages/client/ui-x/src/view.tsx',
|
||||
])
|
||||
const keys = merges.flatMap(m => [...contextKeyMap(m.body, m.sf).keys()])
|
||||
const events = merges.flatMap(m => eventNameList(m.body, m.sf))
|
||||
expect(keys).toEqual(['first', 'fromTsx'])
|
||||
expect(events).toEqual(['second/changed'])
|
||||
})
|
||||
|
||||
it('reads string-literal and identifier member names from an Events merge', () => {
|
||||
const sf = ts.createSourceFile('x.ts', [
|
||||
"declare module 'cordis' {",
|
||||
|
||||
@@ -573,7 +573,9 @@ export interface WalkPartitionMaps {
|
||||
* partition maps, fail-closed in both directions for services AND events: a
|
||||
* rendered key/scope must be mapped to a page, a mapped key/scope must still
|
||||
* render, and — the backstop — a DECLARED key/event the projection cannot see
|
||||
* must carry a named walk exemption (a rendered one must not). Pure so the
|
||||
* must carry a named walk exemption (a rendered one must not). A third
|
||||
* direction guards the scan itself: everything rendered must also be declared
|
||||
* to the scan, so a scan blind spot cannot decay silently. Pure so the
|
||||
* acceptance paths are provable without running the projection.
|
||||
* @param input - rendered surface plus the declared-key/event scans.
|
||||
* @param maps - the curated page maps and walk exemptions.
|
||||
@@ -622,6 +624,17 @@ export function walkPartitionProblems(input: WalkPartitionInput, maps: WalkParti
|
||||
for (const name of Object.keys(maps.eventWalkExemptions)) {
|
||||
if (!input.declaredEvents.has(name)) problems.push(`EVENT_WALK_EXEMPTIONS names '${name}' but no Events merge declares it; remove the stale exemption.`)
|
||||
}
|
||||
// Self-check the scan itself: everything the projection renders is declared
|
||||
// in a Context/Events merge the scan must also reach, so a rendered key or
|
||||
// event the scan cannot see means the SCAN regressed (glob, prefilter, or
|
||||
// block walk) — a partial blind spot that exemption staleness alone would
|
||||
// never surface.
|
||||
for (const key of input.renderedKeys.keys()) {
|
||||
if (!input.declaredKeys.has(key)) problems.push(`ctx.${key} is rendered by the projection but the independent scan finds no Context merge declaring it; the scan has a blind spot (glob, prefilter, or module-block walk) — fix the scan, not the maps.`)
|
||||
}
|
||||
for (const name of input.renderedEventNames) {
|
||||
if (!input.declaredEvents.has(name)) problems.push(`event '${name}' is rendered by the projection but the independent scan finds no Events merge declaring it; the scan has a blind spot (glob, prefilter, or module-block walk) — fix the scan, not the maps.`)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
@@ -642,7 +655,7 @@ export function computeOutputs(): [string, string][] {
|
||||
|
||||
const declaredKeys = new Map<string, string>()
|
||||
const declaredEvents = new Map<string, string>()
|
||||
for (const { rel, sf, body } of contextMergeFiles(root, 'packages/*/*/src/**/*.ts')) {
|
||||
for (const { rel, sf, body } of contextMergeFiles(root, ['packages/*/*/src/**/*.ts', 'packages/*/*/src/**/*.tsx'])) {
|
||||
for (const key of contextKeyMap(body, sf).keys()) {
|
||||
if (!declaredKeys.has(key)) declaredKeys.set(key, rel)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user