DSH / Atlas
2026-07-30implementedprocess

verify-cordis-config gates source-plane resolution of configured plugins

verify-cordis-config 对配置中插件的源码面解析实施门禁

`apps/cli/config/tui.cordis.yml` gained the `@deepseek-ai/dsh-tui/prompt` entry without a matching tsconfig `paths` mapping. The generic `@deepseek-ai/dsh-*` wildcard substitutes `tui/prompt` whole into its `<group>/*/src` candidates, none of which exist, so the [tsx source launch](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md) fell back to package `exports` and resolved `lib/prompt.js` — an artifact-plane

English

Problem

apps/cli/config/tui.cordis.yml gained the @deepseek-ai/dsh-tui/prompt entry without a matching tsconfig paths mapping. The generic @deepseek-ai/dsh-* wildcard substitutes tui/prompt whole into its <group>/*/src candidates, none of which exist, so the tsx source launch fell back to package exports and resolved lib/prompt.js — an artifact-plane file. Every environment with a built lib/ (developer trees after pnpm build) booted fine, and the e2e workflow runs the keyless TUI PTY smoke in lib mode (DSH_EXAMPLE_MODE=lib, built bin under plain Node) so CI never exercises the source vector at all — while every clean checkout failed pnpm dsh at startup with plugin(s) failed to load: @deepseek-ai/dsh-tui/prompt. No gate checked the source plane, so the breakage shipped silently and surfaced only in fresh worktrees.

Decision

scripts/verify-cordis-config.ts (validateSourcePlaneResolution) requires every configured specifier of a local workspace package — harness packages and vendored Cordis alike — to resolve through the tsconfig.base.json paths facade to a .ts/.tsx source file, using ts.resolveModuleName from the repository root. A failed resolution or a .d.ts hit (the exports fallback into built lib/types) fails verify-cordis-config, naming the config files and the specifier. The missing @deepseek-ai/dsh-tui/prompt mapping is added next to the other explicit subpath entries; removing it reproduces the gate failure.

Alternatives considered

Rely on the keyless TUI PTY smoke. In default source mode it boots the real tree through the source vector and does catch the failure — but only on a clean tree. CI's e2e workflow runs it exclusively in lib mode (the built bin resolving real package exports), so no CI line runs the source vector, and developer trees with a stale lib/ stay masked locally. Adding a source-mode CI smoke proves one composition per run; the static gate covers every shipped and example config.

Broaden the dsh-source-launch-smoke compat test to full boot. The node-compat smoke asserts only the TTY refusal, which happens before plugin loading. A full keyless boot per matrix line duplicates the PTY smoke at higher cost and, like it, proves one composition rather than every shipped and example config.

A @deepseek-ai/dsh-*/prompt-style wildcard mapping. Fixes this one subpath but not the class; the next single-file subpath export (/surface, /message, …) regresses identically. The static gate covers all current and future configured specifiers.

Consequences

  • A configured workspace specifier that resolves only through built lib/ is now a red verify-cordis-config (in hygiene and CI) instead of a clean-tree-only startup crash.
  • New single-file subpath exports referenced from a cordis.yml need an explicit tsconfig.base.json paths entry at introduction time; the gate message says so.
  • The gate resolves with tsconfig.base.json options only; a specifier needing client-only compiler options to resolve would fail it, which matches the facade's role as the single resolution surface for tsx and vitest.

中文

问题

apps/cli/config/tui.cordis.yml 新增了 @deepseek-ai/dsh-tui/prompt 配置项,却没有对应的 tsconfig paths 映射。通用的 @deepseek-ai/dsh-* 通配符会把 tui/prompt 整体代入其 <group>/*/src 候选路径,而这些路径全都不存在,因此 tsx 源码启动 会回退到包的 exports,解析出产物面文件 lib/prompt.js。任何带有已构建 lib/ 的环境(开发者目录树运行 pnpm build 后)都能正常启动,而 e2e 工作流以 lib 模式(DSH_EXAMPLE_MODE=lib,构建产物 bin 在普通 Node 下运行)执行无密钥 TUI PTY 冒烟测试,因此 CI 根本不会经过源码启动向量——与此同时,所有干净检出环境中的 pnpm dsh 都会在启动时失败,并报错 plugin(s) failed to load: @deepseek-ai/dsh-tui/prompt。当时没有门禁检查源码面,因此该故障未被发现便进入发布版本,仅在新的 worktree 中暴露。

决策

scripts/verify-cordis-config.tsvalidateSourcePlaneResolution)要求配置中凡是引用本地 workspace 包的模块说明符(包括 harness 包与纳入 vendor 的 Cordis)都必须通过 tsconfig.base.jsonpaths 外观层(facade)解析到 .ts/.tsx 源文件;解析以仓库根目录为起点,调用 ts.resolveModuleName 完成。解析失败或命中 .d.ts(即经 exports 回退到构建出的 lib/types)都会使 verify-cordis-config 失败,并列出配置文件与模块说明符。缺失的 @deepseek-ai/dsh-tui/prompt 映射已添加在其他显式子路径条目旁;删除该映射即可复现门禁失败。

备选方案

依赖无密钥 TUI PTY 冒烟测试。 在默认源码模式下,该测试通过源码向量启动真实目录树,确实能捕获这个故障,但仅限干净目录树。CI 的 e2e 工作流只以 lib 模式运行它(构建产物 bin 通过真实的包 exports 解析),因此没有任何 CI 环节执行源码向量,而带有陈旧 lib/ 的开发者目录树在本地也仍被掩盖。为 CI 增加一个源码模式冒烟测试,每次也只能证明一种组合;静态门禁则覆盖所有随产品发布的配置与示例配置。

dsh-source-launch-smoke 兼容性测试扩展为完整启动。 node-compat 冒烟测试只断言 TTY 拒绝,而该拒绝发生在插件加载之前。每条矩阵版本线都执行一次完整的无密钥启动,会以更高成本重复 PTY 冒烟测试,而且同样只能验证一种组合,无法覆盖所有随产品发布的配置与示例配置。

使用类似 @deepseek-ai/dsh-*/prompt 的通配符映射。 这能修复当前子路径,却不能杜绝这一类问题;下一个单文件子路径导出(/surface/message 等)仍会以同样方式复发。静态门禁覆盖当前及未来配置中引用的所有模块说明符。

结果

  • 配置中的 workspace 模块说明符若只能通过构建后的 lib/ 解析,现在会导致 verify-cordis-config 门禁失败(在 hygiene 和 CI 中执行),而不再成为只在干净目录树中出现的启动崩溃。
  • cordis.yml 中引用新的单文件子路径导出时,必须同步为 tsconfig.base.json 添加显式 paths 条目;门禁消息会明确提示这一要求。
  • 门禁只使用 tsconfig.base.json 的选项执行解析;如果某个模块说明符需要仅客户端可用的编译器选项才能解析,门禁就会失败。这符合该外观层作为 tsx 与 vitest 唯一解析入口的定位。