@obie:周五夜特刊,透露下周即将揭晓的重大惊喜线索!

X AI KOLs Timeline 工具

摘要

tools4imps 发布了 exhale-ruby,一个专为 Rails 打造的代码收缩工具包。它在 Prism/Herb 之上整合了 dryer 和 flay,用于检测 AI 智能体引入的重复代码,并以 `exhale dry` 作为其首个 PR 检查关卡。它会将代码单元标准化,通过稀有指纹 Jaccard 加权算法计算相似度,并将发现的问题标记为「新引入」「发生位移」或「原本就存在」。

周五夜特刊,透露下周即将揭晓的重大惊喜线索!https://t.co/LHwGzyJApA
查看原文
查看缓存全文

缓存时间: 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。

相似文章