From 30cda487eafd17ee5ee1d77b294f3e3fd7642e59 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 30 Jul 2026 21:34:15 +0800 Subject: [PATCH 1/2] fix(cli): map @deepseek-ai/dsh-tui/prompt to source in tsconfig paths The tui.cordis.yml entry for @deepseek-ai/dsh-tui/prompt had no tsconfig paths mapping: the @deepseek-ai/dsh-* wildcard substitutes tui/prompt whole into nonexistent candidates, so the tsx source launch fell back to package exports and required built lib/prompt.js. pnpm dsh failed at startup on every clean tree (fresh worktrees) with 'plugin(s) failed to load'. --- tsconfig.base.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.base.json b/tsconfig.base.json index 527dc44758..801ed2dc66 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -58,6 +58,7 @@ "@deepseek-ai/dsh-llm/brand": ["./packages/llm/llm/src/brand.ts"], "@deepseek-ai/dsh-llm/message": ["./packages/llm/llm/src/message.ts"], "@deepseek-ai/dsh-commands/brand": ["./packages/ui/commands/src/brand.ts"], + "@deepseek-ai/dsh-tui/prompt": ["./packages/ui/tui/src/prompt.ts"], "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], "@deepseek-ai/dsh-user-approval/types": ["./packages/ui/user-approval/src/types.ts"], "@deepseek-ai/dsh-user-interaction/types": ["./packages/ui/user-interaction/src/types.ts"], From 37116d0b0adae58e7e6a5b279e8004e9eb7d81a5 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 30 Jul 2026 21:34:23 +0800 Subject: [PATCH 2/2] test(scripts): gate source-plane resolution of configured plugins verify-cordis-config now requires every configured specifier of a local workspace package to resolve through the tsconfig.base.json paths facade to a .ts/.tsx source file. A failed resolution or a .d.ts hit (the exports fallback into built lib/types) fails the gate, so a missing paths mapping is a red gate instead of a clean-tree-only startup crash masked by built trees in CI. Removing the dsh-tui/prompt mapping reproduces the failure. Agent Note records the decision and alternatives. --- ...fig-source-plane-resolution-gate.i18n.yaml | 6 +++ ...dis-config-source-plane-resolution-gate.md | 27 ++++++++++ ...-config-source-plane-resolution-gate.zh.md | 27 ++++++++++ scripts/verify-cordis-config.ts | 52 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 .agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.md create mode 100644 .agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.zh.md diff --git a/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.i18n.yaml b/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.i18n.yaml new file mode 100644 index 0000000000..9b7308098b --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.i18n.yaml @@ -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/process/2026-07-30-cordis-config-source-plane-resolution-gate.md +2026-07-30-cordis-config-source-plane-resolution-gate.md: f9070d39559948ef27f96df5afccd7c4e076f131 +2026-07-30-cordis-config-source-plane-resolution-gate.zh.md: fac6c4047d334d7dd0685aa270234fee3d15dba8 diff --git a/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.md b/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.md new file mode 100644 index 0000000000..f9070d3955 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.md @@ -0,0 +1,27 @@ +# Agent Note: verify-cordis-config gates source-plane resolution of configured plugins + +Status: implemented + +English | [中文](2026-07-30-cordis-config-source-plane-resolution-gate.zh.md) + +## 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 `/*/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 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. diff --git a/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.zh.md b/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.zh.md new file mode 100644 index 0000000000..fac6c4047d --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-30-cordis-config-source-plane-resolution-gate.zh.md @@ -0,0 +1,27 @@ +# Agent Note: verify-cordis-config 对配置中插件的源码面解析实施门禁 + +Status: implemented + +[English](2026-07-30-cordis-config-source-plane-resolution-gate.md) | 中文 + +## 问题 + +`apps/cli/config/tui.cordis.yml` 新增了 `@deepseek-ai/dsh-tui/prompt` 配置项,却没有对应的 tsconfig `paths` 映射。通用的 `@deepseek-ai/dsh-*` 通配符会把 `tui/prompt` 整体代入其 `/*/src` 候选路径,而这些路径全都不存在,因此 [tsx 源码启动](../architecture/2026-07-29-dsh-source-launch-tsx-esm.md)会回退到包(package)的 `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.ts`(`validateSourcePlaneResolution`)要求配置中凡是引用本地 workspace 包的模块说明符(包括 harness 包与纳入 vendor 的 Cordis)都必须通过 `tsconfig.base.json` 的 `paths` 外观层(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 唯一解析入口的定位。 diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts index 1e0127a826..bda0d5ecfc 100644 --- a/scripts/verify-cordis-config.ts +++ b/scripts/verify-cordis-config.ts @@ -60,6 +60,7 @@ for (const file of files) { errors.push(...validateExampleResolution()) errors.push(...validateAppResolution()) +errors.push(...validateSourcePlaneResolution()) if (errors.length > 0) { console.error('verify-cordis-config: invalid Loader metadata or plugin package resolution:') @@ -138,6 +139,57 @@ function validateAppResolution(): string[] { return missingPluginDependencies(references, dependencies, 'apps/cli/package.json') } +/** + * Every configured specifier of a local workspace package must resolve through + * the tsconfig `paths` facade to a `.ts`/`.tsx` source file. The `dsh` source + * launch (tsx) and vitest resolve in the source plane; without a `paths` match + * they fall back to package `exports`, which reach built `lib/` — present on a + * built dev tree, absent on a clean one — so a missing mapping boots locally + * yet breaks every clean checkout. Anything but a `.ts`/`.tsx` hit (a `.d.ts` + * or `.js` under built `lib/`) is that artifact-plane fallback, not source. + */ +function validateSourcePlaneResolution(): string[] { + const violations: string[] = [] + const localPackages = localPackageDirectories() + const config = ts.readConfigFile(resolve(root, 'tsconfig.base.json'), path => ts.sys.readFile(path)) + if (config.error !== undefined) { + throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n')) + } + const { options, errors: optionErrors } = ts.convertCompilerOptionsFromJson( + (config.config as { compilerOptions?: unknown }).compilerOptions, + root, + 'tsconfig.base.json', + ) + if (optionErrors.length > 0) { + throw new Error(optionErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n')) + } + // convertCompilerOptionsFromJson leaves `pathsBasePath` unset, so relative + // `paths` targets resolve against the host's current directory; anchor it to + // the repository root to keep the gate cwd-independent. + const host: ts.ModuleResolutionHost = { + fileExists: path => ts.sys.fileExists(path), + readFile: path => ts.sys.readFile(path), + directoryExists: path => ts.sys.directoryExists(path), + getCurrentDirectory: () => root, + } + const sourceExtensions = new Set([ts.Extension.Ts, ts.Extension.Tsx]) + const containingFile = resolve(root, 'scripts/verify-cordis-config.ts') + const locationsBySpecifier = new Map>() + for (const reference of pluginReferences) { + const packageName = packageNameFromSpecifier(reference.name) + if (packageName === undefined || !localPackages.has(packageName)) continue + const locations = locationsBySpecifier.get(reference.name) ?? new Set() + locations.add(reference.file) + locationsBySpecifier.set(reference.name, locations) + } + for (const [specifier, locations] of locationsBySpecifier) { + const resolved = ts.resolveModuleName(specifier, containingFile, options, host).resolvedModule + if (resolved !== undefined && sourceExtensions.has(resolved.extension)) continue + violations.push(`${[...locations].join(', ')}: ${specifier} does not resolve to workspace source through tsconfig.base.json paths (add a mapping so the tsx source launch does not depend on built lib/)`) + } + return violations +} + function missingPluginDependencies( references: readonly PluginReference[], dependencies: Readonly>,