前三篇讲了 dsh 的架构、执行流和能力接缝。这些设计靠什么守住?答案藏在根目录 package.json 里:44 个 verify-* 脚本,加上一条毫不客气的覆盖率注释。
44 个 verify-* 脚本
这些脚本不跑测试,它们把仓库本身当成被检查的对象。挑几类看看:
| 类别 | 代表脚本 | 管什么 |
|---|---|---|
| 包结构 | verify-package-invariants、verify-module-graph、verify-runtime-closure | 包的运行时契约、模块依赖图、运行闭包完整性 |
| 组合配置 | verify-cordis-config、verify-cordis-catalog、verify-tool-catalog、verify-config-catalog | cordis.yml 合法性、插件/工具/配置目录与源码一致 |
| 文档 | verify-md-links、verify-md-wrap、verify-type-equiv、verify-doc-budgets | 链接与锚点、换行规则、文档里粘贴的类型与源码同步、字数上限 |
| Agent Notes | verify-agent-note-classification / -format、verify-archived-agent-notes | 决策记录的分类、格式、归档完整性 |
| 双语文档 | verify-translation-pairing、verify-translation-prompt | 中英配对的同步状态 |
| 客户端 | verify-client-packages、verify-client-ui-i18n、verify-client-domain-graph | 前端包的依赖纪律、文案本地化、领域依赖层级 |
举两个细节。verify-client-ui-i18n 会拒绝硬编码文案——所有产品文本必须走类型化的语言字典,界面上不允许出现散落的字符串。verify-doc-budgets 给每份常备文档设了字数上限(比如根 AGENTS.md 不超过 1950 词),超了或者文件丢了都拒绝合并,规矩写在一个 manifest 里。文档也会腐化,他们选择用门禁对抗腐化。
覆盖率:逐文件 100%,不许大文件补贴小文件
vitest.config.ts 里的覆盖率配置,注释比配置还有态度:
// 100% or it doesn't merge (docs/testing.md: excessive tests are welcome).
// Per-file so a well-covered big file can't subsidize a bare one.
thresholds: {
perFile: true,
statements: 100, branches: 100, functions: 100, lines: 100,
}
范围是全部 packages/*/*/src。逐文件是重点:总覆盖率 100% 可以靠一个大文件把裸奔的小文件平均掉,perFile 堵死了这条路。豁免清单存在,但每一条都带注释说明理由;还有一个自定义报告器(scripts/coverage-uncovered-locations.cjs),红灯时直接打印未覆盖语句的精确位置,不让人猜。CI 的入口是 check:ci:coverage,走统一的 scripts/run-gates.ts。
测试分四层,快照层不需要 API key
| 层 | 文件模式 | 特点 |
|---|---|---|
| unit | tests/**/*.spec.ts | 常规单元测试 |
| e2e | tests/**/*.e2e.ts | 打真实 API;没有 DEEPSEEK_API_KEY 时自动跳过 |
| expected | tests/**/*.expected.e2e.ts | 进程级的期望输出,由各 owner 维护 |
| snapshot | snapshots/**/*.snapshot.ts | 无 key 的录制会话回放 |
最有意思的是快照层。snapshots/ 目录的规矩写在它自己的 AGENTS.md 里:提交进仓库的 session JSONL「is replay input and expected persisted output」——同一份录制既是回放输入,又是期望输出。目录按 acp、sdk、session、web 分了四棵子树,每个场景一个目录(比如 snapshots/acp/escalation-approved)。录制需要 key(test:snapshot:record),回放不需要。
这意味着没有 API key 的贡献者和 CI 也能跑整条出厂 profile 的端到端回放。再回头看第二篇的「Model-visible ⟺ logged」:正因为模型看到的一切能从 session log 重建,一份 JSONL 才足以完整重放一个 turn。不变量在这里兑现成了测试基建。
另外还有三个 web 专用配置:功能、性能、压力,各跑各的。
Agent Notes:非平凡变更必须留下决策记录
规约原文:「Non-trivial changes MUST include an Agent Note in the same PR; only mechanical/local edits are exempt.」每个有分量的改动,必须在同一个 PR 里附带一份决策记录,存在 .agents/notes/。
路径本身就是元数据:{lifecycle}/{class}/yyyy-mm-dd-topic-title.md。lifecycle 三态——proposed(提案)、implemented(已落地)、rejected(被否决);class 是封闭集合:feature、bug-fix、simplification、architecture、process、testing,定义在 scripts/agent-note-tree.ts 里,不允许自创分类。
两个细节我喜欢。一是 implemented 的 note 用现在时描述「已落地的现实」,不写「应该怎样」和迁移计划——决策落地后,文档说的是现状。二是 archived/ 树是冻结的:归档的 note 不许编辑、不许当作当前依据,verify-archived-agent-notes 会检查封闭类树、文件三件套、sidecar 哈希和只追加的清单。历史可以查,但不能被篡改。
双语文档靠 hash 管同步
仓库所有重要文档都是中英双语。同步不靠自觉,靠每个双语 md 旁边的一份 .i18n.yaml,记录两侧「上次确认一致时」的 git blob hash:
# README.i18n.yaml
README.md: 9f89db3d4502dea4a0d181799164f304d4976740
README.zh.md: aa66ef1d24a5e2165859e9337273d807dff3d070
两种语言同等权威。改了任何一侧,必须带上另一侧,然后重新记录:pnpm run verify-translation-pairing --write README.md。hash 不一致而没重新确认的,门禁不放行。
文档里的代码块必须能编译
技术文档里粘贴的代码最大的问题是悄悄过期。dsh 的规定:文档里的 ```ts 代码块必须通过类型检查;从源码粘贴的类型声明标成 ```ts type-equiv 并登记进 manifest,verify-type-equiv 负责抓漂移。另外段落一律一个物理行一行(编辑器软换行),verify-md-wrap 盯着。
远程接口也吃同一套类型纪律:packages/typert/(generator / loader / protocol / registry 四件套)在构建期把源码类型声明转成编译器无关的模型和运行时产物,客户端代码调用 Host 能力时拿到的是类型化方法,不用手写 wire 代码;packages/api/ 是远程 BFF 层,gateway 在 Client 和 Host 之间搬运这些调用。
收尾
四篇读下来,dsh 给我的整体印象不是某个聪明点子,而是一致性:可逆注册、只追加日志、三角色接缝、逐文件覆盖率、冻结的归档——全是同一种态度,把「会悄悄腐烂的东西」变成「机器能检查的东西」。它还在 alpha,API 随时会变,但门禁先于功能建好这件事,本身就值得抄。