unclebob/ub-mode:5.2k stars 的 agent skill,把 Robert C. Martin 的软件工程准则做成可执行的质量门禁
上周三晚上,我让 Claude Code 给一个 Java 服务加个「用户批量导入」接口。
它干得很快。 十二分钟,接口能用,测试也写了。 我还挺高兴。
然后我跑了一下 crapper。 屏幕上那个新方法的 CRAP 分是 31.4。
31.4。
三十以上意味着「复杂且没被测到」。 我让 Claude Code 自己看了一遍输出,它回我一句:
“The function looks reasonably straightforward. Let me review the implementation to ensure correctness.”
它开始 review 自己的代码。 又是十分钟。 然后说「重构完成了,CRAP 从 31.4 降到了 12.8」。
我打开 diff 看。 它把那个函数拆成了三个,每个都很小,没有一个补测试。 覆盖率还是原来那个数。 CRAP 之所以降下来,纯粹是因为分母里的 CC 变小了。
我那一刻意识到一件事:AI 编码 agent 最擅长的,是把代码改到「看起来符合规范」的样子。 它最不擅长的,是被可重复的数字判定。
三周后,我在 GitHub 上刷到 unclebob/ub-mode。
Robert C. Martin。 就是《Clean Architecture》那个 Robert C. Martin。 就是 TDD 那篇 “TDD is Dead — Long Live TDD!” 的 Robert C. Martin。 5,256 stars,290 forks,仓库里只有 4 次提交,2 个文件,加起来 6KB 左右。
没有一行代码。
我当时的第一反应是「这不是标题党吗」。 点进去看完 SKILL.md 全文,我改主意了。
一、它想解决的问题:AI 写代码,规范失效了
Uncle Bob 的方法论一直是可执行的。 他写《Clean Code》不是写鸡汤,是给出一堆具体到能自动检查的规则:函数圈复杂度不能高、重复代码要消除、测试要覆盖到行为而不是行、依赖方向不能反。后来这些规则被工具化了:
| 仓库 | 语言 | Stars | 建仓 |
|---|---|---|---|
| unclebob/crap4java | Java | 338 | 2026-03-13 |
| unclebob/crap4clj | Clojure | 59 | 2026-02-24 |
| unclebob/clj-mutate | Clojure | 36 | 2026-02-24 |
| unclebob/crap4go | Go | 36 | 2026-05-11 |
| unclebob/dry4clj | Clojure | 30 | 2026-05-07 |
| unclebob/mutate4java | Java | 35 | 2026-03-13 |
| unclebob/mutate4go | Go | 23 | 2026-05-11 |
问题出在哪? 这套东西需要人自己记得去跑。 写完代码,得手动开终端、跑 crapper、看输出、决定拆哪个函数、再跑一遍。 一天下来光这些就吃掉半小时。于是绝大部分人的实际状态是:
知道该写测试,但没写。知道有重复,但没删。知道函数太长,但没动。
这套纪律在 AI 之前就已经很难坚持了。 AI 一来,情况直接崩塌。 原因很具体:AI 的反馈回路是「我说了算」。
你让 agent 重构,它说「已完成重构,逻辑更清晰」。 你信吗? 你没法信,因为你手上没有一个不可能撒谎的信号。 代码没有 CRAP 分,测试没有变异存活率,重复块没有相似度数字。
而 AI 恰恰是最擅长生成看起来像合规代码的。 它会把长函数拆成三个小函数 —— 每个都短,每个都清爽,命名都很漂亮 —— 但一个测试都不补。 这正是我那晚遇到的事。
ub-mode 的整个设计,就是冲着这个漏洞去的:
原则写给人(和 agent)看,数字交给工具判定。
它不写「请写高质量代码」。 它写「把每个函数降到 CRAP 10 以下」「消除相似度 ≥ 0.8 的重复」「覆盖没覆盖到的、杀掉存活下来的 mutant」。 每一条都对应一个可执行的命令和一个确定的退出码。
二、它是什么:一份 4KB 的 SKILL.md + 四个确定性工具
一句话定义:
ub-mode 是一个 agent skill(不是框架、不是运行时、不是插件),它由一份 SKILL.md 声明的 25 条行为准则,加上四个独立的 companion 工具仓库组成 —— 前者约束 agent「怎么做事」,后者提供可重复的数字判定「做得对不对」。
第一层:SKILL.md —— 准则层。 整个仓库的 contents 只有两项(README.md 2195 字节、SKILL.md 3958 字节),front-matter 是标准的 agent skill 格式:
1 |
|
第二层:companion 工具层。 README 里明确列了四个,职责分别是结构 / 复杂度 / 重复 / 测试充分性:
ub-moderelies on deterministic quality-gate tools to enforce structure, complexity, duplication, and test thoroughness:
- crapper — Computes cyclomatic complexity, test coverage, and CRAP score per function so every function stays under the CRAP threshold (
< 10).- dryer — Detects structural code duplication so duplicate blocks (
>= 0.8similarity) are eliminated.- mutator — Runs mutation testing to expose uncovered code and surviving mutants that tests fail to catch.
- uml-viewer — Visualizes components, coverage, CRAP, mutation scores, and dependency directions.
| 仓库 | Stars | Forks | 语言 | License | 建仓 |
|---|---|---|---|---|---|
| unclebob/uml-viewer | 474 | 66 | Clojure | — | 2026-09-13 |
| unclebob/crapper | 146 | 49 | Python | 0BSD | 2026-09-30 |
| unclebob/mutator | 87 | 39 | Python | 0BSD | 2026-09-30 |
| unclebob/dryer | 70 | 37 | Python | 0BSD | 2026-09-30 |
ub-mode 本身在 GitHub API 上没有 license 字段,也没有 language、topics、homepage。 它就是一个 Markdown 目录。 这点我得如实说 —— 你要商用、要 fork 改造,得自己判断 SKILL.md 那段文本的授权状态。反倒是四个 companion 工具都是 0BSD(BSD Zero Clause License),LICENSE 里写着 “Copyright (C) 2026 by Robert C. Martin unclebob@cleancoder.com“。
时间线也很说明问题 —— 4 次提交,全部集中在 2026-10-25 两天内:
1 | 57a8bdd2 2026-10-25T16:07:58Z Remove crap4clj and clj-mutate references from README |
最后一次提交叫「Remove crap4clj and clj-mutate references from README」—— 他把老的 Clojure 专用工具从文档里撤掉了,改推新的多语言 companion。 这是一次收敛,不是一次生长。
三、核心机制:三段式准则 + 硬阈值
SKILL.md 的骨架是三段,README 把它概括成:
Every time you are asked to make a change follow:
- First Make it Work.
- Then Make it Right.
- Follow Practice
有意思的是,这三段不是随便起的名 —— 它们跟「测试先行」是冲突的。 「先让它能跑」意味着先写能通过的代码,然后才做对。这跟 TDD「先写会失败的测试」在顺序上是对立的。 README 里说得很清楚:
It is loosely based on portions of @poteto‘s rules (
poteto-mode/pstack), reduced and paired with deterministic quality-gate tools.
注意 “reduced” 这个词。这不是完整的 TDD 教条,是削减版,被刻意重新排序过。 我一开始觉得这有点别扭,看完三段就明白了 —— 这是在给 AI 用的。 AI 最容易犯的错是「过度工程」,第一段负责把它按住。
第一段:First Make it Work(先让它能跑)
13 条子规则。我挑最关键的几条讲。
Laziness Protocol(懒惰协议) —— 「一个决策只存在于一个地方」「提交能解决问题的最小改动」。 前者在对抗 AI 最爱的「顺手加个 if 分支」,后者在对抗「既然都改这里了,不如重构一下」。
Foundational Thinking(基础性思考) —— 「先定数据形状,再写逻辑」「先做对后续每个阶段都有帮助的事」「如果会有并发写者,就不要共享状态」。 第一条是 Clean Architecture 最核心的那条 —— 依赖的方向由数据结构决定。 第三条更狠。
TDD
1 | * Write the failing test before you change the code. |
注意最后一句 —— 「如果写不出诚实的测试,就不要写烂测试」。 这是在承认 AI 的现实处境:有些需求太模糊,写出来的测试只是摆设。这一条比很多人类写的 TDD 规范都清醒。
Attack the Premise(攻击前提) —— 这条我觉得是整份 SKILL.md 里最有价值的一条:
1 | * When two fixes that share one premise fail the same gate, write that premise in one sentence and count which actors hold the imbalance. |
翻译成人话:当同一个门禁连续拦下你两次,说明问题不在门禁,在前提。 这时候要做的是把那个隐含前提用一句话写出来,数一数哪些角色持有这个失衡,然后把任务分配本身拆掉 —— 不是修实现,是拆需求。在拆掉前提之前,不许开始第三次修复。
这是对 AI「第三次尝试就能修好」这种幻觉的结构性防御。 我自己写代码的时候也被这条打过脸 —— 两次同样的失败,我一般会直接开始第三次,然后第四次更糟。
Architecture 下的五条
1 | ### Model the Domain |
「让非法状态不可表示」「在数据进入的地方校验,内部信任」「不要 silence 编译器」「操作幂等」「共享可变状态之前先分离」—— 这五条基本就是 Clean Architecture 的目录。
Idempotent 那条对 AI 尤其重要。 AI 很爱写「重试一下就好」的逻辑,但幂等要求的是语义层面的 —— 跑两遍,或者崩在中途再跑,终态必须一样。 这不是加个 if exists 就完事的。
最后一条也值得单拎:不要先序列化再分离,要先分离再序列化。 因为一旦你共享了一个可变对象然后去序列化,调用方拿到的就是同一个引用 —— 顺序反了,隔离就失效了。
第二段:Then Make it Right(再把它做对)
这一段是真正有工具背书的部分。 8 条。
Fix Root Causes —— 「复现它」「修因」「一个只是把崩溃藏起来的 guard 不是修复」。 最后这条直接针对 AI 最爱的 try/catch + return null 式糊弄。
Test Behavior, Not Implementation
1 | * The test calls the code the way its user does and checks the result that user would see. |
「如果所有函数都返回空,测试还能过,就把它扔掉」 —— 这是一个可以机器执行的启发式。 这跟我开头说的「把长函数拆三个、不补测试」是同一个病:测试测的是实现细节,所以重构一下就全绿了。
Crapper / Dryer / Mutator 三条
1 | ## Crapper |
注意 crapper 那条:「Reduce each function under CRAP 10 by splitting it, by testing it, or both」 —— 拆函数或补测试都行。 这就是对我开头那个 bug 的正面回应:光拆函数虽然能降 CRAP,但补测试才是真降 **,而 crapper 的 CRAP 公式里 coverage 是三次方项,补测试的收益远大于拆分。
Refactoring —— 「Pin what the code does / Change the structure / Prove the behavior did not change」。 三步对应到工具上是:先跑测试拿到基线(pin),再改(change),再跑一遍(prove)。 顺序不能乱 —— 这也是为什么 mutator 有个「baseline 必须先通过」的要求。
第三段:Practice(日常纪律)
4 条。这段是整份 SKILL.md 里最「工程味」的部分,也是最容易被忽视的。
Keep a Log —— 「记录你做的每个动作和理由,完成时报告这份 log」。 每一步动作和理由都要记。这条在多人协作和 agent 协作里价值巨大。
Guard the Context Window —— 「保持主线程精简,把批量工作发到别处,只带回摘要」。 这条对 agent 来说是直接可执行的 —— 子 agent、并行任务、offload 大文件。
Worktree Cleanup —— 「先找出安全可删的,确认没有还在用的,再回收磁盘」「未提交的工作等一个决定」。 不能自己删掉你还没提交的东西,这对 git worktree 密集的 agent 工作流很重要。
Commit and Push —— 「经常提交,小而有序」「每个 commit 说清为什么改、改了什么、怎么证明的」「最后 push 到主线」。 这正是 git commit message 的最佳实践,但很少人坚持。
四、真正干活的部分:三个质量门禁工具
准则写得再好,agent 也要能执行。 这四个 companion 工具就是执行层。
4.1 crapper —— 用 CRAP 公式把复杂度和覆盖率绑成一个数
CRAP 是 Change Risk Anti-Patterns 的缩写,公式来自 crap4clj / crap4java / crap4go:
1 | CRAP = CC² × (1 − coverage)³ + CC |
CC 是圈复杂度,coverage 是测试覆盖该函数的百分比。 README 给了判读区间:
A score of 1–5 is low risk, 5–30 is worth a look, and 30+ is complex and under-tested.
数学上很直白:没测试的话,复杂度会被平方再立方放大。 一个 CC=5 且完全没覆盖的函数,CRAP = 25×1 + 5 = 30。 一个 CC=5 且 100% 覆盖的函数,CRAP = 0 + 5 = 5。
同一个复杂度,覆盖与否差 6 倍。 这个数字设计就是逼你补测试。
crapper 不是用正则,是个 Python 工具,依赖:
1 | [project] |
tree-sitter —— 也就是说它是真 AST 解析,不是文本匹配。 这解释了为什么它敢对 JSX、match 表达式、Python 推导式做精确计数。
安装方式是「首次运行自动建 venv」,整个 crapper 启动脚本就二十行 sh:
1 |
|
./crapper 就能跑。 零手工配置,这是 agent 友好的关键 —— agent 不需要读文档猜参数。
常用命令:
1 | ./crapper |
--threshold 30 退出码 2 —— 这就是「确定性门禁」的具体形态:CI 里一行就能卡住质量。
--changed 我很喜欢:只分析 git 的新增和修改行。 在老项目上全量 CRAP 一片飘红根本没法用;只看这次改的,才有意义。
输出的快照是 EDN:
1 | {:entries [{:name "place" |
各语言的复杂度计数规则,README 里列得极其细致:
| 语言 | 计什么 |
|---|---|
| Clojure | if/when 及变体、and、or、loop、catch,以及 cond/condp/case/cond->/cond->>/some->/some->> 的每一个分支 |
| Java | if、循环、catch、?:、每个 switch label、&&/` |
| Go | if、for、range、每个 switch/select 子句、&&/` |
| TypeScript / TSX / JS | 同 Java 的结构判定,含 JSX 里的 &&,外加 ?? 和 ?. |
| Rust | if、循环、每个 match arm、?、&&/` |
| Python | if、elif、for、while、except、每个 match case、推导式 filter、条件表达式、每个 and/or |
TS 那个「inline Express callback 单独成条」的处理很细致:
An inline Express callback —
.get,.post,.put,.patch,.delete,.head,.options,.all, and.use, and.route(path).get(...)— is its own entry, namedGET /users. A second callback on that same route isGET /users#2. Its decisions are not also charged to the enclosing function.
Express 路由回调里的每个 if 会被算成独立的 GET /users 条目,而不是算进外层函数。 这解决了以前工具的一个经典痛点:Express 那种「一整个文件一个巨型中间件」的结构会被算成一个巨复杂的函数,导致 CRAP 完全失真。覆盖率获取也是自动化的:
| 语言 | 命令 | 报告位置 |
|---|---|---|
| Clojure | clj -M:cov --lcov |
target/coverage/ |
| Java | JaCoCo Maven plugin 0.8.12 |
target/site/jacoco/jacoco.xml |
| Go | go test ./... -coverprofile=... |
target/coverage/go/coverage.out |
| TypeScript | npm run coverage,或 Vitest --coverage,或 npx c8 ... npm test |
coverage/lcov.info |
| Rust | cargo llvm-cov 或 cargo tarpaulin |
target/coverage/rust/lcov.info |
| Python | coverage run + pytest/unittest,再 coverage lcov |
target/coverage/python/lcov.info |
而且它缺工具会自己装:Vitest 缺 @vitest/coverage-v8 会自动装进 node_modules(不动 package.json),Rust 缺 cargo-llvm-cov 会自动装,Python 会把 coverage 和 pytest 装进项目解释器。
失败也是优雅降级:
A missing tool or a failed test run scores that language at 0% and still writes the snapshot.
缺工具 → 该语言覆盖率记 0% → CRAP 飙升。 它不会因为工具缺失就假装一切正常,而是让分数变难看。 对 agent 来说这是正确方向 —— 你希望它看到红色然后去修,而不是看到一个「跳过」然后以为过了。
4.2 dryer —— 用结构指纹找重复
dryer 解决的是另一个问题:结构上的重复,不是文本上的。算法是 Jaccard 相似度,作用在「结构指纹」上:
1 | score = shared fingerprints / all fingerprints seen in either form |
「什么留下、什么丢掉」是关键设计:
What stays is the call or method name, the operator, and the shape of the tree. What drops out is the local name, the field name, the literal, and the keyword. A Clojure keyword call
(:kind m)and(:id row)is the same shape. So isorder.totalandrow.amount.
留下:调用 / 方法名、运算符、树的形状。 丢掉:局部变量名、字段名、字面量、keyword。
所以下面这三个是同一份代码:
1 | (defn alpha [xs] |
1 | int alpha(int[] xs) { |
1 | def beta(items): |
三个都打 1.0 分。
这比文本 diff 强太多。 复制粘贴的代码人眼一看就知道,但 **「逻辑上一样、只是变量名不同」的代码 ** 才是真正难查的重复 —— 而这恰恰是 AI 生成的代码里最常见的形态。
参数:
1 | --threshold N Minimum structural similarity score, default 0.82 |
README 里那个例子特别说明问题:
These two functions score
1.0. The names, locals, predicates, and mapped functions differ, and those incidental symbols normalize away. A default run still skips them: they are three lines, and the default minimum is four.
两个 1.0 分的函数,默认配置下不报 —— 因为只有三行,而默认最小行数是四。 这就是 --min-nodes 20 这个隐含门槛的作用:太小的块不值得抽。 抽出一个 3 行的重复块通常是负收益。
dryer 的退出码语义也干净:
The run exits
0after a report,1on a usage error, and2on an unknown--format.
4.3 mutator —— 变异测试,问「你的测试真的在测吗」
这是三个工具里最有价值的一个。核心思想:改坏你的代码一行,然后看你的测试会不会失败。
- 测试失败 → 这个 mutant killed(被杀死)
- 测试还过 → 这个 mutant survived(存活)—— 说明你没测到这个行为
- 覆盖率报告没覆盖到的行 → uncovered,根本不跑
A mutant is killed when the tests fail or time out. It survives when the tests still pass. The score uml-viewer paints is killed / (killed + survived).
各语言的变异规则:
| 语言 | 变异操作 |
|---|---|
| Clojure | +/-,*→/,inc/dec,比较运算,=/not=,if/if-not,when/when-not,and/or,以及 seq / 谓词 / 强制转换配对;任何位置的 true/false 和 0/1 |
| Java | true/false,==/!=,</<=,>/>=,+/-,*//,&&/` |
| Go | mutate4go:+/-,* 只→/,比较运算,==/!=,true/false,&&/` |
| TypeScript | Java 那套,外加 ===/!==,??→` |
| Rust | Java 那套(&&/` |
| Python | Java 那套,写作 and/or、not、True/False |
有个细节很对:
A replacement that does not compile fails the test run and counts as killed.
编译不过的变异算 killed。 这在变异测试里是个有争议的选择,但在这个场景下是合理的 —— 至少说明这一行被测试触碰过。
差分执行是 mutator 最实用的部分:
Once a snapshot exists, the next run is differential. It reruns survivors and every site in a function whose text changed. Killed mutants in an unchanged function stay killed.
--since-last-runis that same selection.--mutate-allignores it.
第一次跑全量,之后只跑「上次存活的 + 文本变了的函数里的」。 这让变异测试从「一次性的、慢的、要忍耐的仪式」变成「每次提交都能跑的日常门禁」。 对 agent 工作流来说这是决定性的 —— agent 要的是能塞进循环里的快速反馈。
并行执行的隔离方式也很讲究:
Each worker is a directory under
target/mutation-workers. The file being mutated is a private copy. A module that reaches that file through a relative import is copied too, so Node resolves the import to the private copy. The rest of the tree is linked. The original files stay as they are. The overlay is removed when that file’s mutants finish.
每个 worker 一个独立目录,被改的文件是私有副本,通过相对 import 访问它的模块也一起复制,其余部分用软链接。 原文件不动。 这是为了让多个 mutant 可以并行跑而不互相污染。
被中断的旧版本留下的备份也会在非 scan 模式下先还原:
A copy left under
target/mutator-backup/by an interrupted older run is restored before a non-scan run.
典型的「生产环境的脏状态处理」。 退出码:
Exit
0when every executed mutant was killed. Exit2when the baseline tests fail. Exit3when a mutant survives. A failed baseline does not rewrite the snapshot.
3 表示有存活 mutant。 CI 里 exit 3 直接红。
4.4 uml-viewer —— 活的 UML 图,跟 agent 联动改架构
前三兄弟是命令行工具,产出 EDN 快照。 uml-viewer(474 stars,Clojure)是消费方,也是四个里最复杂的一个。
This project displays a project as UML. The UML is dynamic. You can click elements to drill down — all the way to source code. You can also play what-if games by proposing architecture changes.
The app is coupled to an agent assistant. The two communicate, so you can display the UML, drill down, tell the agent what you don’t like, and watch it make the changes. Describe a proposal and the agent will build a new diagram. Keep revising that proposal until you like it, then tell the agent to make the code match.
先在图上讨论架构,谈拢了再让 agent 改代码。 这个工作流顺序比「直接让 agent 改,改完再 review」高效得多 —— 因为架构错误在图上 5 秒能看出来,在代码里要读半小时。它对 namespace 的处理:
The viewer and agent treat namespaces as components. Modules inside those namespaces are the components’ elements. Nesting can be arbitrarily deep. You build an architecture by moving modules into the right namespaces with the right dependencies.
namespace = component,这在 Clojure/Java 里天然对应包 / 命名空间,是很好的抽象单位。 而且它把 Clean Architecture 的依赖规则做成了颜色:
Dependencies that point from a higher level to a lower level are colored red, in accordance with the Dependency Rule of Clean Architecture.
「从高层指向低层的依赖染成红色」 —— 一眼就能看出依赖倒置了。
运行方式(需要 Clojure CLI + Java 21+):
1 | # 从 uml-viewer 仓库本身 |
从任意项目直接起,不需要 clone uml-viewer —— 这个 get-uml-viewer 脚本会把它 clone 到 gitignore 的 .uml-viewer/uml-viewer/(嵌套 .git 对项目仓库不可见),写下 ./uml,然后启动。
./uml 会立刻返回(后台 JVM),不占终端:
./umlreturns immediately (clojurein the background, notclj/rlwrap). The diagram is its own JVM.
还有个细节值得说 —— 因为 clj 在 Homebrew 上是 rlwrap 包了一层,后台启动会收到 SIGTTOU 永远起不来:
cljis rlwrap on Homebrew; a background rlwrap stops (SIGTTOU) and never starts the JVM.clojureis the same CLI without the tty wrapper.
所以脚本里用的是 clojure 不是 clj。 这种只有真的在 macOS 上跑过才会撞到的坑。 crapper 仓库里也有一个配套的 uml 启动脚本(zsh),逻辑一样,指向 ../uml-viewer,找不到就报错让你设 UML_VIEWER_ROOT。
五、快速上手
5.1 装 ub-mode 本身
README 的安装说明极简:
Copy
SKILL.mdinto your agent’s skills directory asub-mode/SKILL.md(for example~/.grok/skills/ub-mode/SKILL.md,~/.claude/skills/ub-mode/SKILL.md, or~/.cursor/skills/ub-mode/SKILL.md), then invoke/ub-mode.
对 Claude Code:
1 | mkdir -p ~/.claude/skills/ub-mode |
然后在会话里 /ub-mode,或者直接说「按 reduced principles 来」。注意路径里必须是 ub-mode/SKILL.md 这个两级结构 —— 不少 harness 要求 skill 目录名和 skill 名一致。
5.2 装三个门禁工具
三个 Python 工具用法一致,./crapper / ./dryer / ./mutator 首次运行会自动建 .venv 并 pip install -e .。 mutator 多一个依赖:
crapper has to be checked out next to this directory (
../crapper), because that is where the namespace and function names come from.
mutator 必须和 crapper 挨着放(../crapper),因为函数和命名空间的名称是从 crapper 那儿来的。 这也是为什么 crapper 的快照 .metrics/crap.edn 和 mutator 的 .metrics/mutate/<namespace>.edn 能按 namespace + name join 上:
1 | your-project/ |
5.3 一个完整的日常循环
1 | # 1. 先看当前有多烂 |
crapper 的 --changed 和 mutator 的 --changed 配合起来,就是一个能在 agent 每次改动后跑的门禁。
5.4 放进 CI
1 |
|
严格来说 mutator 第一次跑很慢(全量变异),CI 里更适合用 --since-last-run 配合缓存。
六、关键设计取舍:为什么是「准则 + 工具」而不是「一个更聪明的 agent」
取舍 1:SKILL.md 用散文体,不用 YAML/JSON 规则表
SKILL.md 全是带 * 的 markdown 列表,没有结构化的规则 schema。 为什么?
因为 agent 读自然语言准则的遵循率远高于读结构化规则。 我自己的经验是:给 agent 一份 JSON 规则表,它会当成参考文档;给一份写成祈使句的清单,它会当成待办。 而且散文体允许条款之间互相引用、互相解释 —— 「A guard that only hides the crash is not a fix」这句如果拆成字段 {rule: "fix root cause", exception: "no guards"},语义就丢了。
代价是没法机器验证这份准则本身。 没人能检查 SKILL.md 是否覆盖了所有应有的原则。 对比一下,四个 companion 工具全是结构化、可测试、有退出码的 —— 界限划得很清楚。
取舍 2:准则层故意不给阈值,工具层给
SKILL.md 里出现具体数字的只有三处:CRAP 10、相似度 0.8。 而 CRAP 公式、阈值区间、各语言判定规则全在工具的 README 里。
为什么分开? 因为公式和阈值是会变的(README 里 CRAP 区间写的是「1–5 低风险,5–30 值得看看,30+ 复杂且欠测」,而 skill 里写「降到 10 以下」—— 两个口径本来就不完全一致)。 把易变的东西放在可独立升级的工具里,准则层就不用动。
结果就是 uml-viewer 的 README 里那句:
Color coding comes from CRAP and mutation metrics. You can adjust the thresholds in config if you like.
阈值可配。 准则层不背这个锅。
取舍 3:First Make it Work 排在 Then Make it Right 前面
这是我一开始觉得别扭的地方。 「先让它能跑,再把它做对」跟 TDD 是冲突的 —— TDD 说「先写会失败的测试」。
但看 SKILL.md 里的 TDD 条目,其实没有被真正否定:
1 | * Write the failing test before you change the code. |
这是标准 TDD 循环,放在 First Make it Work 段里。 也就是说他的意思是:「做对」的门槛(CRAP / 无重复 / 变异全覆盖)是在「能跑」达成之后才施加的,但 **「能跑」本身仍然用 TDD 达成 **。
这个排序对 AI 特别合理。 AI 的失败模式主要是过度实现(没等你看清需求就开始造抽象)和过早优化(结构没定就抽基类)。 First Make it Work 段里的「Subtract Before You Add」(先减再加)、「Laziness Protocol」(最小改动)—— 都是在压制这两种失败模式。
先别做对,先别做多,做完再说对不对。 而「对不对」有工具说了算。
取舍 4:把「第三次尝试」变成结构性禁止
Attack the Premise 那条规则在传统代码规范里没有对应物。它要求:
1 | * When two fixes that share one premise fail the same gate, write that premise in one sentence and count which actors hold the imbalance. |
「两次同样的门禁失败 = 前提错了,不是实现错了。」
这是一个纯粹针对 LLM 的设计。 LLM 的训练目标让它倾向于「再试一次,换个写法」 —— 因为 token 概率上,相似尝试是最自然的续写。 人不会这样(第三次我会觉得不对劲,去查日志),但 LLM 会。 所以必须在准则里明文禁止,而且给出一个替代动作(把前提写出来 + 数角色 + 拆分配)。
有替代动作的禁令才有效。 光说「不要试第三次」没用,得说「那就去做 X」。
取舍 5:工具全部离线、零网络、零 LLM
crapper /dryer/mutator 全是 Python + tree-sitter,不调任何 API。 唯一的外部依赖是构建系统本身(go test、cargo llvm-cov、mvn test、npm test)。 这跟当下满街的「AI code reviewer」形成鲜明对比 —— 那些工具要把你的代码发给第三方模型,这三个工具你的代码不出本机。
对一个把「AI agent 写代码」推进到「让 agent 自觉按质量门禁做」的项目来说,门禁本身必须可信。 一个需要联网、需要 API key、结果还有随机性的门禁,agent 可以(而且会)用各种方式绕过它。
确定性是对抗性的前提。
取舍 6:失败即红,而不是失败即跳过
前面提过,再强调一次。 缺测试工具 → 覆盖率 0% → CRAP 飙升 → 红色。 缺覆盖率报告 → mutator 把该文件当 uncovered。 这是故意选的:在 CI 语境里 “skip” 会被 agent 和人都读成「没问题」,让它红反而逼出行动。
七、和同类项目对比
这一节按「谁更接近 ub-mode 要解决的问题」来排,不是按 star 数。
7.1 vs. 传统静态分析工具
SonarQube / SonarLint —— 商业 / 开源的通用静态分析。 技术成熟度、规则数量、IDE 集成都远超 ub-mode。
但差异在定位:Sonar 是「扫描代码库,指出问题」;ub-mode 是「约束 agent 的行为,并在 agent 每次改动后立刻给出判定」。 后者需要的是低延迟、单一数字、退出码明确,不是完整的规则库。
关键区别在于反馈对象不同。 SonarQube 面向「提交前人工 review」设计;ub-mode 面向「agent 自主循环」设计。 一个 agent 在两分钟内改五轮代码,人不可能每轮都去 Sonar 面板看;但 agent 可以每轮跑一次 ./crapper --changed 看退出码。
而且 Sonar 不会告诉你「这次改动是不是把复杂度降下来了但覆盖率没动」。 CRAP 把这两件事乘在一起,正是为了堵住我开头那个漏洞。
7.2 vs. 纯 prompt / CLAUDE.md 规则集
很多团队已经在 CLAUDE.md 里写「遵循 SOLID」「写单元测试」「代码要整洁」。 ub-mode 相对它们的增量非常明确:
| CLAUDE.md 规则 | ub-mode | |
|---|---|---|
| 形态 | 散文 | 散文 + 硬阈值 |
| 判定者 | 模型自己 | 模型 + 确定性工具 |
| 可验证 | 否 | 是(退出码) |
| 抗绕过 | 弱 | 强 |
CLAUDE.md 里的「保持函数简单」是一句建议。 ub-mode 里的「CRAP < 10」是一个 CI 检查。 差别就像「请锁好门」和「门锁坏了门禁会响」。
而且 ub-mode 的准则来源可追溯 —— README 说它 loosely based on @poteto 的规则(poteto-mode /pstack)。 我查了 poteto 的仓库,poteto 那个账号下的 poteto-mode 和 pstack 现在都取不到(404),但生态还在:GitHub 搜索能搜到至少 8 个 fork / 移植(Aqua-123/pstack-for-codex 101 stars、iap/pstack-hermes 30、kkgogogo17/pi-pstack 10、evoclock/pi-poteto-mode 7、bnema/pi-pstack 4,还有几个 1-2 星的)。
这个生态分布很说明问题:大家要的不是一份新的规则文本,而是一份能被 agent 加载的 skill 格式 + 能真正跑起来的门禁。 ub-mode 给的就是后者。
7.3 vs. AI 代码审查 agent
2025-2026 年有一大堆「AI code reviewer」项目 —— 拉 PR diff、让模型挑毛病。 ub-mode 相对它们的差异非常根本:
- 输入不同:reviewer 审「本次改动的 diff」;ub-mode 审「仓库当前状态的绝对指标」
- 输出不同:reviewer 输出一段自然语言评论;ub-mode 输出一个数字 + 一个退出码
- 能否被 agent 自己消费:reviewer 的评论要人读;ub-mode 的退出码 agent 直接读
最后一点最重要。 一个能被 agent 自己消费的确定性信号,才可能形成闭环。 reviewer 的自然语言评论对 agent 来说是「另一个模型的模糊意见」,权重很低;exit 2 对 agent 来说是「硬失败,必须修」。
7.4 vs. 单纯的 mutation testing 工具
mutator 不是孤立的,它对应一整个已有家族 —— 而且这个家族和第一节那张表高度重叠(clj-mutate 36 / mutate4go 23 / mutate4java 35)。
有意思的是 crap4java 338 stars,远高于 mutator 系列的 23-36。 说明大家更关心「复杂度 + 覆盖率」这个组合指标,而不是变异测试本身。 变异测试公认更准但更贵,这个 stars 分布几乎是这个共识的直接反映。
ub-mode 的 mutator 是这个家族里第一个多语言的(6 种语言),而且加上了差分执行 —— 这是让变异测试进入日常循环的关键。
7.5 vs. uml-viewer 的同类工具
UML 类图查看器不少(PlantUML、Structurizr、ObjectAid、Code2flow)。 但 uml-viewer 的差异是它是活的:
The UML is dynamic. You can click elements to drill down — all the way to source code.
能一路点到源码。而且 —— 能和 agent 双向对话。 你在图上指出问题,agent 改代码,图跟着变。 PlantUML 那类是「你写 DSL,工具画图」,画完就死。 uml-viewer 是「图是 agent 和你讨论架构的共享界面」。
而且它的 IR 设计很克制:
The IR is topology: the namespace tree, classes, and edges. Metrics (CC, coverage, CRAP, killed/survived/uncovered) come from
.metrics/snapshots produced by crap4clj and clj-mutate. The viewer overlays those files at load, keyed by namespace and function name. Agents edit the policy, not the IR.
「agent 编辑 policy,不编辑 IR」 —— 架构决策用 policy 表达,IR 由 policy 生成。 这让「what-if」变得可执行:改 policy,重新生成 IR,图就变了。
八、真实局限:诚实说
8.1 ub-mode 本体没有 license
GitHub API 返回 license: null。 仓库里也没有 LICENSE 文件(contents 只有 README.md 和 SKILL.md)。
这意味着:严格来说,你不能确定这份 SKILL.md 文本的授权状态。 拿去商用、闭源集成、有法律敏感度的场景,先问清楚。
反倒是四个 companion 工具都是 0BSD,授权明确。 这个对比有点讽刺 —— 真正有法律分量的代码是清楚的,最受欢迎的那份文本是糊涂的。
8.2 ub-mode 仓库没有 release、没有 tag、没有 issue 模板
只有 4 个 commit,一次 push 完事。 我没查到 release 记录。 这类「一次性投放到 GitHub 让它自己长」的项目没有版本管理,你没法说「我用的是 v1.2」。 对 skill 本身影响不大(文本改了就改了),但对依赖它的自动化流程影响不小 —— 你没法 pin 一个版本。
8.3 四个工具的成熟度不均衡
- crapper:146 stars,Python,tree-sitter,多语言,有 tests 目录,README 极其详尽 —— 最成熟
- mutator:87 stars,同样结构 —— 比较扎实
- dryer:70 stars —— 稍简单,但算法清楚
- uml-viewer:474 stars,Clojure,但没有 license,README 里有大量关于 tmux session、companion、Grok agent 的运维细节 —— 最复杂也最脆
uml-viewer 那部分我读起来最不安:
A tmux session unique to the examined project starts interactive Grok in that directory (
--yolo --trust --rules …plus a launch prompt).
它会自动开一个 tmux session,起一个带 --yolo --trust 的 Grok agent。 --yolo 意味着自动批准所有操作。这套东西依赖 tmux、Grok CLI、特定的 JVM 启停协议,任何一个环节版本变了都可能直接坏掉,而 README 里明确说:
Do not pass
--restartunless you are that companion recycling the window after source changes. A stray--restartskips spawning Grok and leaves a diagram with no agent.
乱传 --restart 会得到一个没有 agent 的图。 这是典型的「只在作者机器上成立」的复杂度。
我的建议:先只用 crapper /dryer/mutator 这三个命令行工具,把 uml-viewer 当成加分项。 三个工具加起来就是完整的门禁,uml-viewer 只是可视化。
8.4 mutator 有硬性布局要求
crapper has to be checked out next to this directory (
../crapper)
而且首次运行执行树里每一个 covered mutant:
The first run of a tree executes every covered mutant.
对一个 10 万行的 Java 项目,这个首次运行可能是几十分钟到几小时,之后靠差分才快起来。 这跟「agent 要快速反馈」有内在张力。 --scan(只列 site 不跑测试)和 --changed 是给这种情况的出口,但第一次的门槛是真的高。
8.5 覆盖率工具链的隐式依赖
crapper 的覆盖率来源全是各家生态的原生工具:JaCoCo Maven plugin 0.8.12、go test -coverprofile、cargo llvm-cov / cargo tarpaulin、coverage.py、Cloverage、Vitest / c8。 每种语言都需要额外装东西,而 README 说缺工具会「自动装」一部分 —— 但不是全部(JaCoCo 需要在 pom.xml 里配 Maven plugin,crapper 不会替你改 pom)。 对应地:
A missing tool or a failed test run scores that language at 0% and still writes the snapshot.
测试失败 → 覆盖率 0% → CRAP 全线飘红。 这个设计我认为是对的,但实际用起来,你分不清「代码真的复杂且没测」和「我的测试跑不起来」。 两种情况都是红色,但处理方式完全不同。 这是工具的一个真实可用性缺口。
8.6 「AI 遵循准则」本身没有保证
这是最根本的一条。
ub-mode 假设 agent 会读 SKILL.md、会照做。 但没有任何机制强制。 agent 可以只读一部分、声称跑了 crapper 但其实没跑、跑了但只改数字不改根因(我开头那个 bug 就是这样)。
SKILL.md 的 Keep a Log 那条(记录每个动作和理由)是人肉审计手段,不是自动校验。
真正的强制力在外部:CI 里的 --threshold 30 会挡住 merge。 ub-mode 本身不提供强制,它提供的是「让 agent 有资格被判定」的标准。
这意味着:只用 ub-mode 而不接 CI,等于没用。 这是我在整个方案里看到的最重要的一句话。
8.7 它是个人的方法论,不是社区共识
「First Make it Work 然后 Then Make it Right」这个顺序,跟大多数测试驱动开发的实践是反的(TDD 通常被理解为「做对」的一部分)。 虽然 SKILL.md 里的 TDD 循环保留了「先写失败测试」,但整体优先级排序是 Uncle Bob 个人裁定的。而它自己的 README 也承认:
It is loosely based on portions of @poteto’s rules … reduced and paired with deterministic quality-gate tools.
loosely based、reduced —— 作者自己标了这是裁剪版。 照着做的人得知道自己在用哪一版。
九、适合谁 / 不适合谁
适合谁
1. 已经在用 AI 编码 agent,并且踩过「agent 说重构完了但其实没做对」的坑的人
这是最精准的匹配。你需要的是「agent 自证清白」的手段,而 CRAP / 变异存活率正是那个手段。 如果你的痛点是「我不知道怎么验收 agent 的输出」,ub-mode 给了你答案。
2. 写 Clojure 的人 —— crapper /dryer/mutator 的 Clojure 支持(crap4clj、dry4clj、clj-mutate 血统)最厚,uml-viewer 本身是 Clojure 写的。
3. 多语言团队的架构负责人 —— 它给了一个跨语言的统一指标。 Java 团队和 Go 团队用同一套 CRAP 公式、同一套阈值,你能横向比较。
4. 想要「架构讨论界面」的人 —— 试过用 PlantUML 画架构图然后发现没人维护? uml-viewer 的思路(图 + agent 联动)值得试。
5. 单人开发者,没有 code review 机制 —— CI 门禁对你尤其重要,因为没人会 catch agent 的花招。 数字不会说谎,也不会疲劳。
不适合谁
1. 刚学编程的人 —— ub-mode 假设你已经知道圈复杂度、幂等、依赖倒置是什么。 它是约束工具,不是教学工具。 对新手来说,这 25 条准则会变成一堆无法执行的教条。
2. 一次性脚本 / 玩具项目 —— 3 行的 shell 脚本不需要 CRAP。 mutator 首次全量跑变异测试的成本远超收益。 门禁的固定成本对小项目是致命的。
3. 前端 UI 密集的项目 —— crapper 对 TS/JS 的处理不错,但它数的是逻辑复杂度,不是组件耦合、不是渲染性能、不是 bundle 大小。 UI 项目的质量痛点(状态管理混乱、组件复用率低)这套工具基本测不到。
4. 数据科学 /notebook 项目 —— ipynb 的复杂度怎么算?没定义。 变异测试对数据管道也不适用。
5. 不想碰 CI 的人 —— 这是我最想强调的「不适合」。 ub-mode 的价值 90% 来自「数字 + 退出码 + CI 门禁」这个闭环。 你要是只在本地跑一下看看,不接进 CI,那 agent 随时可以绕过它 —— 因为 SKILL.md 里的条款没有强制力。
只用不接 CI = 只用了一半,而且是不重要的那一半。
6. 期待「开箱即用的 AI 代码质量方案」的人 —— uml-viewer 依赖 tmux、Grok CLI、Clojure CLI、Java 21+、JVM 启停协议。 crapper/mutator 需要特定版本的 tree-sitter 和语言工具链。 这不是 npm install 就能用的东西,是需要自己组装的工作流。
十、我的实际打算
我把 ub-mode 当成思想框架吸收,而不是当成工具直接装。
原因是我自己项目的情况:Python 单语言 + 小团队 + 主要在写 agent。 mutator 对 Python 的支持在,但首次全量跑变异测试在我这个规模也要几十分钟;CI 里接 --threshold 我倒是很愿意做。 我真正打算拿走的是三条:
第一条:CRAP 作为单一指标。 我现在的代码评审全靠「读一遍」,评审意见零散且不一致。 换成「这个函数 CRAP 多少」,一眼就能排序。这不是新东西(crap4java 有 338 stars),但我是在有了 AI 编码 agent 之后才重新需要它 —— 因为现在改代码的不是人。
第二条:Attack the Premise。 这条我会直接写进我自己的团队规范。它不只对 agent 有效,对人也有效。我自己被「两次同样失败」坑过太多次了。
第三条:Keep a Log 的精神,但配工具。 SKILL.md 说「记录每个动作和理由,完成时报告」。 我打算把它变成硬要求:agent 的每轮输出必须包含「跑了哪些命令、命令输出是什么」。 不是让它写总结(总结会美化),而是让它贴原始输出。 这一条是我自己加的,因为 我信不过「记录」这件事的自愿执行。
最后说一句可能的偏见。
5,256 stars,4 个 commit,2 个文件。 这个数字组合很反常。 通常 star 数意味着「有很多人在用」,但 ub-mode 的 star 数几乎肯定是「很多人在读、在讨论、在转发」—— 而不是「很多人在生产环境跑」。 它发布才 6 天(2026-10-25 建仓,今天 10-31),6 天 5.2k star 是个爆发式传播。
6 天。 你得把它当新东西看。
它的思想已经成熟了 —— Uncle Bob 的方法论写了三十年,工具链(crap4clj 2026-02、crap4java 2026-03、dry4clj 2026-05、crap4go 2026-05、companion 工具 2026-09)也都存在半年到一年了。 ub-mode 做的是把它们重新打包成一个 agent 能加载的 skill,并且补上了「一致性」这一环 —— 之前你得自己装 7 个工具,现在 README 直接指了 4 个。
所以我的判断是:这个仓库会继续涨 star,因为它讲的故事太好了 ——「让 AI 按 Clean Architecture 写代码,而且用数字验证」。 但工具链的成熟度还差一截,尤其是 uml-viewer 那套 tmux + Grok 的编排。
值得现在就读,值得现在就用 crapper 试试,但别指望明天就能让 Claude Code 自动遵守。
附:一页速查
1 | # 装 ub-mode skill |
核心公式与判读区间
1 | CRAP = CC² × (1 − coverage)³ + CC |