跳到主要内容

ADR 0029 — 文档的持续同步

背景​

仓库到达 v0.1.0 时有 28 份 ADR、一份内容丰富的 CLAUDE.md 和六份 runbook——却没有任何一份解释整个系统的文档。这不是缺少写作:而是整体性的文档不会从单点的决定中产生,而 ADR 替代不了地图。

文档使命产出了这一整套。接下来的问题比第一个更难:文档不是因为缺少初次编写而死,而是死于漂移。代码在变,文档留着,总有一天它描述的是一个已经不存在的系统。从那以后,没人再相信任何页面,包括正确的那些。

初步调查测量了这个仓库中风险的大小:

  • 一个月内 96 次提交,其中 apps/api/src/db/schema.ts 被修改 23 次——项目中最易变的文件;
  • 89 个环境变量、64 个事件标识符、110 条 HTTP 路由和 13 种动作类型,在代码中全都是自由字符串,没有任何类型联合迫使人去更新某个列表;
  • 一份已有的 .docmap.yml,其中有八个 glob 指向不存在的路径(apps/api/drizzle/**、k8s/**、helm/**),继承自之前的目录结构。

最后一项最有教育意义。一个什么都匹配不到的 glob 不会失败——它只是永远不触发。规则存在于文件中,给人覆盖的印象,却什么都不保护。一张没有验证的责任地图,会像它本应保护的文档一样静默地退化。

决策​

安装机制,而不是良好的意愿,分三个可靠性递减的层级:生成 > 验证 > 提醒。

1. 生成,在列表就是内容的地方​

docs/reference/scripts.md 完全由 pnpm docs:generate 产出,从 package.json 和 Makefile 中带注解的目标提取。没有要保留的散文,所以没有理由让人手工维护这个列表。

2. 验证,在散文比列表更有价值的地方​

configuration.md 和 events.md 是手写的——“出错时会怎样”一列和“这个事件何时发生”才是真正的价值,没有脚本能写出这些。但列表必须完整。

解决方案是手写文件中位于 <!-- BEGIN:GENERATED:<id> --> 和 <!-- END:GENERATED:<id> --> 之间的一个区块。区块是机械的清单;周围的文字是解释。清单标记出代码中存在而散文中没有描述的内容。

第一次运行就发现了两种真实的事件类型——agent.response 和 tool.result,后者由钩子 Engine.Harness.Hooks.EventLog 发出——手工提取漏掉了它们。

3. 提醒,在需要人类判断的地方​

docs/.docmap.yml 把代码路径与依赖它们的文档关联起来,严重级别为 block 或 warn。一个脚本把 PR 的 diff 与地图交叉对照,追讨没有更新的内容。

其中的四个决定:

地图在 CI 中被验证。 死 glob 会失败,先于任何其他验证。坏掉的地图会让其他一切都说谎。

逃生口是必需的。 标签 docs-not-needed 或 PR 正文中的 docs-not-needed: <motivo> 一行可以放行检查。没有合法的出口,形成的习惯就是绕过——为了让闸门通过而对文档做一次装饰性提交——然后机制开始说谎,这比不存在更糟。

误报是缺陷,而不是可接受的噪声。 变量验证器的第一个版本指控了七项,而它们以 POSTGRES_HOST / _USER / _PASSWORD 这种缩写形式被记录着。验证器被教会了展开缩写。一个会出错的检查会训练读者忽视它。

站点的构建是最便宜的闸门。 onBrokenLinks、onBrokenAnchors 和 onBrokenMarkdownLinks 设为 throw:移动一个文件而不修正指向它的地方,会让 CI 失败,而不是在生产中变成 404。

4. 定期审计,针对 PR 抓不到的东西​

漂移检查抓住在某个 PR 中变错的文档。没人碰过而变旧的文档什么也不会触发。每月一次的审计报告:对应代码之后发生了变化的停滞页面、待处理的 TODO(humano)、无法解析的 arquivo:linha 引用,以及处于 proposed 超过 60 天的 ADR——总是在同一个 issue 中更新。每月新开一个 issue 会变成垃圾信息,而垃圾信息会被关掉。

5. 单一来源,站点只读取​

Markdown 存在于根目录的 docs/ 中。website/ 中的 Docusaurus 站点通过 path: '../docs' 从那里读取,绝不存在 website/docs/。重复的内容就是会分歧的内容。

后果​

PR 变得更贵。 触碰 apps/api/src/domain/** 而不更新 business-rules.md 会失败。这是有意的代价:替代方案是文档腐烂,而那个代价会在几个月后出现,分散且更大。

地图需要维护。 重命名的目录会留下死 glob——现在 CI 会指出,但总得有人修正。这是新的、小的、可见的工作,换掉旧的、大的、看不见的工作。

提醒有时会出错。 地图按文件路径工作,而不是按语义:一次重命名内部变量的重构会触发 dominio-e-regras,而没有改变任何规则。逃生口就是为此而存在的。如果同一条规则在同一周内被豁免三次,问题在于规则:收窄 glob 或降低严重级别。

机制不验证文字是否正确。 它验证的是代码改变时文字是否被审阅过,以及列表是否完整。一个没人碰过的事实错误的句子会通过所有检查。为此有人工阅读以及斜杠命令 /sync-docs。

GitHub Pages 需要公开仓库或 Enterprise 套餐。 只要仓库是私有的,站点的部署就会失败。PR 中的构建作业——正是它承担闸门的价值——在两种情况下都能工作。

根目录中两个新依赖,yaml 和 picomatch,都是开发依赖,并在文档工具的预先批准例外范围内。

整个机制在 docs/explanation/documentation-workflow.md 中有解释,包括当它不公正地抱怨时该怎么做。没人理解的机制,就是会被人关掉的机制。