一次改动要过三层:工具、测试与评测、人看。前一层不过不进后一层——同一件事交给不同层去判,成本差两个数量级。
这一条要求,有没有一条命令能证明它被满足?谁在什么时候跑?
找不出命令的,先改写成可判断的写法;只能靠人的,写清由谁在哪一步看。写了「靠人」却没写名字和步骤的,按没做处理。
同一句「改好了」,落在不同层上成本差两个数量级。按改动落在哪一层选手段,不要一律交给最贵的那一层。
| 判断方式 | 谁来判 | 单次成本 | 置信度到哪 | 覆盖不到什么 |
|---|---|---|---|---|
| 工具判断:格式化、静态检查、类型、复杂度、密钥、依赖漏洞 | 本地跑一条入口命令 | 2–5 分钟 | 规则写死的接近 100% | 逻辑对不对、越权语义、答非所问 |
| 测试判断:单元与接口测试 | 测试命令 | 30 秒–3 分钟 | 已写进用例的行为高;没覆盖的为 0 | 界面文案、模型输出质量、性能 |
| 评测判断:固定问题集与判分规则 | 评测脚本,按样本条数计时 | 3–15 分钟 | 问题集内可复现;问题集外不可外推 | 视觉、交互、布局 |
| 人看:关键路径、改动意图、红线 | 另一名组员,指名到人 | 30–60 分钟 | 覆盖面最宽,复现性最差 | 批量回归;人换了就回到原点 |
顺序不能换:前一层不过不进后一层。2 分钟的工具能挡住的问题,别交给 60 分钟的人。反过来,工具报绿也不等于能跳过人看——「这个改动该不该做」只有人能判。
四类里只有评测判断不进那一条入口命令:单次 3–15 分钟且按量计费,只在改提示词、换模型、改问题集时单独跑。性能与容量也不落在四类里任何一类——它靠基准压测跟上次的基线比,判的是趋势不是对错。
一次门禁就是一条命令跑完八项,任一项非零即停在本地。下面是一次全绿的输出。
$ make check fmt ✓ 0 处 lint ✓ 0 处 types ✓ 0 处 unit ✓ 142 通过 / 0 跳过 新增行覆盖 91% api ✓ 9 通过 secrets ✓ 0 命中 complex ✓ 0 超限 policy ✓ 指令文件未被改动 → 全部通过,可推送
要认的是这个形状:八项一次跑完、只报通过与否、不看人怎么解释。任何一项变成「警告」「先合后修」,这一条命令就不再是判据。
$ make check fmt ✓ 0 处 lint ✓ 0 处 unit ✓ 138 通过 / 0 跳过 complex ✓ 0 超限 → 全部通过,可推送 (八项这次只出现四项:api、secrets、policy 没跑)
认它的办法:先看项数,再看勾。八项变四项、或某一项从「非零即停」改成「警告」,比某项红了更危险——红了会被看见,少了不会。入口命令里出现 || true、--no-verify,或某一项被整段删掉,都是同一个动作的不同写法。
场景:助手给「按租户导出」写完实现,人直接提了合并请求,说明里只写了「实现导出」。
门禁是唯一不依赖人盯着的约束:人可以不看,但它每次都在。检查名可以点开,进到那一项的说明。
| 检查 | 挡住什么 | 失败时 |
|---|---|---|
| 格式化 | 空格、换行、import 顺序 | 自动修,不值得讨论 |
| 静态检查 | 可疑写法、未使用变量、空值处理错误 | 报出文件、行、规则号 |
| 类型检查 | 接口对不上、字段名拼错 | 阻断合并 |
| 单元与接口测试 | 行为回归 | 阻断合并,附失败用例 |
| 复杂度 | 函数过长、嵌套过深、圈复杂度超限 | 阻断合并,指出超限的那个函数 |
| 密钥扫描 | 误提交的密钥与凭据 | 阻断合并,并轮换已泄漏的那一个 |
| 依赖漏洞 | 含已知漏洞的依赖 | 高危阻断,中危限期修 |
| 规约场景回归 | 这次改动违反了自己写的规约 | 阻断合并,指出违反的是哪一条 |
每一项的阈值数字写在对应的检查页,这一页不重复。第八项最容易漏:规约里的场景不能自动跑,它就只是一份文档。覆盖率则相反,是最容易被误用的一项——它挡的是「新增代码一行测试都没写」,不是「测试写得好不好」。
红线是「不可降级的底线」,不是「所有质量问题」——风格、命名、格式交给工具。前四条本地能拦,后四条靠分支保护与评审;每条都要写得出发现方式,写不出的先别进清单。
| 红线 | 违反了怎么发现 | 挡在哪一步 |
|---|---|---|
| 密钥、令牌、私钥进仓库 | 密钥扫描退出码非 0;或改动差异里出现密钥样式的前缀与私钥头 | 提交前钩子 + 门禁 |
| 调试输出留在要合并的改动里 | 静态检查报出对应规则号;或差异里出现打印语句 | 门禁 |
| 大段注释掉的旧代码 | 改动差异里连续多行以注释符开头且含括号 | 评审 |
| 函数超过复杂度上限 | 复杂度检查退出码非 0,报出函数名与超限值 | 门禁 |
| 为让检查变绿而改判据 | 同一次合并请求里既有测试文件、又有阈值或规则文件的改动,却没有对应的规约变更 | 评审 |
| 核心逻辑没人读过就合入 | 批准人数为 0,或批准者说不清这段改动解决了什么 | 分支保护要求 ≥ 1 个批准 |
直接推 main | 主分支的提交里出现未经合并请求的提交 | 分支保护 |
| 看不懂的助手改动照合 | 评审人对改动意图给不出答案,却点了批准 | 评审:拆小,或让作者把意图写进说明 |
第 5 条最难发现,因为它常伪装成「顺手优化」。判断方式很简单——同一次合并请求里出现检查配置与功能代码,就拆成两次。
助手最常见的「修复」不是改错,是改判据——它要的是让报错消失,而让报错消失最快的方式往往是削弱检查。下面这些改动一律挡回,不算完成。
skip;把检查错误降级为警告;给整份文件加类型忽略;把复杂度上限调高好让当次通过。
真想放宽一项检查,就走单独的合并请求:不夹带功能改动,说明为什么,由人提交。检查配置、阈值、指令文件都是判据本身,改动要比普通代码更严——把它们放进 CODEOWNERS,指定必审的人。指令类文件也列进来,是因为助手会读它们,与判据是同一类东西,专用写法见指令与配置保护。
同一类问题在测试里还有另一种形态:假测试。它们跑起来是绿的,一条行为也没验证,却同时把覆盖率抬上去——所以看覆盖率看不出它们,评审里也最容易被放过,因为改动说明都写得像是修复。
| 现象 | 说明什么 | 评审时看什么 |
|---|---|---|
| 报告全绿,新增测试里没有断言 | 一条行为也没验证;覆盖率数字被当成成绩单 | 打开新增的测试文件,看有没有断言那一行 |
| 只断言替身 | 被测代码没真正执行,替身返回什么就断言什么 | 看断言右值是不是被测逻辑的输出 |
| 跳过的与被注释掉的用例留在提交里 | 报告仍绿,被断言的行为再没人管 | 看跳过数是零还是在涨 |
| 期望值跟着实现改 | 改的是判据不是行为 | 看同一条用例的期望值被改过几次 |
把两条要求写进合并请求:新增的测试至少有一条会在数据错时变红的断言;跳过的用例单独写原因与恢复时间。
入口命令全仓库只有一条,本地与持续集成跑同一条,不存在「本地过、线上不过」;工作流只调这条命令,不在 YAML 里另抄一份规则。工作流本身不能代替分支保护——检查跑绿了,仍然有人可以直接推到 main,所以要同时打开「禁止直接推送」与「合并前必须通过检查」两项。
| 文件 | 它管什么 | 改动由谁签字 |
|---|---|---|
Makefile | 入口命令的聚合入口,全仓库只有这一处定义 | 项目负责人 |
pyproject.toml / .golangci.yml | 各检查项的阈值,集中在一处 | 项目负责人 |
| 工作流配置 | 什么时候触发 | 项目负责人 |
AGENTS.md、.claude/、.cursor/ | 会被助手读进上下文的指令 | 项目负责人 |