Redmine per-commit detection probe — does Rigor catch real bugs?
日付: 2026-05-21。Rigor: v0.1.9(master)。
対象: redmine/redmine、6.0.0 → 6.1.2の期間。
Redmineリリースタグsweepの姉妹編。あのsweepは — Mastodonのものと同じく — リリース済みのタグにわたるベースラインの安定性を測った。リリースタグはspecゲートを通過した母集団であるため、リリースタグsweepはRigorのバグ検出力を測れない(rigor-regression-sweep SKILL §「Phase 1」がこれを記録している)。このprobeはより細かく — バグ混入コミットで — サンプリングし、検出という問いを直接ぶつける。
既知のバグ修正コミットについて、その親(バグのある状態)をチェックアウトし、rigor checkを実行して、Rigorがそのバグの箇所で診断を出したかを見る。検出可能なバグクラス=Rigorのルールがターゲットとするもの: NoMethodError / nilレシーバー / メソッド欠落 / アリティ / 引数型。
候補プールは薄い — それ自体が一つの発見
Section titled “候補プールは薄い — それ自体が一つの発見”この期間には555コミットがある。NoMethodErrorのバグクラスについて、app/**/*.rbまたはlib/**/*.rbに触れるコミットに限定してコミットメッセージを検索すると、2件が得られる:
3712ecb01— Fix NoMethodError in IssuePriority#high and #low when no default or active priorities exist (#42066).41ed48fd7— NoMethodError when creating a user with an invalid email address and domain restrictions are enabled (#42584).
diff pickaxe(app/libの.rb diffに&. / .nil? / return if / presenceを追加し、メッセージに「fix」とあるコミット)で広げると、さらに4件が表面化した — しかしその4件はすべてロジック / レンダリングのバグ(壊れた脚注参照、SVGアイコン表示)であってnilレシーバー型バグではなく、加えて純粋なRuboCopクリーンアップが1件である。リリース期間の修正のほとんどはロジック / UI / 機能の作業であり、構造的にRigorの検出範囲の外にある。より大規模な検出研究にはdiff形状のヒューリスティックとはるかに大きなコーパスが必要だろう。メッセージのgrepだけでは、ここでの分母は2になる。
結果 — 2件中0件を検出
Section titled “結果 — 2件中0件を検出”両方のバグは本物のnilレシーバーNoMethodErrorであり — まさにRigorのcall.possible-nil-receiverルールのターゲットクラス — そしてRigorはそのどちらも修正前コミットでフラグしなかった(rigor check app lib、両ケースとも該当ファイル内の診断は0件)。
C1 — IssuePriority#high? / #low?
Section titled “C1 — IssuePriority#high? / #low?”def high? position > self.class.default_or_middle.position # NoMethodErrorenddefault_or_middle(def self.のクラスメソッド)は、デフォルト / アクティブなプライオリティが存在しないときnilを返す。nil.positionは例外を投げる。見逃した理由は、Rigorがself.class.default_or_middle呼び出しをDynamicとして型付けする — クラスメソッドの戻り値をIssuePriority | nilとして推論しない — ため、それに対する.positionにはフラグすべきnilアームが存在しないからだ。
C2 — EmailAddress.domain_in?
Section titled “C2 — EmailAddress.domain_in?”def self.domain_in?(domain, domains) domain = domain.downcase # NoMethodError when domain is nildomainは呼び出し側がnilを渡すメソッドパラメータである。見逃した理由は、ADR-5のrobustness原則に従い、Rigorはパラメータを寛容に型付けし、呼び出しサイトのフローがそれを証明しないかぎりパラメータがnilであるとは仮定しないからだ。
2件の見逃しはランダムではない — それぞれが意図的なRigorの設計選択にたどり着く:
- C1 → 解決されないメソッドの戻り値は
Dynamicにフォールバックする(漸進的型付けの最後の手段)。 - C2 → パラメータは寛容に型付けされる(ADR-5の第2項)。
これはフォルスポジティブ規律の裏面である(overview.md §「False-positive
discipline」)。リリースタグsweepはRigorが動いているコードを怖がらせないことを示した — surfacedは2プロジェクトのリリースラインを通じてゼロまたはゼロ近くにとどまった。このprobeは、その同じ寛容性のコストを示している: 潜在的なNoMethodErrorバグクラスに対する低い再現率(recall)。正直なところを合わせた全体像は — Rigorが提供する価値は適合率(precision、動いているコードでのフォルスポジティブが少ないこと)であって、再現率(recall、あらゆる潜在的なクラッシュを捕まえること)ではない。それは動いているコードを尊重する解析器であって、網羅的なクラッシュ発見器ではない。
- ごく小さなサンプル(n=2)。結果は定性的なものであり — 「Rigorはこのバグ形状を見逃す、そしてその設計上の理由はこうだ」 — 較正された検出率ではない。
- メッセージのgrepは
NoMethodErrorとラベルされていないバグを見逃す。本物の検出率研究には、diff形状のバグクラスヒューリスティック、より大きな期間、そして2つ目のプロジェクト(Mastodon)が必要である。
検討に値するフォローアップ
Section titled “検討に値するフォローアップ”- C1は手の届く範囲にある可能性がある。ボディがnil許容な値を返す
def self.mは静的に解析可能であり、その戻り値をT | nilとして推論し(それをself.class.mを通じてスレッドする)れば、nilレシーバールールを発火させられるだろう。ADR-24のselfメソッド解決の作業に隣接している。スケジュールはされておらず、適合率・再現率の問いとしてキュー済み。 - C2は見逃したままにすべきである。寛容に型付けされたパラメータをnilリスクありとフラグすることはADR-5に矛盾し、フォルスポジティブ規律という価値が禁じているまさにその防御的コードへの圧力を再導入することになる。C2を安全に捕まえるには、呼び出しサイトのnilフローをパラメータへとたどる必要があり — FPリスクが高い。規律をそれと引き換えにする価値はない。
~/repo/ruby/rigor-survey/redmine/(クローンしたチェックアウト)+
~/repo/ruby/rigor-survey/_redmine-sweep/rigor-no-as.yml。各コミットCについて: git checkout C~1、その後rigor check --config rigor-no-as.yml app lib。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.