Merge remote-tracking branch 'origin/master' into feature/readme-current-capabilities

This commit is contained in:
NI0317
2026-07-31 11:00:05 +08:00
932 changed files with 28447 additions and 4423 deletions

View File

@@ -1,6 +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
2026-07-06-node-engine-floor.md: f1754ea7ca32452a04c6cd8a0599568f602e47dd
2026-07-06-node-engine-floor.zh.md: 9d376a639378d3a0b9b645aa36c1a5d320d1d147
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-node-engine-floor.md
2026-07-06-node-engine-floor.md: ef047d885a442106a35922f4716d2996d8a98ca7
2026-07-06-node-engine-floor.zh.md: a0281addf7d4327d7f6ea30e3a3f0f40d6782bd0

View File

@@ -10,7 +10,7 @@ The Node 22 branch of the root `engines.node` range is a contract for the instal
## Decision
Set `engines.node` to `^22.19.0 || >=24.0.0` and test the keyless CI compatibility matrix on `['22.19', 24, 26]`. Every matrix leg runs the TypeScript typecheck plus a keyless source-mode worker smoke, so the floor is exercised through both a complete source typecheck and a real unbuilt runtime path. The real-API e2e workflow stays on Node 24 because it exercises API integration rather than the runtime floor.
Set `engines.node` to `^22.19.0 || >=24.0.0` and test keyless CI on `['22.19', 24, 26]`. The primary Node 24 jobs own the complete typecheck and unit coverage inventory; every version runs focused source-worker, Zstandard, source-launch, and [jsdom storage](../testing/2026-07-30-vitest-jsdom-webstorage-ownership.md) smokes without repeating that inventory. The real-API e2e workflow stays on Node 24 because it exercises API integration rather than the runtime floor.
Two Node features gate the source runtime:
@@ -24,7 +24,7 @@ Those source features clear on the 22.x line at **22.18**, but the installed Pi
## Consequences
- The advertised LTS branch no longer undercuts the Pi adapter dependency floor.
- CI proves the Node 22 LTS floor directly with Node 22.19, keeps the Node 24 branch on `node: 24`, and keeps Node 26 for the next even line; each leg typechecks the source graph and launches the unbuilt workflow worker for real.
- CI proves the Node 22 LTS floor directly with Node 22.19, keeps primary coverage on `node: 24`, and exercises Node 26 as the next even line; focused compatibility smokes run on all three versions.
- The built-bin smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the test stays the plain `node lib/bin.js` path it documents.
- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this Agent Note in the same change.

View File

@@ -10,7 +10,7 @@ Status: implemented
## 决策
`engines.node` 设为 `^22.19.0 || >=24.0.0`,并在 keyless CI 兼容性矩阵中测试 `['22.19', 24, 26]`。每条矩阵分支都运行 TypeScript 类型检查加一次 keyless 的源码模式 worker 冒烟测试,因此引擎下限通过完整的源码类型检查和真实的未构建运行时路径两条路径得到验证。真实 API 的 e2e 工作流保持在 Node 24 上,因为它验证的是 API 集成而非运行时下限。
`engines.node` 设为 `^22.19.0 || >=24.0.0`,并在 `['22.19', 24, 26]` 上运行 keyless CI。主要的 Node 24 任务负责整套类型检查和单元测试覆盖率任务;三个版本均运行 source-worker、Zstandard、source-launch 和 [jsdom 存储](../testing/2026-07-30-vitest-jsdom-webstorage-ownership.md) 专项冒烟测试,不重复这套类型检查和覆盖率任务。真实 API 的 e2e 工作流保持在 Node 24 上,因为它验证的是 API 集成而非运行时下限。
两个 Node 特性决定了源码运行时的门槛:
@@ -24,7 +24,7 @@ Status: implemented
## 后果
- 宣传的 LTS 分支不再低于 Pi 适配器依赖的下限。
- CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,Node 24 分支保持 `node: 24`Node 26 用于下一个偶数线;每条分支都对源码图执行类型检查,并实际启动未构建的工作流 worker
- CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,将主要覆盖率任务保留在 `node: 24`并用 Node 26 验证下一个偶数线;三个版本均运行聚焦的兼容性冒烟测试
- built-bin 冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此测试保持其文档所述的纯 `node lib/bin.js` 路径。
- 未来若依赖或源码 API 提高运行时下限,必须在同一变更中同步调整 `engines.node`、兼容性矩阵和本 Agent Noteagent 决策记录)。

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/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

View File

@@ -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 `<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 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.

View File

@@ -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` 整体代入其 `<group>/*/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 唯一解析入口的定位。

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/process/2026-07-31-coverage-exempt-heavy-suites.md
2026-07-31-coverage-exempt-heavy-suites.md: 7235a5193554947ecf71f62d522d09f4e21cb1da
2026-07-31-coverage-exempt-heavy-suites.zh.md: b739e4494ae8d240b0e35109920a49876ebd222d

View File

@@ -0,0 +1,61 @@
# Agent Note: Coverage-exempt heavy suites
Status: implemented
English | [中文](2026-07-31-coverage-exempt-heavy-suites.zh.md)
## Problem
The CI coverage lane (`check:ci:coverage`) had its wall clock pinned by a handful of heavy test files: in a local 6-worker full-suite profile, 555 test files aggregated 1595 seconds, with `packages/typert/generator/tests/type-model.spec.ts` alone at 885 seconds and the top 10 files holding 84% of the aggregate. These suites share one shape — every case performs whole-workspace compiler analysis or drives real subprocess fixtures — and v8 instrumentation multiplies exactly that kind of runtime.
The decisive waste: the instrumentation tax these suites paid contributed **nothing** to the per-file 100% thresholds — the measured code they execute in-process is either outside the threshold scope already or independently fully covered by other suites. Running them instrumented traded lane time for zero information.
## Decision
The `ci-coverage` aggregate splits into two parallel gates; every test still runs, and only the heavy suites stop paying the instrumentation tax:
- **Instrumented gate** (`test:coverage`): sets `DSH_COVERAGE_EXEMPT_HEAVY=1`, which makes `vitest.config.ts` drop the exempt suites from both projects' excludes; every remaining file runs instrumented and carries the entire threshold proof. The variable is injected through the gate's own env (the existing `Gate.env` mechanism), not the workflow-global environment, so the uninstrumented gate beside it and any local `vitest run` never see it and behave unchanged.
- **Uninstrumented gate** (`test:coverage-exempt-heavy`): runs exactly the exempt suites through paired positional filters, keeping the correctness signal whole.
`scripts/coverage-exempt.ts` is the single roster point, holding the membership contract and the filter/exclude pairs so the two sides cannot drift.
### The roster, reconciled entry by entry
A suite contributes to coverage exactly when it executes measured files in-process (`coverage.include` spans the package src trees). The current roster, audited:
| Exempt suite | Measured code executed in-process | Who carries the coverage |
| --- | --- | --- |
| All 6 typert generator specs | The generator's own src | Generator src is threshold-excluded as a package (`vitest.config.ts`) — outside the threshold scope to begin with |
| tools-catalog.spec additionally imports | `typert-registry` and `tool-cordis` src | Each package's own tests cover them fully (verified with focused coverage runs, zero threshold errors) |
| `scripts/install-lefthook.spec.ts`, `scripts/oxlint-contract.spec.ts`, `scripts/change-scope.spec.ts` | None — they test `scripts/` sources (never in `coverage.include`) and work by spawning child processes | Nothing to carry |
### Membership contract
A new exemption must satisfy both: every measured file the suite executes in-process is already fully covered by other suites (or threshold-excluded), and the filter and exclude select exactly the same file set. The contract text lives beside the roster in the same file.
### The gate polices the roster automatically
The per-file 100% thresholds are themselves the roster's guard; a wrong roster cannot pass silently:
- If a future exempt suite in fact solely covers some measured file, the instrumented gate goes red on the spot (that file drops below 100%).
- The converse holds too: new code covered only by an exempt suite turns the gate red immediately.
Coverage-result invariance therefore does not rest on humans maintaining the roster, in line with the misconfiguration-fails-loud convention. The only thing given up is that the exempt suites' own execution no longer produces coverage data — the table above shows that data was entirely redundant, so the final report is file-for-file identical in threshold terms.
## Alternatives considered
- **CLI `--exclude` to drop the exempt suites from the instrumented gate.** Proven ineffective: vitest 4's `cliExclude` does not participate in per-project include resolution, so under a multi-project config the exempt suites stayed selected; the env + config route replaced it.
- **Lowering worker counts or raising gate concurrency.** Measured ineffective during the incident: the lane's wall clock was pinned by the longest tail files (aggregate/wall ≈ 4× effective parallelism), and the concurrency knobs moved nothing in either direction.
- **Cross-runner sharding (`--shard` + blob merge).** Would compress the wall clock further but adds matrix, artifact-pipeline, and merge-job complexity; with the split landed the lane sits near 2 minutes, which does not justify the cost. Revisit if the suite grows substantially.
- **Deleting or skipping the heavy suites.** Rejected: they are the sole correctness evidence for the typert generator and the scripts tooling; running them uninstrumented in parallel preserves the full signal.
## Verification
Measured on CI (16-core runner): the gate segment went from 424 seconds to the two gates in parallel — `test:coverage` 95.9 s + `test:coverage-exempt-heavy` 71.1 s — with the lane converging on the slower at about 96 seconds; the instrumented gate reported zero threshold errors both before and after the split. `vitest list` verifies the env toggle adds and removes exactly the exempt set; `run-gates.spec.ts` covers the aggregate graph construction.
## Consequences
- The coverage lane's gate segment drops from about 7 minutes to about 96 seconds with no change in threshold outcome or executed test set.
- `DSH_GATE_CONCURRENCY` has two schedulable gates in this lane again, so the aggregate scheduler is no longer a pass-through.
- Adding a heavy suite to the roster requires the membership audit above; a wrong entry fails the instrumented gate loudly rather than eroding coverage silently.
- The exempt suites no longer appear in the coverage report's file list of contributors; their correctness signal lives solely in the uninstrumented gate's pass/fail.

View File

@@ -0,0 +1,61 @@
# Agent Note: 覆盖率豁免重型套件
Status: implemented
[English](2026-07-31-coverage-exempt-heavy-suites.md) | 中文
## Problem
CI 覆盖率 lane`check:ci:coverage`)的墙钟被少数几个重型测试文件钉死:本地 6-worker 全量剖析中555 个测试文件聚合 1595 秒,其中 `packages/typert/generator/tests/type-model.spec.ts` 一个文件占 885 秒,前 10 个文件占聚合时长的 84%。这类套件的共同点是每个用例都做全工作区编译器分析或真实子进程 fixturev8 插桩把这类代码的运行时间放大数倍。
关键的浪费在于:这些套件缴纳的插桩税对 per-file 100% 阈值**没有任何贡献**——它们进程内执行的被度量代码,要么本来就不在阈值口径内,要么已由其他套件独立满覆盖。继续在插桩下运行它们,纯粹是用 lane 时长换零信息。
## Decision
`ci-coverage` 聚合拆成两个并行 gate全部测试仍然执行只有重型套件不再交插桩税
- **插桩 gate**`test:coverage`):设 `DSH_COVERAGE_EXEMPT_HEAVY=1``vitest.config.ts` 据此从两个 project 的 exclude 中剔除豁免套件,其余全部文件照旧插桩并承担全部阈值证明。经 gate 自带 env 注入(既有 `Gate.env` 机制),不进 workflow 全局环境,因此并排的无插桩 gate 和本地直跑 `vitest run` 都看不到该变量、行为不变。
- **无插桩 gate**`test:coverage-exempt-heavy`):用配对的 positional filter 恰好运行豁免套件,保证正确性信号不缩水。
`scripts/coverage-exempt.ts` 是唯一名单点,集中持有成员资格契约与 filter/exclude 配对,防止两侧漂移。
### 豁免名单与逐项对账
一个套件对覆盖率有贡献,当且仅当它在进程内执行了被度量的文件(`coverage.include` = 包 src 树)。现行名单逐项核对:
| 豁免套件 | 进程内执行的被度量代码 | 覆盖由谁接住 |
| --- | --- | --- |
| typert generator 全部 6 个 spec | generator 自身 src | generator src 已整包 threshold-excluded`vitest.config.ts`),本不在阈值口径内 |
| 其中 tools-catalog.spec 额外 import | `typert-registry``tool-cordis` 的 src | 两包各自的测试独立满覆盖focused coverage 实测无阈值错误) |
| `scripts/install-lefthook.spec.ts``scripts/oxlint-contract.spec.ts``scripts/change-scope.spec.ts` | 无——被测对象是 `scripts/` 源码(从不在 coverage.include执行方式是 spawn 子进程 | 无需接 |
### 成员资格契约
新增豁免必须同时满足套件进程内执行的每个被度量文件都已由其他套件满覆盖或在阈值排除名单内filter 与 exclude 选中完全相同的文件集。契约文本随名单同文件维护。
### 门禁自动守卫名单正确性
per-file 100% 阈值本身就是豁免名单的守卫,名单错误无法静默通过:
- 若未来某个豁免套件实际独家覆盖着某个被度量文件,插桩 gate 当场红(该文件跌破 100%
- 反向同理:出现"只有豁免套件才覆盖"的新代码,同样立刻红。
因此覆盖率结果的不变性不依赖人工维护名单,符合"misconfiguration fails loud"约定。唯一失去的是豁免套件自身的执行不再产出覆盖数据——由上表可知这些数据全部冗余,最终报告在阈值意义上逐文件相同。
## Alternatives considered
- **CLI `--exclude` 从插桩 gate 剔除豁免套件。** 实证无效vitest 4 的 `cliExclude` 不参与 per-project include 解析,多 project 配置下豁免套件仍被选中,故改走 env + config。
- **降低 worker 数或提高 gate 并发。** 事故期间实测无效lane 墙钟被尾部最长文件钉死(聚合/墙钟 ≈ 4× 有效并行),并发旋钮两个方向都动不了尾巴。
- **跨 runner 分片(`--shard` + blob 合并)。** 能进一步压墙钟但引入 matrix、artifact 管道与合并 job 的复杂度;拆分落地后 lane 已到约 2 分钟,不值得付。若未来套件规模再涨可重新评估。
- **直接删除或跳过重型套件。** 拒绝:它们是 typert generator 与 scripts 工具的唯一正确性证据,无插桩并排执行保住全部信号。
## Verification
CI 实测16 核 runner拆分前 gate 段 424 秒,拆分后两 gate 并行 `test:coverage` 95.9 秒 + `test:coverage-exempt-heavy` 71.1 秒lane 收敛于较慢者约 96 秒;拆分前后插桩 gate 阈值错误均为零。`vitest list` 验证 env 开关两态恰好增删豁免集;`run-gates.spec.ts` 覆盖聚合图构造。
## Consequences
- 覆盖率 lane 的 gate 段从约 7 分钟降到约 96 秒,阈值结果与执行测试集均无变化。
- `DSH_GATE_CONCURRENCY` 在本 lane 重新拥有两个可调度对象,聚合调度器不再是直通。
- 向名单新增重型套件必须完成上述成员资格对账;错误条目会让插桩 gate 大声失败,而不是静默侵蚀覆盖率。
- 豁免套件不再出现在覆盖率报告的贡献文件列表中;其正确性信号完全由无插桩 gate 的红绿承载。