附录 C — 一份常驻文件的逐行注解
一份 153 行的常驻文件,管 3,104,960 行代码,比例约 1:20,000。 这份附录不复述那份文件,而是挑几条做深度注解 —— 因为它们的写法比它们的内容更值得学。 你抄不走那份文件的内容(它是那个仓库的), 但你可以抄走它每一条规则的形状。
C.1 注解一:一条禁令的完整形态
绝不用原地批量替换修改文件。 一个为你推理过的那些调用点写的模式,也会重写你没有推理过的: 已经应用过的 SQL 迁移、生成的锁文件、fixture 与 golden 数据、 设计文档、CI 配置。损坏是静默的 —— 构建照样通过, diff 大到没法逐行看,而一个被重写的迁移在已经跑过它的数据库里无法撤销。 用版本控制的移动命令移动文件,然后搜索剩余引用,逐个打开、逐个改。 一次跨几百个文件的重命名也是这么做:先枚举全部命中, 提交前读一遍每一个非机械文件的 diff。
这一条由六个部分组成,缺一不可:第一句是禁令本身; “也会重写你没有推理过的”给出失败机制,带五个具体的受害者; “损坏是静默的”说明为什么这个失败特别危险; “一个被重写的迁移无法撤销”给出不可逆性; “用移动命令逐个改”给出替代方案; 而最后一句”一次跨几百个文件的重命名也是这么做” 堵住了”但我这次量很大”这个例外。
大部分团队的规范只写了第一部分。 而最后一部分尤其值得学, 因为它预判了这条规则最可能被绕过的理由 —— “我这次要改三百个文件,逐个改不现实”是每个人 (和每个 Agent)第一个会想到的例外,而规则提前回答了它。 一条不预判自己例外的规则,会在第一次遇到例外时被绕过, 而那一次绕过会变成先例。
C.2 注解二:一条带验证动作的规则
绝不对仓库根目录跑递归命令。 兄弟工作区就住在树内部,所以递归搜索和从它们管道出来的一切 都会走进每一个其它检出 —— 一次写入会同时到达几十个不相干的 worktree, 在你从没看过的分支下。把每一次递归读取和每一次批量编辑 限定到任务拥有的具体目录,并在动作之前确认匹配列表里没有工作区路径。
最后半句是关键:它给了一个可执行的验证步骤。 对比”请小心使用递归命令”—— 后者对 Agent 等于不存在, 因为”小心”不是一个可以执行的动作。
一条规则如果只说”要小心”,它不是规则,是祈祷。 判断一条规则有没有跨过这条线,有一个很简单的测试: 把它交给一个完全不理解上下文的执行者,他能不能照做? “确认匹配列表里没有工作区路径”能,“小心”不能。
C.3 注解三:一条规则给了三个判例
目录可选层那条(小节 6.2)不只是说”每层必须被挣得”, 而是紧接着给了三个真实路径:双平台的产品长什么样、 多进程的产品长什么样、单平台单进程的产品长什么样。
判例的作用是让规则可以被机械地应用。 一个 Agent 遇到一个新产品时,不需要理解”挣得”这个概念的哲学含义, 它只需要把当前情况和三个判例比对。 规则加判例,比规则加解释更管用—— 解释需要被理解才能应用,判例只需要被比对。
C.4 注解四:预判 Agent 会怎么想错
文件里有几句不是在描述架构,是在描述 Agent 的失败方式。
“把评审和追问当成提高抽象层级的信号,而不是打点修复的请求”—— Agent 收到”这里有问题”时的默认反应是修那个点, 而这句话改变的是它对反馈的解读方式。 “不要在一个重复的机制之上打磨业务 bug”—— 它预判的是 Agent 会在一个本该被上移的重复机制上, 一次次地修表面症状。 “有’页面’这个词、或者能被展示,都不足以让它成为一个页面”—— 它预判的是 Agent 会按名字和直觉分类,而不是按获取方式分类。
这三句的共同点是它们是观察的产物,不是设计的产物—— 写这三句的人看着 Agent 在这些地方失败过。 一份好的常驻文件因此不可能被一次写成: 它的前半部分(架构、禁令)可以设计出来, 而这一类只能被观察出来。
C.5 注解五:一条规则的压缩
常驻文件的克制不是靠删规则达到的,是靠压缩。 看这条关于共享基础设施的规则:
实现任何能力之前,先勘察共享基础设施:搜索共享层的全部子树、 相关平台的子树,以及所属部署件的共享包。找找有没有现成的机制 提供了全部或大部分能力(提示、存储、重试、队列、窗口、主题、密钥……)。 默认复用或扩展现有基础设施,充分利用它优先于写新代码。 重新实现一个共享层已经提供的能力是一个缺陷。 只有在确认没有匹配之后才动手建。
它压缩掉了四样东西:共享层有哪些子树被压成”搜索全部子树”, 有哪些现成机制被压成括号里的七个例子, 怎么判断”匹配”被压成”全部或大部分能力”, 而违反的后果被压成了“是一个缺陷”这四个字。
最后一行是压缩率最高的部分。”重新实现一个共享层 已经提供的能力是一个缺陷”这句话, 把一个价值判断(“我们希望大家复用”) 变成了一个分类判断(“这属于缺陷”)。 这个转换有实际效果:一个缺陷会被修,一个”希望”不会—— 它把这条规则接进了已有的处理流, 而这正是 小节 19.5 讲的那条原则。
C.6 注解六:那些没有出现在这份文件里的东西
一份 153 行的文件,同样重要的是它不包含什么。 对照一份典型的团队规范,下面这些通常会有而这里没有: 代码风格(缩进、命名格式)交给了格式化工具、零人工; 提交信息格式和分支命名规范都没有强制, 因为工作区名本身是生成的;各种”最佳实践”清单 要么进了结构,要么不存在;技术选型指南进了按需查阅的说明; 目录清单和模块索引在文件后半部分有,但只是引用性质。
前三项的共同点是它们都可以被工具无成本地保证, 所以不需要占用注意力。而”最佳实践”那一项是最重要的: 它们之所以不在,是因为它们无法通过 小节 8.3.1 的第二条测试 —— 写不出具体的失败形态。 一条写不出失败形态的”最佳实践”,本质上是品味 (小节 7.5.1 讲过怎么对待品味), 把品味写进常驻文件的代价是它会稀释旁边那些真规则。
C.7 注解七:文件的后半部分是一份地图
那 153 行不全是规则,后半部分是一份目录地图—— 哪个顶层目录放什么、文档该归到哪。 这部分值得单独说,因为它示范了一个取舍: 地图信息该不该占用常驻文件的位置?
按 小节 8.3.1 那三条判据,答案应该是”不该”—— 地图可以被 Agent 自己看目录得到,它不需要被”记住”。 它仍然在那里,理由是目录名不等于目录的用途。
看到 Docs/ 这个名字,Agent 会假设”文档放这里”, 而这套系统的实际规则是相反的:一份文档由它描述的东西拥有, 不由一个 Docs/ 桶拥有,Docs/ 只剩两类, 而且两类都不是工程材料的家。这条规则必须常驻, 因为它推翻的是一个非常强的先验(小节 8.11 的第二个机制)。
这给出了一条判据的补充:当一个结构的名字会误导 Agent 时, 纠正它值得占用常驻位置。反过来说, 一个名字已经准确传达了用途的目录,不需要出现在这份文件里 —— 而这条补充规则能把一份”目录说明”从三十行压到五行。
C.8 注解八:这份文件的自我约束
最后一处值得注意的:这份文件对自己也有约束。 配套的说明里写着,必须无条件触发的规则要留在这份文件本体, 不能挪进按需发现的地方(小节 8.2.1)。
这是一条关于”什么该放进这份文件”的规则, 而它本身就放在这个体系里。它的效果是: 当有人想把一条规则挪出去以缩短文件时,这条约束会先拦住他—— 而这正是常驻文件最常见的一种腐化方式, 方向恰好和 小节 8.9 讲的相反:不是为了补漏而变长, 是为了瘦身而把无条件规则挪走。
一个体系如果不能约束自己的演化方式,它会在维护中漂移。 这条自我约束的成本是一句话,收益是防住了一整类错误的重构。
C.9 常驻文件的健康指标
给三个可以定期自查的数:行数与代码行数之比越小越好 (这里是 1:20,000,对应的是稀释效应); 写不出失败形态的条目占比应该为零,因为它们不是规则; 能写进结构却没写的条目占比越小越好, 因为每一条都是一次错过的前馈。
第三个指标最难测,但它可以被近似: 过去半年里,有几条规则从这份文件里被删除, 因为它对应的问题已经在结构上消失了? 如果是零,那说明”把规则变成结构”这条路径没有在走 —— 小节 15.10 讲过同样的判据,只是那里的对象是规则集。 两处用同一条判据不是巧合:任何一种”承载约束的东西”, 健康的标志都是有进有出。
C.10 从这份文件能学到的三条
长度是一个设计约束,不是一个结果。 153 行不是 “写着写着就这么长了”,是被守住的 —— 而守住它的方式是每加一条,先问那三条判据。
每一条规则的形状是固定的。 禁令加失败机制, 加不可逆性(如果有),加替代方案,加例外的堵截。 固定的形状让规则可以被快速扫描,也让缺失的部分一眼看出来 —— 你不需要读懂一条规则就能看出它少了失败形态。
它预判了读者会怎么想错。 小节 C.4 里那三句不描述系统,描述的是 Agent 的失败方式。 这一条是最难做到的,因为它需要观察: 你得先看着 Agent 在某个地方失败很多次,才能写出那一句预判。 所以这份文件的质量取决于你观察了多久,而不取决于你想得多周全 —— 这也是它没法被一次写完的原因(小节 C.4 那三句就是例子)。
C.11 注解九:一条规则里最贵的是那个否定
回头看这份文件里的禁令,会发现一个共同点: 每一条都在禁止一个”更省事”的做法。
批量原地替换比逐个打开省事。对仓库根跑递归命令比 逐个指定目录省事。给一个缺失的服务加兜底比 去修组合根省事。这不是巧合 —— 一条禁止”更麻烦的做法”的规则不需要存在, 因为没有人会去做它。
这个观察有一个实际用处:当你在写一条禁令时, 先确认它禁的那个做法确实更省事。如果不是, 那么这条规则大概率在解决一个不存在的问题, 它会占用注意力却从不命中(小节 A.10 那个 “从来不报违规的规则收益是零”)。
它还有一个更实用的推论:一条禁令必须给出替代方案, 而那个替代方案必然比被禁的做法麻烦。 所以只写”不许 X”是不够的 —— 读者会问”那我怎么办”, 而如果规则不回答,他会自己找一个, 而他找到的那个多半是 X 的一个变体。
这也是 小节 C.1 那条禁令为什么要用六个部分 才说得完的原因:它禁掉的是所有人的第一反应, 所以它必须同时给出理由、后果、替代方案, 以及对”但我这次量很大”的回答。 规则的长度应该和它禁掉的那个做法有多诱人成正比。
C.12 怎么用这份附录
打开你自己的常驻文件,对每一条问三个问题。
它有失败形态吗? 没有的话补上,补不出来的删掉。 它有可执行的验证动作吗? 如果它说的是”注意”“小心”“尽量”, 它不是一条规则。它能被写进结构吗? 能的话写进去, 然后从这份文件里删掉 —— 这一条通常能砍掉最多。
三个问题过一遍,大部分团队的常驻文件会掉下去一半以上, 而剩下的那一半,每一条的到达概率都提高了。 这个提高不是心理作用:注意力是一个总量固定的东西, 删掉一半内容,剩下每一条分到的份额就翻倍了。
C.13 常驻文件的最小版本
给一个可以直接抄的骨架,适用于任何规模:
# 仓库指导
## 动手原则
- 改代码前,先问这次改动涉及哪个不变量、它的 owner 是谁。
在 owner 那里修,不在症状点打补丁。
- 实现任何能力之前,先搜共享层有没有现成的。
重新实现共享层已经提供的东西是一个缺陷。
## 不许做的事(每条带失败形态)
- 绝不批量原地替换文件。[失败形态:...]
- 绝不对仓库根跑递归命令。[失败形态:...]
- [你自己的第三条]
## 结构
- [三到五行,说明目录怎么组织,以及一条判据]
## 验证
- 改完必须跑 [你的检查命令],不通过不算写完。
- 退出码 2 表示环境问题,不要改代码。
- 修 bug 时,先让新测试红一次再让它绿。大概 30 行,而它覆盖了书里成本最低、收益最高的那几条。
C.13.1 为什么这个骨架长这样
四个小节,各对应一件事。“动手原则”在这里, 是因为它们无法被结构表达 —— 小节 8.3.1 的第三条测试没通过,所以必须常驻。 “不许做的事”在这里,是因为每一条都是不可逆或静默的 (第二条测试),所以必须在落笔前在场。 “结构”只有三到五行,因为大部分结构信息由目录自己传递 (小节 6.20),这里只写目录名传递不了的那部分。 “验证”在这里,是因为它们是判定层和 Agent 之间的接口, 而这个接口必须无条件生效(小节 11.8.0.1)。
四个小节,而没有”代码风格”那一节 —— 因为那些交给格式化工具,零人工(小节 C.6)。 这个骨架的行数分配本身也是一条信息: 禁令和验证占了大半,而描述性的内容只占五行, 因为描述性的内容是这四类里唯一一类 Agent 可以自己查到的。