Back to skills
SKILL.md
Verify And Fix
ASecurity交付前需验证完成,或修 bug 需修病因不修症状时。
- 4 stars
- 0 votes
- 0 copies
- 2 views
- Added September 6, 2026
Security analysis
100/100Pro scans all 2 files and shows the line behind each finding
npx -y skills add Lion-1209/Lion-Skills --skill verify-and-fix --agent claude-codeAre you the author of Verify And Fix?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/lion-1209-verify-and-fix)---
name: verify-and-fix
description: 交付前需验证完成,或修 bug 需修病因不修症状时。
---
# 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 |
| 重构只验"能跑",不验"行为没变" | 重构要覆盖原代码处理过的所有边界,标准比新写更高 |
| 修复后只跑出问题的测试 | 跑全部相关测试(调用方/共享逻辑/全量),防回归 |
Files in this skill
- SKILL.md
- evals/evals.json
Attribution
Comments
Loading comments…