@obie:周五夜特刊,透露下周即将揭晓的重大惊喜线索!
摘要
tools4imps 发布了 exhale-ruby,一个专为 Rails 打造的代码收缩工具包。它在 Prism/Herb 之上整合了 dryer 和 flay,用于检测 AI 智能体引入的重复代码,并以 `exhale dry` 作为其首个 PR 检查关卡。它会将代码单元标准化,通过稀有指纹 Jaccard 加权算法计算相似度,并将发现的问题标记为「新引入」「发生位移」或「原本就存在」。
查看缓存全文
缓存时间: 2026/10/03 08:54
周五夜特别版,敬请期待下周的大揭晓!https://t.co/LHwGzyJApA
tools4imps/exhale-ruby
来源:https://github.com/tools4imps/exhale-ruby
exhale
exhale 是面向 Rails 的收缩工具集(contraction toolkit),负责“不耐烦编程“(Impatient Programming)中呼气阶段的检查。它的首个检查命令是 exhale dry。当代码库中残留着 Contract(契约)未保留的重复代码时,它会让拉取请求(Pull Request)失败,并告知负责清理的智能体(agent)每一处副本应该折叠进哪份原始代码。
智能体默认会复制代码。它们阅读代码库,找到一种可行的写法,然后照搬。当拉取请求在没有人逐一审阅 diff 的情况下合入时,那份复制的代码就会进入 main,除非有一台机器拦住它——而之后的每次会话又会把它再复制一遍。exhale 就是这台机器,服务于呼吸中呼气的那一半:先展开去学习,然后在同一个拉取请求内,把你学到的东西收缩回已有的结构之中。
exhale dry 结合了 Uncle Bob 的 dryer(https://github.com/unclebob/dryer)和 Ryan Davis 的 flay(https://github.com/seattlerb/flay),并基于 Prism(https://github.com/ruby/prism)和 Herb(https://herb-tools.dev)重建,因此它能像 Rails 书写它们的方式一样,阅读现代 Ruby 和 ERB 代码。这是 0.1 版本中唯一的检查。CRAP 评分检查和守卫泄漏(leaked-guards)检查已在计划中。
安装
# Gemfile
group :development, :test do
gem "exhale", require: false
end
bundle install
bundle binstubs exhale
bin/exhale dry
exhale dry 在代码库干净时退出码为 0。当关卡失败时退出码为 1:存在未保留的重复代码,或者某条 Contract 条款已过时,或指向了并不存在的代码。当它无法运行时退出码为 2,通常是因为某个文件无法解析。
比较的对象是什么
比较的单元(Unit)包括方法、Rails DSL 调用的代码体(如 scope、validate、各种回调、before_action 代码块),以及 ERB 模板。在每个单元内部,它还会比较片段(fragment):代码块、条件分支、HTML 元素,以及从方法中间截取出来的三行及以上连续语句。
像 has_many 和 validates 这样的类体声明永不计入比较,db/、config/、vendor/、tmp/ 下的内容也不计入。测试默认排除,除非你传入 --include-tests——因为测试应该满足 DAMP,而不是 DRY。
归一化(normalization)会保留操作的名称,去掉实体的名称。调用点的方法名会被保留,运算符、HTML 标签以及 Stimulus 的 data-controller 值同样被保留。局部变量、实例变量、常量、符号和字面量则会变成占位标记。以下两个方法的形状是相同的:
def alpha(xs)
ys = xs.select(&:odd?)
ys.map(&:succ)
end
def beta(items)
kept = items.select(&:even?)
kept.map(&:pred)
end
评分方式
归一化后单元的每个子树都是一个“指纹“(fingerprint)。每个指纹按照其稀有程度加权,公式为 ln(1 + 1000 / count),两个单元的得分基于这些权重的 Jaccard 相似度。一种在每个控制器中都出现的形状权重几乎为零,因此脚手架生成的代码不会淹没真正的重复。
默认阈值为 0.80,下限为 4 行代码和 20 个归一化节点。
关卡(Gate)
每次运行都会扫描整个代码库。main 分支本身也要通过同一道关卡,因此拉取请求触发的任何问题都是它自己造成的。exhale dry 只在基于合并基点(merge base)比较时用于标注发现:
| 标注 | 含义 |
|---|---|
| Introduced(新引入) | 拉取请求改动了其中一侧,而在基点处该对代码并不匹配 |
| Shifted(偏移) | 两侧都未被改动,但拉取请求的变更改变了权重,使该对代码越过了阈值 |
| Already there(已存在) | 该对代码在基点处就已经匹配 |
报告还会列出该拉取请求收缩了哪些内容:在基点处匹配、现在不再匹配的代码对。
一个已有的应用在首次运行时不会全面通过。exhale dry --introduced-only 只对新引入和偏移的发现进行把关,其余列为警告。一旦 main 分支干净了,就去掉这个标志。
Contract(契约)
有意为之的重复是一种设计决策,因此它会在 Contract 中连同理由一起声明。Contract 按原语(primitive)组织,每种分析在原语内部都有自己的文件:
contract/
provider_adapter/
README.md 该原语的说明文字,外加一个 covers 代码块
duplication.md 它保留的重复代码、原因,以及它的配置
parallel 代码块声明有意保持并行的单元,它的理由就是它所在的章节:
## Adapters stay independent
Each provider adapter stays independent. Providers change on their
own schedules, and a shared base class would couple their releases.
```parallel
Payments::*::Adapter
原语 `README.md` 中的 `covers` 代码块指明属于它的代码,`duplication.md` 中的 `settings` 代码块则为这些代码覆盖默认配置:
```markdown
```covers
Payments::*::Adapter
views/payments/**
threshold: 0.75
min-lines: 6
min-nodes: 30
一条指向已不复存在之代码的条款会让关卡失败,已无可保留内容的条款同样如此。Contract 要么保持真实,要么构建就一直亮红灯。
## 在 CI 中
```yaml
exhale:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: exhale
shell: bash
run: bin/exhale dry --base origin/${{ github.base_ref || 'main' }} | tee -a "$GITHUB_STEP_SUMMARY"
fetch-depth: 0 让 exhale dry 拥有查找合并基点和执行 git blame 所需的完整历史。exhale dry --format json 以数据形式输出同样的发现,--format edn 则以 dryer 写入 .metrics/dry.edn 的格式输出。
确定性
同一份提交在任何机器、任何一天都会得到相同的判定结论。判定只读取该提交的树、它的 Contract,以及它 Gemfile.lock 中的 gem 版本,别无其他。摘要(digest)不做随机种子处理,权重是不依赖平台浮点对数函数的定点整数,所有平局都按稳定的键来裁决。
缩小运行范围
exhale dry PATH... 只报告位置在这些路径之下的发现。缩小范围的运行仍然会把关,依据是其路径范围内的发现;并且每条路径都必须存在,因此像 exhale dyr 这样的拼写错误会以退出码 2 结束,而不是通过。
exhale 如何约束自己
exhale 拥有自己的 Contract,位于 contract/:11 个原语(source、unit、shape、fingerprint、matcher、sweep、gate、clause、report、revision 和 cli)下共有 64 条编号义务(obligation)。每条义务都至少有一个测试,通过 # Contract: / 注释将其指名,而 rake contract 会发布 Contract 覆盖率,并在任何义务缺少可执行测试时让构建失败。
CI 还会在 exhale 自身上运行 exhale dry,读取它自己的那份 Contract。
变异关卡会针对 lib/ 让 Mutineer(https://github.com/davidteren/mutineer)能生成的每一个变异体跑完整套测试。每一个变异体要么被某个测试杀死,要么被列入 .mutineer.yml 并注明没有任何测试能捕获它的原因:等价变异体、无限循环,或是 Ruby 覆盖率工具看不见的某一行。bin/mutate 在 Ruby 3.4 下运行它。
0.1 版本的已知局限
- 意图层面的重复——同一个想法用不同结构书写——结构匹配无法触及。
- 当一个方法整体作为嵌套
def被复制进另一个方法时,如果仅其代码体低于大小下限,就不会被报告。 - 每次运行都会冷启动扫描当前分支(head),只缓存合并基点。精确的增量扫描将在 0.2 版本中推出。
- 一段被复制超过 50 次的语句序列,会以它出现最广的那份副本为中心连接成星形结构,这可能漏掉只与另一份副本匹配的近似副本。
许可证
MIT。版权所有 Obie Fernandez。
相似文章
@obie:我和 Stephen 在 ZAR 的合作非常紧密,在这篇文章中他深入探讨了我们在学习以呼吸般的节奏使用编码智能体时所发现的基本原理……
exhale-ruby 是一个面向 Rails 的代码收敛工具箱,它能在拉取请求中检测重复代码,并引导编码智能体将重复副本折叠回已有代码中,其内置的 DRY 检查基于 Prism 和 Herb 构建。
@obie:exhale 0.2.0 已发布。新功能:exhale complexity 会让让 Ruby 方法变得更难理解的 PR 检查失败,并告诉你的 agent……
exhale 0.2.0 为 Ruby gem 新增 exhale complexity 功能,基于认知复杂度算法在 PR 中检测代理式 AI 逐步引入的代码复杂化,并给出可修复提示,而非仅对遗留代码设固定阈值。
@mattpocockuk: 将 Martin Fowler 的《重构》中的一组代码异味添加到我的 /review 技能中:Mysterious Name, Duplicated Code, Fe…
Matt Pocock 宣布将 Martin Fowler 的代码异味添加到他的 /review 技能中,该技能用于 AI 编码代理,并分享了他的开源可组合代理技能集合,旨在改善实际工程工作流。
@obie: flinch 0.1.0 已发布。当你的 Contract 所覆盖的代码可能被破坏、而没有任何 Contract 测试注意到时,它会让拉取请求失败。当……
flinch 0.1.0 发布:这是一款 Go 开发者工具,当 Contract 所覆盖的代码被破坏而没有任何 Contract 测试捕获到时,它会使拉取请求失败。这是 Impatient Programming 在 exhale 和 qualm 之后推出的第三款工具。
@obie: 下一组精心设计的规范和原语将存在于比实现代码更高层次的抽象中 …
这条推文讨论了未来的软件规范和原语将如何在比实现代码和单元测试更高层次的抽象中运作,参考了Rails World的使命来构建这类事物。