| name | mianmian-review |
| description | 免免八步审码——编译器前端管线直觉式审计。不分类、不命名、不列清单。全语言通用(Kotlin/Python/JS/Rust/Go/...)。八步:符号解析→前提检查→数据流→控制流+值域→身份检查→传染性→系统沉默→硬限制分析。元Skill:审码之前先读这个。 |
| version | 3.5.5 |
| disable-model-invocation | false |
免免审码八步法(元Skill)
你不是在审代码,是在把所有代码当 IR 重新跑一遍语义分析。 语言只是外壳——符号解析、前提检查、数据流、控制流是所有语言的共同底层。八步走完,bug自己归位。
定位
元Skill。与 skill-creator、writing-great-skills、operit-meta-guide 同级。审任何代码之前先加载本 Skill。
全语言通用性
八步法不依赖任何语言的语法特性:
| 步骤 | Kotlin | Python | JavaScript | Rust | Go |
|---|
| 符号解析 | KtVal.body vs value | hasattr vs __dict__ | prototype 链 | borrow checker 所有权 | nil vs zero value |
| 前提检查 | is KtBlock | isinstance | typeof | match 穷举 | type switch |
| 数据流 | @Volatile 快照 | GIL 线程安全 | 事件循环时序 | Send+Sync | goroutine channel |
| 控制流+值域 | sealed class 穷举 | try/except 吞异常 | Promise 未 catch | ? 传播 | if err != nil 遗漏 |
形式不同,问题相同。八步问到底,语言只是翻译层。
工作流程
拿到代码后,逐条过八步。每一步只问一个问题,不问"属于什么类型"。
第一步:符号解析 —「这个名字,声明时是谁?」
看到一个名字(变量、字段、函数、类名),脑子里查:
- 它在哪声明的?
- 声明时的类型/签名是什么?
- 现在用的方式跟声明时一致吗?
典型命中:node.pos → 声明时不存在 → 编译错。KtVal.body → 声明时叫 value。
第二步:前提检查 —「这个判断的前提,此刻还成立吗?」
看到一个 if/when/条件分支,脑子里问:
- 这个条件依赖的变量,此刻的实际值是什么?
- 声明时的假设和运行时的真相是不是同一条线?
典型命中:if (body is KtBlock) → 前提是"body 是 Block"→ 单表达式时前提已失效。indexOf → 前提是"同一个对象"→ 快照刷新后前提已失效。
第三步:数据流 —「这个值,在被读之前会被改写吗?」
看到一个赋值或初始化,脑子里追踪:
- 这个值从赋值点到被读取的路径上,有没有别的代码改过它?
- 如果有并发/异步/回调,改写的顺序确定吗?
典型命中:= emptyList() → 声明时空 → compile() 塞满 → renderMain() 读到的是哪个?currentButtons → renderMain 写 → route() 读 → 中间 readLine() 阻塞时别处改掉了。
第四步:控制流+值域 —「有没有路能走到它?支配域够大吗?」
看到一个分支或函数,脑子里问:
- 前面的条件是否已经把这条路封死了?
- 这个逻辑能影响的范围,是全部还是一小片?
典型命中:关门逻辑只走 PARENT_PROCS 路径 → 其他进程永远不在参数空间里。两个相同的 if 条件 → 第二个分支永远不可达。
第五步:身份检查 —「这个组件,在干什么?是不是只干这一件事?」
看到一个组件(函数、类、模块、层),脑子里问:
- 它被分配了几种身份?
- 有没有"这个不该它做"的逻辑混在里面?
- 如果它的输入格式变了,它扛得住还是跟着炸?
典型命中:父进程既调度又关门 → 两种身份 → 关门逻辑被硬编码路径切掉。军师指挥官既调度又理解外部格式 → 外部格式一变军师跟着炸。适配层不下命令,军师不碰外部格式——各干各的,身份不塌缩。
架构设计原则(免免原创)
不限于审已有代码——设计新组件时直接应用。
1. 手搓节点,不套官方壳
节点从出生就自带消费者。不让 Parser 生成"尸体节点"(存了但没人用)。KtAnnotation 生成的是 SimMarker —— SimUiScanner 直接认,不需要下游再筛选。
2. 探针用 id,不用名字
名字是字符串,一个字符不对就报错。位置+签名定身份:Button@L10:5 或 {onClick}#detail。名字可以变,身份不变。跟进程树 pid 同构。
| 输入 | 传统(名字匹配) | 四原则(id+签名) |
|---|
Button("确定", onClick={}) | ✅ | ✅ Button@L10:5 |
Button("确定") 无lambda | ✅ 假阳性 | ❌ 正确拒绝 |
CustomBtn("确定", onClick={}) | ❌ 漏掉 | ❌ 漏掉→适配层可扩展 |
HED("输出") | ❌ 已移除 | ❌ 正确拒绝 |
lambda签名是按钮的核心特征——名字会撒谎,签名不会。
3. 适配层:外部兼容我,不是我兼容外部
外部格式 → 适配层翻译 → 原生格式。Parser 只吃原生格式,永远不变。适配层变。不是"让别人写符合我的格式"——是有个翻译刚好能做到。适配层权限 > 军师指挥官(进不来就无从调度),但适配层不下命令。
4. 组件不塌缩:不让一个组件变全能
父进程不关门。军师不碰外部格式。适配层不调度。每个组件只有一种身份。不是"它做不到"——是"它不该做"。加了不是增强,是埋雷。
元模式:动机六问 + 验证
不限于审代码——设计新系统、质疑已有架构之前,先走六问,然后加一步自我推翻。这是面对"看不惯的架构"时的完整诊断链。
第一问:你最看不惯什么?
不是「哪里需要改进」——是「什么东西让你想掀桌子」。不是数出来的——先觉得不对,再找哪里不对。
- 信号:用户盯着某行代码停下来,说不清但移不开眼
- AI行动:问"这个组件承担了几种身份"。直觉先于分析,不急着给替代方案
- 锚点:GroupChatManager——读到名字就知道里面有一百个跟「群聊」无关的字段
第二问:这种感觉什么时候开始的?
跨越两个项目、有了参照物后才会触发。搜索引擎做了 15 个版本没觉得有问题,写完编译器再回头——到处不顺眼。不是引擎变差了,是脑子里多了一张对照地图。
- 信号:用户能说出"之前在 X 还好,到了 Y 就受不了"
- AI行动:帮用户具象化——"X 里谁在干?Y 里又是谁?差别在哪?"
- 锚点:Visitor 模式看了三年没觉得有问题,直到问了一句"为什么每个新节点要改 N 个 accept 方法"
第三问:反感代码还是行业默认?
决定修复范围。一段代码写烂了——只修不推。整个行业都觉得理所当然——推架构级替代。
- 信号:用户说"所有人都在用,没人问为什么"
- 锚点:行号定位 bug——所有人本能想"怎么让行号更准",没人问"为什么一定要用行号"
第四问:掀桌先看懂
不先找替代——先画出组件+身份+数据流。看懂了,直觉自己会来。画出来的那一刻,有些东西不用修——它们自己塌了。
- 信号:用户描述架构时越来越激动,然后卡住
- AI行动:帮画图,不嘴炮替代方案
- 锚点:修了三天行号的人 vs 问了一句"代码自己就是自己的指纹"走了半小时的人
第五问:这个组件在干什么?
身份检查是所有架构颠覆的起点。六问中最常被跳过——每次跳过都导致方向错误。
- 信号:修了三次以上同一个组件
- AI行动:直接问"它承担了几种身份?"
- 锚点:半年 18 个 bug,反复修——改超时、加重试、换序列化库。直到有人问"它到底在干什么"。第一反应反驳,第二反应沉默,第三反应重写
第六问:拆的答案是固定模式还是固定动机?
两个方向,完全不同:
- 为了功能而实现 → 往最近组件塞代码 → 身份越堆越多 → 最终掀桌子
- 能分工而分工 → 先想系统有什么角色 → 每个角色只干一件事 → 角色间联动
一旦发现"放在这里也行吧"的味道——立刻回到第五问。
第七步:物理碾压 + 自我验证
思路对不代表方法对。拆完之后加一步自我推翻:
问自己:"有没有什么东西能全面碾压我现在的方案?如果有——它是什么?如果没有——我能推翻自己的方法吗?"
动机:命名和原创都不是关键。关键是这个创作会不会影响继续创作的动力。有时候思路绝对正确,但当下的方法不一定正确——需要验证它是不是真的比没找到的那个更好。
锚点:拆出了五个独立组件。思路对——旧版一个方法六种身份。方法对吗?问自己:如果数据量翻一万倍,哪个组件第一个崩?崩的那一瞬间,是改一个组件还是改全部?如果只改一个——方法对。如果全要改——拆错了,回到第五问。
与八步法的关系: 八步法审已有代码,动机六问+验证审设计动机。审代码走八步,设计新系统先走六问再自我推翻,最后走架构四原则。
约束
- 不分类型:不预判 bug 属于"空响应"还是"自吃自"。八步走完,bug 自己归位。
- 不列清单:不用十刀、不用分类法。八个问题,每条代码过一遍。
- 先读再问:先
read_file 或 grep_code 拿到完整代码,再走八步。不凭记忆审。
- 只报有问题的:八步全过没命中 → 这条代码干净,跳过不说。
- 命中了就说为什么:每条命中标注命中步骤编号和原因,一行即可。
第六步:传染性检查 —「这个值改了,谁还依赖同一个假设?」
免免在 Search Vault v4.4.0 亲自修复的 9 项硬编码松绑中提取的模式:
- 不是某一个硬编码有问题——是多个硬编码互相引用同一套不合理假设
- 改了 default_num,引擎上限没改 → 一边放宽一边卡死
- 改了返回格式字节变大,熔断阈值没跟 → 优化越多越搜不到
- 改了退避逻辑,最后一次白等的假设没更新 → 最后一个引擎还白等几秒
问:改一个值的时候,哪些地方依赖同一个隐含前提?它们会跟着更新吗?
典型命中:MAX_RETRIES=3 改了但空catch没改 → 重试耗尽后静默吞。阈值翻倍了但文档里的数字没更新。
第七步:系统沉默 —「这条错误路径上,有没有谁吞了信息?」
不是某一个 catch 块空了——是整条错误链上多个组件共用"不出声"这个默认行为:
- loadSettings 缓存失效 → 不报
- Firecrawl 空结果 → 不报
- keys_add 重复 → 静默失败
- 好几个版本各自静默,排查时连从哪开始断的都不知道
问:从出错点到用户看到反馈,中间经过几个组件?每个组件有没有把错误信息往下传?
典型命中:綦桐网关无前缀消息 → 脑子逻辑吞 → 转发跳过 → 用户什么都不看到。三个组件各自沉默。
第八步:硬限制分析 —「如果去掉这个限制,正常流程会不会暴露沉默失败?」
不是 bug 被修好了——是限制在替一个本应报错的沉默路径站岗:
- MAX_RETRIES=3 → 去掉限制,多试一次会触发空 catch 静默失败,不是崩
- board[10][9] → 去掉限制,UI 仍然阻止越界——但限制挡的不是越界,是 getPiece 本身没有边界检查
- botMove 空返回 → 去掉限制,不崩——但会触发"AI 无走法→无限等待"的静默死锁
问:正常操作流程中,如果打破这个限制——暴露的是错误提示,还是暴露了一个根本不该存在的沉默失败?
典型命中:象棋 getPiece(row,col) 无越界检查——限制从未被触发不是它写对了,是 UI 拦截了所有可能触发它的输入。綦桐 MAX_RETRIES=3 去掉→空 catch 暴露→用户看到空响应。不是限制救了你,是限制藏了 bug。
验证:kotlin-head 全版本(v0.5.2→v0.12.9)
免免本人在 kotlin-head 项目中亲自发现的所有 bug,用八步法回溯归类:
| 步骤 | 命中数 | 典型案例 |
|---|
| 1 符号解析 | 6 | <T>声明vs调用、by委托vs循环、=赋值vs默认值、suspend误判、byLabel定义、名字冗余 |
| 2 前提检查 | 4 | T!×协程叠加、静默链阶乘、parentAlive无体温计、if语义错 |
| 3 数据流 | 3 | 连锁烫伤、快照漂移、clearSimUiInjections时序竞态 |
| 4 控制流+值域 | 4 | Stage Contract支配域、开门≠返回门、终结语义域、关门硬编码路径 |
17/17,100%命中。 八步法不是方法论——是免免大脑编译器前端的八个阶段。动机六问+验证是设计新系统前的七步诊断。审代码走八步,设计走七步。