


在大型代码库中修复根本原因,并保持公共 API 兼容
让 AI 修改大型分层代码库时使用。它会精简构建日志,把 bug 追溯到上游原因再修,检查自己的改动里有没有只为掩盖问题的辅助函数,并写出清楚的拉取请求。
要使用这个AI规则
## 精简构建日志,提交前运行格式化工具和测试
- 构建时,将所有构建输出重定向到空设备,只有在构建失败时才以详细模式重新运行同一条命令,这样成功的构建就不会把token耗费在日志上。例如:`build-command >/dev/null 2>&1 || build-command`。
- 优先使用本地缓存的依赖,而不是从网络重新拉取;如果可用,启用构建或编译器缓存以加快增量重建。
- 对于长时间无人值守或由智能体驱动的构建,在配置阶段就禁用模态中止或断言弹窗,避免运行过程卡在等待人工响应上。
- 每次提交前运行格式化工具,并使用其仅检查模式来验证而不修改文件;不符合项目风格的代码会造成本可避免的评审往返。
- 从代码仓库根目录运行测试套件,并将回归测试作为文件添加到项目的测试目录下。当没有GPU或特殊硬件可用时,改用CPU或解释器测试模式,而不是跳过覆盖率。
- 在跨Shell环境中,例如由Unix Shell调用宿主工具时,附加正确的可执行文件扩展名,以确保运行的是预期的宿主二进制文件,而不是错误平台的版本。
## 不用防护代码掩盖,去修上游的真正原因
- 追求从构造上就正确且健壮的表示形式,即使这意味着比可能的最小差异更大的重构。
- 修复根本原因,而非表面症状。当bug出现在输出生成这类后期阶段时,原因通常在上游:更早的转换、合法化、特化、降级,或数据表示本身。要追溯到那里并在那里修复。
- 在保留每一处改动前先质问它:为什么需要它,缺少它哪个测试会失败,这是不是正确的修复,还是问题本身在提示表示方式有缺陷?如果说不出缺少这处改动会导致哪个测试失败,它大概就不该存在。
- 不要掩盖问题。用来掩盖畸形结构的保护判断、空值检查或特殊情况,隐藏的是表示层面的缺陷,而在正确输入下从未触发的保护判断就是死代码。让表示本身正确,下游消费者才能保持简单。
- 审视输入的形状。每当处理某种特定形状的输入时,都要问这个形状本身是否正确,还是应该修复其上游生产者;只有在这个形状确实是合法输入时,才在本地处理它。
- 优先选择正确的表示,而不是最小编辑距离。如果两种表层形式在语义上应当等价,就用同样的方式对它们建模;对概念上无序的键值数据,应按键或角色访问,而不是按位置或索引访问。
- 在整个任务过程中持续记录工作日志:问题及其典型例子、遇到的阻碍、一个修复如何连锁引出下一个、最终选择的修复方案及其为何符合原则、以及被否决的备选方案。把它提炼进改动说明里,不要留在已提交的代码中。
## 检查改动中是否有只为掩盖糟糕设计的辅助函数
- 在敲定一处非平凡改动之前,检查这份diff是否存在弥补糟糕表示的迹象,并把以下模式当作危险信号,除非能证明它们确实应该出现在这一层。
- 自定义的语义等价性辅助函数,例如新写的递归判断"是否等价"或"是否匹配"的函数,往往意味着两种替代性的表示被允许同时存在;先要问清楚,为什么现有的规范化、替换或相等性判断路径没能让它们本就相同。
- 未经审查的辅助函数增殖:把每一个新增的辅助函数、回退分支或try式函数都当作审查对象,尤其是那些只为让某个失败测试通过而存在的,或者重新实现了已有机制一部分的函数。
- 从语义反向重建语法,即从已经校验过的语义数据重新构建表层语法,这通常意味着生产者保留了错误的表示形式;应当以校验过的语义值作为唯一可信来源。
- 通过图遍历重新发现上下文,即通过遍历操作数、引用或所有权链来找回参数、键或父节点,这通常属于下游修补;应当在生产者处直接构建规范形式。
- 消费端打补丁:后期阶段不应该修补来自早期阶段的畸形结构,而硬编码的表示细节(针对特定子类的特殊情况、魔术名称、参数索引或条目顺序)需要一个强不变量支撑,并且通常应当放在规范构造的边界处。
- 静默处理不可能出现的形状:对超出契约范围的形状返回默认值的保护判断会掩盖bug;应当对这种不可能的形状做断言,而不是悄悄接受它。
- 从"可疑辅助函数清单"开始审查:列出每个新增的辅助函数、它与哪个现有机制重叠、以及缺少它会导致哪个测试失败。对每一处被标记的改动,指出确切的输入形状及其生产者,判断这个形状是规范形式还是意外产生的替代形式,先尝试在生产者一侧修复,说清现有的可信来源是什么,并做"回退演练":移除该改动,运行最小的失败测试,追溯真正的破坏点。不要仅仅因为一处被标记的改动能让测试通过就保留它。
## 写带示例的注释,同一信息只放在一处
- 把函数注释写成完整的句子:先说明这个函数做什么,如果不是一目了然,再说明它为什么存在,对于非平凡的行为要给出具体例子,而不是写一句生硬的片段。
- 在注释和说明中使用对话式的例子。优先用"来看这个例子:"配上真实代码,然后逐步描述发生了什么、哪个处理阶段产生了这种形状、本地代码维护的是哪个不变量、又有哪个消费者依赖它,而不是用抽象标签来代替真正的解释。
- 先复用后编写,再把非平凡的逻辑提取成一个命名清晰、有文档说明的辅助函数。先搜索共享工具库;当逻辑确实是全新的,就给它一个能表达意图的名字和文档注释,而不是把一段多步骤的计算埋在内联代码块里。
- 保持唯一可信来源,重构后删除死代码:对同一件事情的映射或分类只在一个地方进行;当某处改动让一个分支或辅助函数变得不可达时,应当删除它而不是留在原地。
- 每个值只保留一种规范表示,并对这一不变量做断言。同一逻辑值的第二种表示会破坏相等性、同一性和去重;当某个不变量保证某种输入永远不会产生某种形式时,应在构造处对此做断言。
- 对不符合契约的输入要大声报错。当一个辅助函数只对有限的一组输入有效时,应对该范围之外的任何输入做断言,而不是悄悄返回一个默认值。
## 附上回归测试,按五个部分写拉取请求说明
- 先格式化代码,然后为这次改动打上标签:默认标记为非破坏性改动,只有在改变了二进制接口或语言行为时才标记为破坏性改动。
- 附上在修复之前会失败的回归测试。
- 按固定的五段式结构撰写描述:动机(问题及一个具体的引发场景)、提议的方案(采取的方法及其为何符合原则)、改动摘要(列出改动涉及的文件或区域及各自作用)、概念与术语(一份简短的术语表,只收录报告所依赖的、该代码库特有或不易理解的术语,不收录常识性基础概念)、以及一份过程报告。
- 在过程报告中,为每一处改动给出符合逻辑的理由。对于连锁引发的问题,要给出其引发场景,并给出点名具体涉及函数的代码追溯,而不仅仅是文字描述。对于任何针对某种输入形状做保护或特殊处理的改动,要说明这个形状究竟是合法输入,还是本该在其生产者处修复,好让评审者能确认这处修复放在了正确的层级上。
- 面向缺乏你这份上下文的评审者来写:从一个具体例子开始,附上相关代码片段而不是只提及类型或函数名,并按顺序说明步骤。
## 只在 enum 和接口末尾追加,不破坏公共 API 和 ABI
- 把对公共API头文件的每一处改动都当作需要为已经基于旧版本编译的调用方保持二进制和源码兼容性来对待。
- 枚举类型:绝不在现有枚举的中间插入新的枚举项,因为插入会使其后的每一个值都发生偏移,从而悄悄破坏那些存储或比较过该值的调用方。新增枚举项应紧接在终止计数或哨兵成员之前追加,并显式指定下一个连续的值;要废弃某个枚举项时,把它改名为"已移除"标记形式,同时保留其原始整数值,绝不重用已废弃的值。
- COM风格接口中的虚函数表,其布局由声明顺序固定,因此绝不要重新排序虚方法,绝不要更改某个方法的签名或调用约定,绝不要在中间插入新方法(应追加在结尾、闭合括号之前),也绝不要移除某个方法:应把它的实现替换成"未实现"的占位体,同时保留声明。
- 当客户端可能实现某个公共接口或按标识符查询它时,应避免就地扩展这个已有接口;优先添加一个拥有自己标识符的新派生接口或版本化接口,同时继续支持原接口,以兼容现有调用方。
