Skip to content
My Blog
Go back

逐文件 100% 覆盖:dsh 的质量门禁长什么样

前三篇讲了 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-catalogcordis.yml 合法性、插件/工具/配置目录与源码一致
文档verify-md-links、verify-md-wrap、verify-type-equiv、verify-doc-budgets链接与锚点、换行规则、文档里粘贴的类型与源码同步、字数上限
Agent Notesverify-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

文件模式特点
unittests/**/*.spec.ts常规单元测试
e2etests/**/*.e2e.ts打真实 API;没有 DEEPSEEK_API_KEY 时自动跳过
expectedtests/**/*.expected.e2e.ts进程级的期望输出,由各 owner 维护
snapshotsnapshots/**/*.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 随时会变,但门禁先于功能建好这件事,本身就值得抄。

本系列


Share this post:

Previous Post
第 05 课 · 会话日志与「模型可见 ⟺ 已记录」
Next Post
模型看到的必须在日志里:dsh 的 Turn 执行流