附录 A — 规则全量表

二十三条架构规则的全量清单,排序不按字母,按”值得先抄哪条”。 需要先说明的是:这张表的正确用法不是从上往下抄, 小节 A.12 会给出用法,而中间这几节解释的是 为什么这些规则长成了现在这个样子—— 那部分比清单本身可迁移得多。

A.1 先看一个统计

按实现方式分,二十三条规则分成两类:九条是文本模式匹配, 十四条是语义分析,而每一类语义规则各自是一个独立的分析器 —— 服务查找归属、访问器契约、可选用法契约、兜底契约、目标分离、 单例绕过、界面目标粒度、本地化表归属、依赖规则、注释代码、 文件健康度、测试宿主契约,等等。

超过一半的规则不是”grep 一个字符串”,而这个比例本身就是一条信息: 当你认真对付一类问题时,文本匹配很快就不够用了 —— 小节 13.5 里那个五层过滤的例子说明了这个过程是怎么发生的。 档位上的分布是二十条拦截、三条报数。

A.2 第一梯队:任何团队都该有,零基建

这五条不依赖任何构建系统,用一个脚本加一份配置就能实现。 它们的共同点是判据都落在单个文件甚至单行文本上 —— 不需要知道这个文件属于哪一层,也不需要知道谁依赖谁。

规则 它守什么 换来的可测性
无条件跳过检测 跳过语句必须有真实的运行时理由 覆盖不会被静默删除
注释掉的代码 代码即唯一事实源,历史归版本控制 读代码时不必判断哪段是活的
单一日志 owner 日志只在一个文件里定义 日志配置有唯一入口,可改可测
文件健康度 按变更原因拆,行数是启发式上限 单个文件的变更原因唯一
重试点击辅助函数 禁止”点到成功为止” 一次意图只提交一次

第一条和第五条是专门用来防”测试看起来通过了”的(小节 12.10), 而它们值得排在最前面,因为假绿比没有测试更危险—— 没有测试的时候你知道自己没有保护,而假绿会让你以为自己有。

A.3 第二梯队:有共享层之后

这一梯队需要一个”共享层 / 产品层”的划分,但不需要构建图。 也就是说,你得能从路径判断一个文件属于哪一边, 而这个能力靠目录约定就能提供(小节 6.14.1)。

规则 它守什么 换来的可测性
协议优先查找 只在协议层顶层做服务查找 契约不被绕过
可选访问器契约 返回可选的访问器不许在未注册时崩 调用方的可选分支是真的
禁止服务兜底 没有不经过正常装配的隐藏路径 行为唯一,可断言
协议与实现目标分离 契约和实现不编在同一个目标 分层不是伪边界
具体单例绕过 消费者不许直接用具体实现的单例 生命周期归微内核
产品互不依赖 跨产品复用必须下沉共享层 产品可独立构建与测试
本地化表归属 只有一个消费者的文案不放共享表 改一句文案不动全产品
权益单一 owner 权益查询收口到一个服务 不存在第二个真相源

“禁止服务兜底”是这一梯队里最值钱的一条,而它也是实现最复杂的一条 (小节 13.5)—— 这两件事不是巧合: 一条规则的价值和它的实现难度经常正相关, 因为容易表达的约束通常也是容易遵守的约束。

A.4 第三梯队:需要构建依赖图

第三梯队的六条各自守着构建图上的一条性质: 界面目标粒度守的是页面和组件按功能拆成独立目标; 部署件归属守的是兄弟部署件不许直连、只经共享层; 测试宿主契约守的是有界面测试就必须有可执行宿主; 测试 bundle 经包装器守的是名字由包装器推出、不由调用方起; 发布走账本守的是发布流水线不许绕过账本直接触发; 构建工具不依赖客户端守的是构建期工具与客户端图隔离。

这六条的共同前提是你的构建系统能被查询—— 如果你的构建是一堆脚本,这六条一条都实现不了, 而那本身就是一个值得先解决的问题。

A.5 三条报数模式的

规则 现状 为什么还没拦
本地化表归属 966 处违规 存量太大,正在收敛
产品互不依赖 零违规 已经干净,随时可切
音频会话单一写者 零违规 产品域规则,观察期

第二行和第三行值得注意:它们已经零违规了,却还在报数模式。 这说明一件事 —— 从”零违规”到”切成拦截”之间还差一个决定, 而这个决定没有 owner(小节 15.7)。 小节 A.10 会算清楚这个状态到底损失了什么。

A.6 每条规则必须有的四个字段

不管是哪一类,四个字段是强制的,而每一个都对应一种具体的失效。 incident 记的是产生它的那次失败,缺了它半年后没人能判断它该不该留; fix_hint 记的是怎么修,缺了它 Agent 会换个写法再撞一次; sentinel_min 记的是至少应该看到多少个事实, 缺了它规则坏了会表现成通过;档位记的是拦截还是报数, 缺了它上线即停摆。

第三个字段在实现上是必填且类型上不能为零(小节 13.4.3), 也就是说你没法写一条不带自检的规则。 这四个字段合起来其实是一个很小的要求 —— 写一条规则多花五分钟 —— 而它们防住的是这一章末尾那张表里列的七种死法。

A.7 语义规则和文本规则的分界线在哪

十四条语义规则、九条文本规则,这个分界线不是随意的, 它有一个清楚的判据:这条规则要判断的东西, 能不能从单行文本读出来?能就是文本模式够用,不能就需要语义分析。

三个例子摆在一起最清楚。禁止内联日志器只需要知道 “这一行有没有那个构造调用”,单行够;禁止无条件跳过只需要知道 “这一行的参数是不是常量”,单行够;而禁止服务兜底需要知道 “这个 ?? 左边的值是从哪来的”—— 单行不够。

第三条实际上需要回答四个问题:这个变量是不是从一个已知可选的访问器来的? 它有没有被局部变量遮蔽?这个 ?? 是不是在同一条语句里? 右值是不是那个被明确允许的”不可用”?四个问题, 全都超出了单行的范围,而这就是那个五层过滤器存在的理由。

这个分界线有一个很实用的推论:当你发现自己在给一条文本规则 加第三个例外时,它可能已经该变成语义规则了。 因为例外的数量正是”单行信息不够”的症状 —— 每一个例外都是在用一个笨拙的方式补充上下文, 而当你补到第三次的时候,直接去拿真正的上下文会更便宜。

A.8 规则的可移植性

这张表里有多少条能直接搬到别的仓库?粗估一下: 无条件跳过、注释代码、文件健康度这三条可移植性高, 概念和实现都通用;单一日志 owner 和协议优先中等, 概念通用但实现绑定语言;服务兜底和访问器契约, 绑定这套微内核架构;目标粒度和部署件归属也,绑定构建系统。

高可移植的只有三到四条。 这个估计比”抄这二十三条”有用得多, 因为它把注意力引向了真正可移植的东西 —— 不是规则,是方法。

方法有五条:怎么发现该有哪条规则(小节 15.5)、 怎么调它的边界(小节 14.4)、 怎么让它上线而不停摆(小节 15.3)、 怎么让它自检(小节 13.4.3)、 怎么判断它该退休(小节 15.7)。 这五条方法,可移植性是百分之百—— 它们不依赖语言、不依赖构建系统、也不依赖仓库规模。

A.9 二十三条规则的分布说明了什么

把这二十三条按”它守什么”归类,会看到一个不均匀的分布: 八条守单一 owner 与写入权,四条守测试的可信度, 四条守分层与依赖方向,三条守目标粒度与资源, 两条守代码即事实源,两条是产品域特定的。

第一类占了三分之一以上。 而加上路径不变量那二十份里的九份 (小节 14.5),整个规则体系里 超过 40% 在回答同一个问题:这块状态归谁写。

这个分布不是设计出来的,是撞出来的(小节 15.11)—— 也就是说,在这个系统撞过的所有墙里, “同一份状态有两个写者”占了将近一半。 这个数字给形状 B(小节 4.3)提供了一个量化的支撑, 而它对读者的用处很直接:如果你只能防一个形状,防这个。

A.9.1 第二类也值得注意

四条规则守的是”测试的可信度”,而不是”测试的覆盖”—— 无条件跳过、重试点击辅助函数、界面测试必须有可执行宿主、 测试 bundle 经包装器。没有一条是”必须写测试” 或”覆盖率必须达标”。

这个选择很能说明这套系统的判断:覆盖率这类指标 可以用别的方式保证(流程、评审、习惯), 而”绿灯可不可信”必须有独立的守卫。 理由在失效的可见性上 —— 覆盖率失效时会被发现,因为数字会掉; 而绿灯的可信度失效时不会(小节 12.3), 它表现为一片安静的绿色。

A.10 “值不值”怎么算

和路径清单那个算法(小节 14.14)类似,但变量不同: 收益约等于违规发生的频率乘以每次违规漏进主干的代价, 成本约等于实现加调参加持续的误报打断。

这个算式给出了一个反直觉的结论:一条从来不报违规的规则, 收益是零。 不管它守的东西多重要 —— 如果没有人试图违反它, 那么它没有拦住任何东西。

这就是 小节 A.5 里那两条零违规的报数规则 处境尴尬的原因:它们既不拦人(无保护),又没有违规(无收益), 只是每次多跑一遍扫描。正确的处理不是删掉它们 —— 因为”现在没人违反”不等于”以后没人违反”—— 正确的处理是切成拦截:成本仍然接近零(不会误报,因为没有违规), 而它从此提供了保护。

一条零违规的报数规则,是唯一一种可以零成本切成拦截的规则。 这套系统里有两条这样的规则,都还没切 —— 这是那个”没有 owner 的决定”最具体的表现。

A.11 三个梯队之间的顺序为什么不能跳

三个梯队按基建要求排,而它们同时也是一个推荐的实施顺序 —— 理由不只是”先做便宜的”。

第一梯队守的是判定本身的可信度小节 12.10)。 在这五条建成之前,后面任何一条规则的绿色都是没有意义的: 你不知道那条规则是真的没有违规,还是它的扫描面已经缩到了零。 先建可信度,再建覆盖面—— 这和 小节 16.14.4 是同一条,只不过那里说的是检查, 这里说的是规则。

第二梯队守的是分层,而它有一个前提: 你的仓库里得先有一条清晰的层。 一条”产品互不依赖”的规则,在一个还没有区分产品和共享层的仓库里 写不出来 —— 不是难写,是没有一个可靠的方式判断 一个文件属于哪一层(小节 6.14.1)。 所以第二梯队的真正前提不是技术,是结构。

第三梯队守的是构建图上的性质,前提最硬: 你的构建系统得能被查询。这一条是全书唯一一处 可能需要重基建的地方(小节 10.16), 所以它排在最后不是因为它不重要, 是因为它是唯一一个可以正当地被无限期推迟的梯队。

三个梯队的顺序还有一层含义:它们的规则数量应该是递减的。 如果你的第三梯队比第一梯队条数还多, 那通常说明规则集是从一个”我们的构建系统能查什么”出发写的, 而不是从”我们撞过什么”出发写的 —— 而前者正是 小节 15.11 那张表里误报率最高的那一类。

A.12 怎么用这张表

不要从上往下抄。 正确的用法是读一遍,然后合上书, 写下你团队在评审里重复说得最多的那三句话。 如果其中有一条和这张表里的某条重合, 那说明你已经”挣得”了那条规则,可以直接开始实现。

如果一条都不重合,那就实现你自己那三条,别管这张表 —— 小节 15.5 讲过为什么。 这张表最大的用处不是提供答案,是提供一个对照: 让你看到一套长了半年多的规则集大概长什么样、 密度是多少、分布是什么形状。

A.13 规则自查表

对你自己的每一条规则,逐项打勾:

□ 我能说出它诞生于哪次具体的失败
□ 它有 incident 字段(或等价的"为什么有这条")
□ 它有 fix_hint 字段(或等价的"怎么修")
□ 它有哨兵下限,且哨兵不能被关掉
□ 它上线时走了报数模式
□ 它的误报率我知道是多少
□ 它有一个有名字的 owner
□ 我知道它变成结构之后会是什么样

八项,而大部分规则集能打勾的不超过三项。 但这八项的价值不在于”全打勾才合格”—— 在于每一个没打勾的项,都对应一种具体的死法: 说不出诞生原因,半年后没人能判断它该不该留; 没有 fix_hint,Agent 换个写法再撞一次; 没有哨兵,规则坏了会表现成通过; 没走报数模式,上线即停摆然后被关掉; 不知道误报率,被绕过而你不知道; 没有 owner,没人判断它该不该退休; 不知道结构化的终点,规则集单调增长。

七种死法,而 小节 15.9 讲了其中三种的检测方式—— 剩下四种目前只能靠人定期去看,而这本身就是一个待补的缺口。