# Verify And Fix

> 交付前需验证完成，或修 bug 需修病因不修症状时。

- Skill: `lion-1209/verify-and-fix` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add lion-1209/verify-and-fix`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lion-1209/verify-and-fix/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Lion-1209 (https://skillmd.com/u/lion-1209)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lion-1209/verify-and-fix

---


# Verify and Fix

## 概述

把"声称完成"变成"经验证完成"。核心：**代码写完 ≠ 测试过 ≠ 类型对 ≠ 真的没问题**——这三个"≠"是大多数返工和线上事故的源头。本 skill 的纪律是：交付前用工具实际跑一遍验证，修 bug 时找根因而非压症状，绝不为"通过"而弱化检查。

> 灵感来自 multi-agent "loop engineering"（写代码的 agent 与检查的 agent 分离、循环到全绿）。但本 skill 提炼的是**纪律本身**——无论单 agent 迭代还是多 agent loop，验证-修复的纪律不变。loop 是一种强制实现，纪律才是本质。

## 何时使用

- 刚写完代码，准备声称"完成"/"修好了"
- 修 bug，决定怎么修
- 测试/类型检查报错，要处理
- 来回改同一处反复出问题（怀疑在原地打转）

**不该用**：纯探索/原型阶段（本来就不要求正确，验证是负担）；明确无失败可能的确定性逻辑（过度验证也是负担）。

**与相邻 skill 的衔接**：`verify-and-fix` 在 task-breakdown 的下游——每个任务的"完成定义"就是它的验证目标。task 拆出"做完 X 后能验证 Y"，verify-and-fix 负责"实际去验证 Y、不通过就修"。它把 task-breakdown 里的"完成定义"从纸面标准变成实际跑过的证据。

## 核心内容

### 第一原则：未经运行的代码不算完成

"我看了一遍，应该没问题"——这是最危险的完成声明。**代码审查（肉眼看）不能替代运行验证**。肉眼能发现风格、明显逻辑错，但发现不了：实际运行时的类型不匹配、边界数据触发的分支、依赖交互、并发时序。

完成的标准必须是**实际跑过的证据**：

- 有测试 → 跑测试，全绿才算过
- 有类型系统（TS/带 type hint 的 Python/Rust）→ 跑类型检查
- 能跑 → 实际跑一遍目标场景
- 三者都要，不是任选其一（测试过不代表类型对，类型对不代表运行时分支都对）

**重构场景尤其要防回归**：把 for 循环换成 `find`、换库、提函数、改异步为同步——这类"等价改写"最易**悄悄丢失原代码处理的边界**（原循环在没找到时可能返回 null/抛业务错，`find` 改写后这个分支没了）。验证重构时，问自己：**原代码处理过哪些情况？新写法每一条都覆盖了吗？** 尤其是边界——空集合、找不到、异常输入。重构的验证标准比新写更高：新写只验"能跑"，重构还要验"行为没变"。

> 反例：用户说"我把 for 循环改成 find，应该没问题"，你附和"嗯看着对"——但 `find` 可能返回 undefined，`.name` 就抛错。这种 bug 肉眼看不出来，跑一下立刻暴露。

### 修病因，不修症状

修 bug 最常见的错法：**在报错末端兜底，把错误压住**，而不查它为什么报错。

- 报错 `Cannot read 'map' of undefined` → 加可选链 `data?.map(...)` 压住。错误消失了，但 `data` 为什么是 undefined 的病因还在——换个场景又会炸。
- 测试报 `result is 4, expected 5` → 把断言改成 `toBe(4)`。测试变绿了，但"为什么是 4 而不是 5"的问题被掩盖。

**区分"合理兜底"和"掩盖 bug"**：

- **合理兜底**：值**合法地可能为空**（如可选字段、外部数据可能缺失），兜底是设计的一部分。例：`user.nickname ?? user.name`——昵称本来就可不填。
- **掩盖 bug**：值**本不该为空却空了**——说明上游有 bug（数据源问题、初始化遗漏、路径没覆盖）。这时兜底只是把火盖住，火还在烧。

判断尺子：**问"这个 undefined/null 在什么情况下出现？"**——能说出一个合理的业务场景（"用户没填昵称"）→ 合理兜底；说不清、或答"反正运行时出现了"→ 是 bug，查源头。

修病因的路径：沿错误**向上游追溯**——报错点 ← 数据从哪来 ← 谁产生/传递的 ← 什么条件下变成这样。修在最上游的"产生"处，而不是最下游的"消费"处。

**断言失败时的灰度——先确认期望值的权威性**。测试报"实际 4，期望 5"时，别本能地认定"代码错了、断言对"。先问：**期望值 5 从哪来？** 是规格文档/业务约定的硬需求 → 代码错了，修代码；还是开发者随手写的猜测（"我觉得应该是 5"）→ 可能断言本身错了。区分两种"改断言"：

- **错的改断言**：期望值有权威来源（规格/约定），为了通过把它改成实际值——这是弱化检查。
- **对的改断言**：发现期望值本身就是错的（规格已改、当初写错了），改成正确的期望值——这是修正错误期望，不是放水。

判断尺子：**改完的断言，描述的是"应该怎样"还是"现在怎样"？** 前者（哪怕是改出来的）可能正确；后者（让断言迁就现状）一定错。

### 红线：绝不为"通过"弱化检查

这是不可越界的底线。当测试/类型检查挡路时，**正确的反应是修代码让它真正通过，错误的是改检查让它放行**：

- ❌ 把断言从期望值改成实际值（`toBe(5)` → `toBe(4)`）——测试退化成"描述现状"，失去验证意义
- ❌ 删掉报错的测试
- ❌ `@ts-ignore` / `any` 压住类型错（除非有明确临时理由 + 留 TODO）
- ❌ 用 `try/catch` 吞掉异常让流程"通过"
- ❌ `|| []` / `|| {}` 给本该有值的东西兜个默认值让它"不报错"

**为什么这条是红线**：弱化检查制造的是**欺骗性通过**——绿灯看起来像"完成"，实际是"绕过了验证"。它比直接失败更危险，因为失败会逼你修，欺骗性通过会让你带着 bug 交付。检查挡路是信号，不是障碍——它在告诉你"这里有真实问题"。

唯一例外：临时调试想跑通流程时可以**临时**禁用某项检查，但必须留显式标记（TODO + 理由），且不进主分支。

### 保留原始失败信息

修 bug 时，失败报告（报错、堆栈、行号、复现步骤）是定位根因的唯一线索。**别在传递过程中"总结"或"过滤"它**：

- 差："测试挂了，有个 bug"——builder/你自己无从下手，只能瞎猜。
- 好：完整保留报错原文、行号、堆栈、触发场景。"`TypeError: Cannot read 'name' of undefined at user.js:12，调用栈：getProfile←render，复现：用户列表为空时触发`"。

为什么：根因往往藏在**行号、调用栈、触发条件**里。"有个 bug"这种总结把这些全丢了，逼修的人从零重新发现。哪怕你觉得某些信息"无关"，也原样保留——是否相关由修的人判断，不由你过滤。

> 在多 agent loop 里这条尤其关键：checker 的失败报告必须**原样转发**给 builder，编排器不能自作主张总结（"checker 说有点问题"会把行号丢了）。单 agent 迭代同理——别把上一轮的报错"消化"成模糊印象，留着原文对照。

### 修复后防回归：跑全部相关，不只跑改的

修完一个 bug，**只跑出问题的那个测试**是不够的——你的修复可能让别处悄悄坏了。修复后的验证范围应该是**全部相关测试**，不是只跑改动点：

- 改了函数 X → 跑所有调用 X 的测试，不只跑 X 自己的
- 改了共享逻辑 → 跑全量测试
- 改了配置/类型定义 → 跑受影响的所有模块

为什么：修复本质是改动，改动就有副作用。只验"原本报错的地方不报了"会漏掉"修复让别处报错了"。这就是回归——表面上修好了 A，实际引入了 B。养成习惯：**修完默认跑全量相关测试**，把"只跑改的那个"当作偷懒的危险信号。

**验证范围要匹配改动的影响面**，不是无脑跑全量（那是另一种浪费）：

- 改了**叶子函数**（没人调用或只一处调用）→ 跑该函数 + 唯一调用方
- 改了**共享工具/被多处依赖的逻辑**（utils、配置、类型定义）→ 跑所有调用方的测试
- 改了**核心数据流/公共组件** → 跑全量，甚至手动冒烟关键路径
- 改动**只影响自己**（如函数内部重构、行为不变）→ 跑该函数测试即可

判断尺子：**这次改动可能波及哪些代码？** 影响面 = 验证范围。低估影响面（把共享改动当叶子改）是回归的常见源头；高估影响面（叶子改动跑全量）只是慢，不危险——拿不准时往大了跑。

### 循环要有刹车

修了 A 引入 B、修 B 又引入 A——这种循环不罕见。没有刹车的循环会无限转下去，越改越烂。预设停止条件：

- **全绿**：所有验证通过 → 成功，停。
- **超最大轮次**（如 5 轮）→ 停，承认当前方案走不通，退回去重新想（不是硬撑）。
- **同一失败连续两轮**：说明上次修复没生效或方向错 → 停，换思路。
- **修复引入回归**：新改动的代价大于收益 → 停，回退这次改动。
- **无进展**：几轮都在原地打转 → 停，把问题升级（求助/换方案/拆小）。

刹车不是放弃，是**防止把"修不好"变成"越改越糟"**。到达刹车条件时，正确动作是退回重新设计或求助，而不是继续在同一处硬磨。

## 常见错误

| 问题 | 修法 |
|------|------|
| 没跑就声称完成（"看着应该没问题"） | 跑测试/类型检查/实际运行，用证据替代肉眼判断 |
| 修症状（末端兜底压住报错） | 向上游追溯根因，在最上游产生处修 |
| 为通过弱化检查（改断言/删测试/any/吞异常） | 红线不可越——修代码真正通过，而非改检查放行 |
| 把失败报告总结成"有个 bug" | 原样保留报错/行号/堆栈/复现步骤 |
| 修复无限循环、越改越乱 | 预设刹车（轮次/重复失败/回归/无进展），到点停并退回 |
| 把合理兜底（可选字段）当 bug 修，或反过来 | 问"空值何时出现"——有合理业务场景=兜底，说不清=bug |
| 重构只验"能跑"，不验"行为没变" | 重构要覆盖原代码处理过的所有边界，标准比新写更高 |
| 修复后只跑出问题的测试 | 跑全部相关测试（调用方/共享逻辑/全量），防回归 |

