コンテンツにスキップ

Real-world Rails project survey (2026-05-15)

v0.1.5進行中(コミット642cf28以降)の状態で、4つの実世界Railsコードベースに対してrigor checkを走らせた走り書き記録です。目的は2つあります。

  1. 解析器の到達範囲を測定する。仕様コーパスに含まれていなかったRails型のコードに対する、ウォール時間、ピークメモリ、診断ミックス、プラグインのカバレッジギャップなど。
  2. エンジンのバグや人間工学的なギャップをあぶり出す。合成フィクスチャやrigor自身のlib/では現れないもの。各プロジェクトはエンジンの異なる部分(サイズ、メタプログラミングの深さ、gemの表面積、モンキーパッチの密度)に違ったストレスを与えます。

方法論と対象サイズ。

プロジェクト状態ファイル数備考
Redmine完了3474つのうち最小 — エンジンバグの試運転に使用。
Discourse完了1,804フォーラム基盤。プラグイン/フックの表面積が大きい。
Mastodon完了1,302ActivityPubソーシャルサーバー。ActiveJob/Sidekiqを多用。
GitLab FOSS完了11,130元の4つのうち最大 — 深いメタプログラミングを持つRailsモノリス。
Forem完了(第2ラウンド)1,250DEV.toコミュニティ基盤。
Solidus完了(第2ラウンド)1,914Eコマースモノレポ(core + api + backend + admin + promotions + legacy_promotions)。
Chatwoot完了(第2ラウンド)802カスタマーサポート基盤。
Canvas LMS完了(第2ラウンド)3,248InstructureのLMS。app + lib + gems(インツリーのgem)。
OpenProject完了(第2ラウンド)6,817プロジェクト管理基盤。app + lib + modules(サブエンジン)。
Loomio完了(第3ラウンド)563協働/グループ意思決定のRailsアプリ。
Publify完了(第3ラウンド)15(アプリシェルのみ)Railsアプリシェル。実コードは外部のpublify_core gemにある。
Diaspora完了(第3ラウンド)371連合型ソーシャルネットワークRailsアプリ。
Dependabot Core完了(第3ラウンド)1,089(19エコシステムディレクトリにまたがる)Rails以外 — 依存性自動更新のためのRuby SDK/ライブラリ。「Bundler内部を多用する非Rails慣用句のRubyに対して解析器はどう振る舞うか?」のベースラインとして有用。
tDiary Core完了(第3ラウンド)244(lib + plugin + entryスクリプト)Rails以外 — Rails以前のRubyブログエンジン。「ActiveSupportがスコープに無い古典Rubyの慣用句に対して解析器はどう振る舞うか?」のベースラインとして有用。

各パスは次のように走らせます。

Terminal window
nix --extra-experimental-features 'nix-command flakes' develop \
--command bundle exec exe/rigor check --format=json \
/tmp/<project>/app /tmp/<project>/lib > <project>.json

逐次/ウォームキャッシュ実行が主要な計測対象です。プールモード(--workers=N)は等価性チェックとタイミング比較のためにのみ実行します。「ウォーム」とは、同一リビジョンに対する事前の逐次パスでプロジェクト毎の.rigor/キャッシュが構築済みであることを意味します。


ソース: https://github.com/redmine/redmine(depth-1の浅いクローン、特定リビジョンへの固定は無し — 2026-05-15時点のmaster HEAD)。

この調査が引き起こした2つのエンジン改善(コミット642cf28)の前後で2つのスナップショット。

メトリクス642cf28以前642cf28以後Δ
スキャン対象ファイル347347
ウォール時間(ウォームキャッシュ)2.82 s2.82 s
ウォール時間(コールドキャッシュ)3.77 s3.77 s
ピークRSS266 MB~268 MB
診断件数合計389343−46
エラー334288−46
警告5555

46件の減少は1つのルール群のみに由来します。call.possible-nil-receiverが69→23。他のどのルール件数も動かず、新しい診断も導入されていません。(プールモード比較は後述。)

件数ルール
243call.undefined-method
23call.possible-nil-receiver
24flow.always-truthy-condition
22(パースエラー — Rails generatorのテンプレートファイル。後述参照)
17flow.dead-assignment
14def.ivar-write-mismatch

642cf28の深いshareability修正の後、Redmineに対する--workers=4は逐次とバイト単位で同一の診断ストリームを生成します(狭化前のスナップショットで389 == 389、その後で343 == 343)。タイミング。

モードウォール時間ピークRSS
逐次(ウォーム)2.82 s266 MB
プールworkers=4(ウォーム)3.70 s948 MB
プールworkers=8(ウォーム)8.39 s1.60 GB

このサイズのプロジェクトではプールモードは依然として誤ったデフォルトです。Ractor毎のenvビルド+Marshal復元が、ファイルあたり~10 ms推論の並列スピードアップを圧倒します。これはADR-15 OQ1の注意事項と一致します。プール経路は今や正しい(IsolationError無し、バイト単位で同一の出力)。いつ「より速く」なるかという問いは未解決のままです。

call.undefined-method — Rails拡張のロングテール

Section titled “call.undefined-method — Rails拡張のロングテール”

243件のほとんどはstdlibのRBSに無いActiveSupport/Railsのコア拡張です — 実際のバグではありません。

件数セレクタ/レシーバー由来
75String#html_safe, "…".html_safeActiveSupport::SafeBufferミックスイン
24Array.wrap(...)ActiveSupport::CoreExtensions::Array::Wrapper
12Time.parsestdlib time — ユーザーコードにrequire 'time'が欠落
6+Hash#deep_dup, Hash#symbolize_keysなどActiveSupportのHashコア拡張
6+Integer#days, #minute, #day, #year, #secondsActiveSupport::Duration
4String#constantizeActiveSupportのStringコア拡張
2+String#underscore, #demodulize, #to_hoursActiveSupportのStringコア拡張

専用のrigor-activesupport-core-extプラグインが予定された対応策です。モンキーパッチの事前評価のためのconfigノブが、プロジェクト固有のパッチに対する補完的な対応策です。両方ともエージェント側のメモリストア(project_activesupport_core_ext_plugin)に将来の方向として記録されていますが、コミットされたマイルストーンはありません。

バグ発見価値のある所見(Rails非依存)

Section titled “バグ発見価値のある所見(Rails非依存)”
  • call.possible-nil-receiver 23(修正後)。残りのケースのほとんどは、ループ反復パターンか、ガードの後に再束縛される形で、狭化がまだカバーしていないものに見えます。まだフラグされるパターン例。

    if cond
    val = compute # nullable
    next if val.nil?
    val.attr # narrowing across `next if` not yet flow-tracked
    end
  • def.ivar-write-mismatch 14。同じインスタンス変数が異なる代入で非互換な型に束縛されています。User#@projects_by_roleNilClass/HashWiki#@page_found_with_redirectFalseClass/TrueClassなど。メモ化済みかまだ計算されていないかという状態を表すRubyの慣用句で、警告として使えますが境界的にノイズが多いです。

  • flow.dead-assignment 17。複数の_controller.rbアクションといくつかのヘルパーで、未読のローカル変数代入があります。上流に報告する価値あり: 例えばapp/controllers/issues_controller.rb:401bulk_update内でjournalを代入していますが読みません。

  • flow.always-truthy-condition 24。定数畳み込みされた分岐。例: app/controllers/repositories_controller.rb:427-429

Redmineが駆動したエンジン改善(既にランディング済み)

Section titled “Redmineが駆動したエンジン改善(既にランディング済み)”

642cf28 — 「Bank Redmine real-world findings: pool shareability + assignment narrowing」。

  1. プールモード(Phase 4b.xフォローアップ): 3つの深いshareabilityギャップがこのプロジェクトでワーカーRactorのIsolationErrorによって表面化:

    • NumericCatalog#@catalog(YAMLグラフを深く共有化)
    • Type::Refined::CANONICAL_NAMES(ネストされたArrayキー)
    • Builtins::RegexRefinement::RULES(ネストされたArray行)
  2. if cond && (var = expr)の狭化Inference::Narrowing#analyseに4つの新しい書き込みノードケース(LocalVariableWriteNode + ivar/cvar/global)。Redmineではこれがcall.possible-nil-receiverを69→23に減らし、回帰はゼロでした。

フォローアップトラックに繰り越されたオープン項目

Section titled “フォローアップトラックに繰り越されたオープン項目”
ID項目
O1active_support/core_extプラグイン + config側モンキーパッチ事前評価。(メモリ: project_activesupport_core_ext_plugin。)
O2マクロテンプレート展開(ERB .rbテンプレート、class_eval <<~RUBYヒアドキュメント)— lib/generators/redmine_plugin_model/templates/migration.rbの22件のrb-with-erbパースエラーも回復させる。(メモリ: project_macro_template_expansion。)
O3同一ブロック内の早期離脱ガードを越えたnext if x.nil?return if x.nil?のフロー追跡狭化。

Bundlerを意識した解析 — 今日のO4の探索

Section titled “Bundlerを意識した解析 — 今日のO4の探索”

調査の自然な続き: rigorはプロジェクトのgemもスコープに入れた状態でプロジェクトを解析できるか? — つまりUser.where(...)がActiveRecordのwhereに解決され、Sidekiq::Worker#performがSidekiqのRBSにマッチする、といった具合に。

Dockerベースのbundle installデモンストレーション(Mastodon、2026-05-15)

Section titled “Dockerベースのbundle installデモンストレーション(Mastodon、2026-05-15)”

bundle installのハードルに続けて、ruby:4.0.3-slim-trixie(Docker)の中で実際にMastodonのbundle installを実行しました。エンドツーエンドで動作しました — aptの依存関係がインストールされ(libpq-devlibidn-devlibxml2-devlibxslt-devlibvips-devlibjemalloc-devgitなど。1つのパッケージ名変更: Trixieはlibpcre3-devではなくlibpcre2-devを提供)、Bundler 4.0.11がインストールされ、bundle install --jobs=8が343個のgem全てを解決し、/tmp/mastodon-bundle(271 MB)に展開しました。

重大な所見: インストールされた343個のgemのうち、sig/ディレクトリをgemパッケージに同梱しているのはわずか10個(約3%)です。全リスト:

prism, aws-sdk-s3, aws-sdk-kms, aws-sdk-core,
playwright-ruby-client, mutex_m, webrick, base64,
stoplight, ffi

pgmysql2nokogiribcryptredisidn-rubyactionpackactiverecordactivesupportsidekiqdevisepunditkaminaripuma、そしてその他の人気のあるRails/認証/キャッシュ/キューファミリーのgemパッケージにはsig/ありませんgem_rbs_collectionが、これらのgemに対する型付き契約の事実上のソースです。

このデータポイントはコミットf9b94d2の設計判断を強化します。よく使われる半ダース程度のネイティブ拡張gemに対するベンダーRBSをrigor自身に同梱することが、現実的な「すぐ使える」道です。そうでなければエンドユーザーはgem_rbs_collectionsignature_paths:にgemバージョン毎に手動で配線する必要があり、その配線はいずれにせよO7(後述)にぶつかります。

オープン項目O7(RBSのenvビルドの崖)は当初の評価より深刻です。rigor自身のロード済みsig上にgem同梱のsig/ディレクトリを1つだけ(具体的にはprismのsig、約19個の.rbsファイル)追加すると、RBS::Environment.from_loaderは5分以上ハングします(強制終了)。Diasporaの16-paths-coldの実験(11分以上)は同じ症状でパス数がより多いものです。もっともらしい説明: prismのsigはrigorの事前ロード済みprism RBS(rigorは内部でprismを使用)と重複するクラスを宣言しており、リゾルバが重複クラスのグラフ走査で爆発します。O7が修正されるまで、gem同梱のsig経路は使えません — bundle installが成功してもです。

最新の状態(コミットf9b94d2でベンダーRBSがランディング後)

Section titled “最新の状態(コミットf9b94d2でベンダーRBSがランディング後)”

rigorは現在、6つの一般的なネイティブ拡張gemに対する組み込みRBSスタブpgmysql2nokogiribcryptredisidn-ruby)を同梱しています。スタブはdata/vendored_gem_sigs/<gem>/配下にあり、自動的にロードされます — signature_paths:設定は不要です。すぐに使える状態で、利用可能なRBSクラスは1,134から1,273(+139)に増加しました。

14の調査プロジェクト全体に対する定量的影響。

プロジェクトベースラインO1 v2を入れた状態+ ベンダースタブ
Redmine389157157
Discourse1,439423429
Mastodon521124124
GitLab FOSS2,982489491
Forem691146149
Solidus5284242
Chatwoot3001921
Canvas LMS3,2961,4961,506
OpenProject2,356175176
Loomio2076363
Publify000
Diaspora6555
Dependabot Core2055858
tDiary Core111106106
合計13,0903,3033,327

ベンダースタブはネットで+24の増加を生みます — 追加されたRBSが本物の新しい問題と、不完全なスタブによる偽陽性の両方を捕まえる、精度/カバレッジのトレードオフです。ほとんどのプロジェクトは+0で、わずかな増加はCanvas LMS(+10)、Discourse(+6)、Forem(+3)、GitLab FOSS(+2)、Chatwoot(+2)に集中しており、いずれもベンダーされた4.2/1.11スナップショットに無いgem APIを使うものです。これらのギャップを埋めるのはgem毎の<gem>_extras.rbsファイル(nokogiri_html5.rbsredis_extras.rbsが最初の2つ)によって漸進的に行えます。

より大きな全体像での勝利。

  • Mysql2::ClientPG::ConnectionNokogiri::XML::NodeのレシーバーがDynamic[top]でなくなる — 全ての呼び出し箇所で精密なディスパッチが効くようになります。
  • Mastodonのidn-rubyブロッカーは静的解析にとって無意味になる。ユーザーは有用なMastodon解析を得るためにlibidnをシステムにインストールする必要が無くなりました。
  • すぐ使える設定: エンドユーザーは各gemのRBSをsignature_paths:に手動で配線する必要がもはやありません。

今日動くもの(ベンダリング前 — 文脈として残す)

Section titled “今日動くもの(ベンダリング前 — 文脈として残す)”

新しい解析器コード無しでも2つの経路があります。

  1. 対象プロジェクトのBundlerコンテキストの中でrigorを実行するBUNDLE_GEMFILE=<target>/Gemfile bundle exec rigor check ...とすればRBS::EnvironmentLoader.add(library: gem_name)sig/を同梱しているgemを全て見つけます。今日のRbsLoader.build_env_forlibraries:設定を介してこれを実際に尊重します — ただしユーザーはライブラリを明示的に列挙する必要があり、rigorはまだ対象のGemfile.lockからそれらを自動発見しません。
  2. signature_paths:にgemのRBSを追加するgem_rbs_collectionはコミュニティRBSリポジトリです — 2026-05-15時点で172個のgemがあり、gem毎にバージョン管理されています(例: gems/activerecord/{6.0, 6.1, 7.0, 7.1, 7.2, 8.0})。該当するバージョン毎のパスを.rigor.ymlsignature_paths:に追加すれば、rigorはそれらを拾います。
  • ネイティブgemのビルド。Mastodonに対するbundle installidn-rubyで失敗しました(Nixシェルにlibidnが無い)。Railsプロジェクトは日常的にpgmysql2nokogiriidn-rubyffiなどのシステムライブラリを必要とするgemに依存しています。エンドユーザーはBundlerコンテキストが既にビルドされる自身の開発/CIマシンでrigorを実行すれば解決します。調査マシンではそうではありません。
  • Rubyバージョンの不一致。14のサーベイ対象プロジェクトのほとんどは.ruby-versionでRuby 3.3/3.4を固定しています。rigorのFlakeは4.0.4を提供します。バージョンが不一致だとBundlerはインストールを拒否します。Mastodon(ruby '>= 3.3.0', '< 4.1.0')は調査の中で4.0.4を許容するRubyバージョン範囲を持つ唯一のプロジェクトでした。
  • gem_rbs_collectionのバージョン固定。コレクションがgems/<name>/<version>/という構造のため、ユーザーはgem毎に正しいバージョンを選ぶ必要があります。rigorはこの解決を自身では行いません — それがO4が埋めるべき欠けたピースです。

定量的実験(Diaspora + Mastodon、中規模gemサブセット)

Section titled “定量的実験(Diaspora + Mastodon、中規模gemサブセット)”

Diaspora(Rails 6.1)でO1 v2 + 5つのgemサブセット(activerecord/6.1 + activesupport/7.0 + activemodel/7.1 + actionpack/7.2 + activejob/6.0)の場合。

メトリクスO1 v2のみ+ 5-gem RBSサブセット
利用可能なRBSクラス1,0392,478
コールドウォール時間1.35 s9.47 s
ウォームウォール時間(該当なし)1.05 s
診断53

Mastodon(Rails 8)でO1 v2 + 同じサブセットの場合。

メトリクスベースラインO1 v2+ 5-gem RBSサブセット
利用可能なRBSクラス1,0391,0392,505
コールドウォール時間3.31 s(同程度)12.39 s
診断521124128

Mastodonの診断件数はgemサブセットの下でわずかに増加しました — 教科書的な精度/カバレッジのトレードオフ: 既知のRBSが増えればより多くのメソッドをチェックでき(よってAR/AS-Inflectorなどの残存call.undefined-methodが約0に下がる)、かつより多くのnullableレシーバー狭化が正しく発火します(call.possible-nil-receiverが約70から97に上昇)。新しい診断のうちいくつかはrigorが以前見えなかった本物のバグでしょうし、いくつかはgemのRBSそのものが厳格すぎる偽陽性(典型的にはStringと宣言された入力に実際の呼び出し元はActiveSupport::SafeBufferなども渡す)でしょう。

オープンなパフォーマンス問題(上記の項目O7)

Section titled “オープンなパフォーマンス問題(上記の項目O7)”

Diasporaで10個以上のgem RBSの.sigをsignature_paths:に同時に入れてコールドロードすると、11分以上経過後に強制終了されました。同じワークロードを5パスで行うと7〜9秒で完了します。RBS::Environment.from_loaderresolve_type_namesで重複する名前空間が多数収束した際の非線形な相互作用の可能性があります。O4がランディングする前に調査する価値あり — 実世界RailsプロジェクトのGemfile.lockは通常50〜150個のgemを列挙しており、5個ではありません。

  • 解析対象パスの隣にあるGemfile.lockを自動発見する。
  • gem毎のバージョン解決: Bundler.locked_gems.specs.find { |s| s.name == "activerecord" }.version -> gem_rbs_collectionの利用可能バージョンにマッチさせるか、「生」のRBS envにフォールバックする。
  • gem毎のRBSソース解決: gem内のsig/を優先(一部のgemは独自に同梱している)、gem_rbs_collectionにフォールバック、最終フォールバックはADR-10のdependencies.source_inferenceウォーカー。
  • キャッシュ: Gemfile.lockのダイジェスト毎に1つのRBS::Environmentキャッシュスロットを、gem毎のバージョンタプルでキーとして持つ。
  • gemのRBSが利用不能なときの優雅な低下メッセージ(ユーザーがインストールするか、ソース推論をオプトインすべきと知るため)。

第3ラウンドのプロジェクト(Loomio/Publify/Diaspora/Dependabot Core/tDiary Core)

Section titled “第3ラウンドのプロジェクト(Loomio/Publify/Diaspora/Dependabot Core/tDiary Core)”

第3ラウンドの一掃。小/マイクロ/中規模のRailsアプリ3つ(Loomio/Publify/Diaspora)と、解析器とActiveSupport型のRBSバンドルがRailsの慣用句の外でどう振る舞うかを較正するためのRails以外のRubyプロジェクト2つを含みます。

プロジェクトファイル数ウォール(ウォーム)ピークRSSベースラインO1 v2を入れた状態Δ
Loomio5632.36 s238 MB20763−144(−70%)
Publify(アプリシェルのみ)150.66 s243 MB000
Diaspora3711.35 s258 MB655−60(−92%)
Dependabot Core(非Rails)1,08913.02 s226 MB20558−147(−72%)
tDiary Core(非Rails)2441.61 s254 MB111106−5(−5%)

5つ全てでプール ≡ 逐次(IsolationErrorゼロ)。

注目すべき所見(第3ラウンド)

Section titled “注目すべき所見(第3ラウンド)”
  • PublifyはただのRailsアプリシェルapp/ + lib/配下に15個の.rbファイル)。実際のPublifyコードはgem "publify_core", github: ...で参照される外部のpublify_corepublify_amazon_sidebarpublify_textfilter_codeのgemにあります。rigorはこのリポジトリにチェックインされたものしか見ないため、診断件数はゼロです — 有用な境界ケースですが、Publify本体を代表するものではありません。
  • DiasporaはサーベイのRailsアプリの中で最もクリーン — O1 v2の後で371ファイルに対し5件の診断。
  • Dependabot Core(非Rails)でもActiveSupport型のバンドルから実質的に恩恵を受ける(−72%)。理由: 多くの非Ruby Railsプロジェクトは起動時にActiveSupport(またはactive_support/core_ext/...経由でその断片)をロードし、コードはRailsアプリと同じObject#blank?#present?#tryString#exclude?Enumerable#index_byの慣用句を使います。残りの58件はBundlerのSingletonクラス呼び出しBundler::Definition.build × 10、Bundler.settings × 7、Bundler::Dependency.new(...)が誤った引数数として5回フラグされる)で占められています — 全てO4(対象Bundler認識)の症状です。Dependabotはbundler/helpers/v*/monkey_patches/配下にBundlerに対する独自のモンキーパッチを同梱しており、正しく型付けするためにrigorは事前評価する必要があります。
  • tDiary CoreはO1からほとんど恩恵を受けない(−5%)。これはActiveSupport-as-utilityの慣用句より前のもので — Rubyは古典的なstdlibのみのスタイル。tDiaryの残存診断は#month=#year=セッターがon Objectとしてフラグされる(misc/plugin/category-legacy.rbで35件)で占められています。プラグインファイルは実行時にホストプラグインクラスへinstance_evalされ、defがファイルのトップレベルにあるためrigorはレシーバークラスを見ることができません — まさにオープン項目O2(ヒアドキュメント/instance_eval Ruby展開)に並んでいるマクロ展開の経路です。
  • Loomioのミックスは異常 — 63件のうち34件はflow.dead-assignment(54%)で、call.undefined-methodはわずか11件。コードベースは他より顕著にAS慣用句的でなく、RBSバンドルからの恩恵が少ないです。
  1. これまで一掃した14プロジェクト全てでプール ≡ 逐次が証明された(約29,560ファイルにわたってRactor::IsolationErrorゼロ)。Phase 4b.xの4つのshareabilityフォローアップ + CONSTANT_CONSTRUCTORSのlambda修正は、実世界の対象の多様性に対して堅牢です。
  2. ActiveSupport型のRBSバンドルは非RubyのRubyにとっても有用 — Dependabot Coreの−72%はActiveSupportの慣用句(Object#blank?ファミリー、Enumerable#index_byString#exclude?)がRailsの外でも広く使われていることを確認しました。
  3. tDiaryのinstance_evalプラグインパターンはO2の動機付け — Rails以前の時代の慣用句もRailsジェネレータの.rb-as-ERBテンプレートと同種のメタプログラミングの壁にぶつかります。

第2ラウンドのプロジェクト(Forem/Solidus/Chatwoot/Canvas LMS/OpenProject)

Section titled “第2ラウンドのプロジェクト(Forem/Solidus/Chatwoot/Canvas LMS/OpenProject)”

5つの追加Railsプロジェクトの第2ラウンドの一掃。第1ラウンドのエンジン修正(Pool deep-shareabilityフォローアップ#1〜#3、狭化拡張、パラメータ化された祖先射影、v1 RBSバンドル)の後で実行しました。

プロジェクト毎のルールミックス

Section titled “プロジェクト毎のルールミックス”
プロジェクト合計call.undefined-methodpossible-nil-receiverflow.always-truthy-conditiondef.ivar-write-mismatchcall.wrong-arity
Forem14655471527
Solidus42333411
Chatwoot1961112
Canvas LMS1,4967664451948311
OpenProject175138271184

第2ラウンドが引き起こしたエンジン改善

Section titled “第2ラウンドが引き起こしたエンジン改善”

v1 RBSバンドルは、このラウンドで表面化した5つの追加メソッドファミリーを伴ってv2に拡張されました。

  1. Array#compact_blankHash#compact_blank(Rails 6.1+)。
  2. Array#exclude?String#exclude?Hash#exclude?Enumerableも再公開)。
  3. Enumerable#index_with#index_by#pluck#pick#sole#including#excluding#without
  4. Hash.from_xmlHash#reverse_merge#reverse_merge!
  5. DateTimeの計算(#utc#in_time_zone#yesterday#tomorrow#beginning_of_*#end_of_*#ago#since)。

9つのサーベイプロジェクト全体に対する組み合わせv1 + v2の定量的影響: 合計12,502 → 3,071(-75%)call.undefined-method10,589 → 1,426(-87%)

注目すべき所見(第2ラウンド)

Section titled “注目すべき所見(第2ラウンド)”
  • Solidusのlib/の数は誤解を招く(リポジトリルートにはわずか2ファイル)。エンジンのサブツリー(core/api/backend/admin/promotions/legacy_promotions/)にコードがあります。rigorの設定は各サブツリーを明示的なパスとして列挙します。Solidusの診断件数は42まで下がります — 非常にクリーン。
  • Canvas LMSは第2ラウンドの残存を支配する(1,878件のうち1,496件 — 80%)。トップセレクタ: []= on Integer(70 — おそらくレシーバー推論が誤り)、[]= on nil(51)、<< on nil(40)。これらはRBSカバレッジのギャップではなく、狭化層の限界です。Canvasはまた、プロジェクト固有のNumeric#decimal_megabytesFile.mime_typeなどを同梱しており、そこのロングテールを閉じるにはO4(対象Bundler認識)と、.rigor.yml内のCanvas固有のモンキーパッチ宣言が必要です。
  • OpenProjectのfrom_xmlcompact_blankクラスタはv1 → v2の動機付けHash.from_xml単独でOpenProjectの残存undefined-methodsの10件を占めていました。

ソース: https://github.com/discourse/discourse(depth-1の浅いクローン、2026-05-15時点のmaster HEAD)。

定量サマリー(642cf28 + Discourseが駆動したshareability修正の後)

Section titled “定量サマリー(642cf28 + Discourseが駆動したshareability修正の後)”
メトリクス逐次ウォームプールworkers=4ウォーム
スキャン対象ファイル1,8041,804
ウォール時間7.46 s5.82 s(逐次より1.28倍速い)
ピークRSS244 MB842 MB
診断件数合計1,4391,439
エラー1,3251,325

このサイズではプールが逐次より速い — 調査が観測した最初のウォール時間でのクロスオーバー

件数ルール
1,078call.undefined-method
217call.possible-nil-receiver
61flow.always-truthy-condition
46def.ivar-write-mismatch
22call.wrong-arity
8call.argument-type-mismatch
7flow.dead-assignment

Discourseが引き起こしたエンジン改善

Section titled “Discourseが引き起こしたエンジン改善”

Discourseに対するプールの初回実行は、Rigor::Inference::MethodDispatcher::ShapeDispatch::REFINED_STRING_PROJECTIONS(2要素Symbol配列をキーとするHash — Redmineのパスで表面化したPhase 4b.xの3つのフォローアップ箇所と同じ形)へのワーカーRactorディスパッチで8件のRactor::IsolationErrorを表面化させました。現在はRactor.make_shareable済み。新しい監査アサーションが不変条件を固定します(spec/rigor/ractor_readiness_spec.rb § 「Phase 4b.x — module catalog shareability」)。修正後、プール ≡ 逐次。

  • Time.zone 182件ActiveSupport::TimeWithZone拡張。Redmineよりさらに大きいActiveSupport拡張のボリューム。
  • Integer#day#hour#minute#days#minutes#hoursActiveSupport::Durationの数値強制。数百件。
  • call.wrong-arity on Class 18件 — Discourseのサービスクラス(DatabaseRestorer.new(...)MetaDataHandler.new(...)OpenStruct.new(...))。レシーバークラスがrigorのRBS envに無いため、ディスパッチはClass#new(引数なしのデフォルトイニシャライザ)にフォールバックし、引数数を誤りと報告します。OpenStructは特にRuby 4.0でデフォルトgemの地位を失いました。DiscourseのGemfile.lockはそれをピン留めしますが、rigorの解析envは対象プロジェクトのBundlerコンテキストを見ないため、gemのRBSはロードされません。
  • call.argument-type-mismatch on URI.encode_www_form 5件以上 — RBS署名は(?Enumerable[[_, _]])ですが、実世界の呼び出し元はHashを渡します。Hashは実行時に[K, V]ペアに対するEnumerableです。rigorのサブタイピングはここでHash → Enumerable[[K, V]]の関係を認識しません。別のエンジントラックとして調査する価値あり。

Discourseが提起したオープン項目

Section titled “Discourseが提起したオープン項目”
ID項目
O4対象プロジェクトのBundler認識 — プロジェクトのbundle execコンテキストの外で実行しているときに対象のgem RBSをロードする(Ruby 4.0+のOpenStructや、出荷RBSのある非デフォルトgemをカバー)。
O5パラメータバインダー向けのHash <: Enumerable[[K, V]]サブタイピング。

ソース: https://github.com/mastodon/mastodon(depth-1の浅いクローン、2026-05-15時点のmaster HEAD)。

メトリクス逐次ウォームプールworkers=4ウォーム
スキャン対象ファイル1,3021,302
ウォール時間3.31 s3.98 s
ピークRSS238 MB878 MB
診断件数合計521521(≡ 逐次)
エラー487487

プール ≡ 逐次がすぐに成立 — 新しいエンジンバグは見つからず。このサイズではプールは逐次より遅く、クロスオーバーはMastodon(1.3 Kファイル)とDiscourse(1.8 Kファイル)の間に位置し、Marshal復元のオーバーヘッドでシフトしています。

件数ルール
414call.undefined-method
73call.possible-nil-receiver
26def.ivar-write-mismatch
8flow.always-truthy-condition
  • 同じRails拡張のロングテール(Integer#day/#hour/#minute/#minutes/#secondsString#squishTime.zone)。ランキングは異なりますが原因はRedmineやDiscourseと同一で、active_support/core_extのRBSカバレッジが欠落していることです。

ソース: https://gitlab.com/gitlab-org/gitlab-foss(depth-1の浅いクローン、2026-05-15時点のmaster HEAD)。サーベイの中で最大の対象。

メトリクス逐次ウォームプールworkers=8ウォーム
スキャン対象ファイル11,13011,130
ウォール時間(ウォーム)25.27 s15.43 s(逐次より1.64倍速い)
ウォール時間(コールド)25.33 s
ピークRSS248 MB1.30 GB
診断件数合計2,9822,983(+1。下記参照)
エラー2,8572,858

このサイズのプロジェクトではプールが逐次より十分に速いです。ピークRSSが1.3 GBになるのが代償で、逐次の5倍です。ここではクロスオーバーが確固として確立しています。将来のプールモード作業に対する問いは、遅延されたRactor毎のCache::Store共有ファサード(ADR-15 § OQ1)でRSS/ウォール時間のトレードオフをさらに動かせるか、です。

件数ルール
2,676call.undefined-method
136call.possible-nil-receiver
71def.ivar-write-mismatch
52flow.always-truthy-condition
43call.wrong-arity
2flow.dead-assignment
1call.argument-type-mismatch
1(Redmineと同じく.erb型の.rbジェネレータテンプレートからのPrismパースエラー)

プール対逐次 — 決定的な+1の乖離

Section titled “プール対逐次 — 決定的な+1の乖離”

プールは逐次が出力しない1件の診断を、workers=4workers=8と複数回の実行にわたって決定的に出します。

lib/gitlab/mail_room.rb:17:56
call.argument-type-mismatch
argument type mismatch at parameter `dir` of `expand_path` on Pathname:
expected String, got String | nil

最小再現(逐次は無音、プールは診断を出す)。

require "pathname"
x = Pathname.new("../..")
y = x.expand_path(__dir__) # __dir__ returns String | nil per RBS

__dir__のRBSの返り値はString?です。逐次はMethodDispatcher::ConstantFoldingtry_fold_pathname_binary層を介して定数畳み込みします。プールはRBSディスパッチ層に到達し、そこでパラメータチェックがString | nilを拒否します。乖離は決定的で稀(11,130ファイル中1箇所)ですが、契約はバイト単位で同一の出力です — オープン項目O6として記録。

  • Time.current 324件 — ActiveSupport。このコーパスでRails拡張の欠落としてはるかにトップ。
  • Array.wrap 228件Integer#minute 163Time.zone 125 — 小さい対象と同じactive_support/core_extのテール。比例して大きい。
  • String#demodulize 34#underscore 32#squish 37 — Inflector/ActiveSupportのStringコア拡張。
  • ユーザー定義クラスのwrong-arity問題(Discourse O4)がより大きな規模で繰り返されます。

GitLab FOSSが提起したオープン項目

Section titled “GitLab FOSSが提起したオープン項目”
ID項目
O6Pathname引数チェックでのプール対逐次の精度の乖離。逐次がtry_fold_pathname_binaryを介して畳み込むのに対しプールはRBSディスパッチに到達する。両方の経路は個別には弁護できるが、契約はバイト単位で同一の出力を要求する。

プロジェクトファイル数逐次ウォームプールウォームプール ÷ 逐次ピークRSS(逐次/プール)診断(ベースライン)
Redmine3472.82 s3.70 s(w=41.31倍遅い266 MB / 948 MB389
Chatwoot8022.67 s(異常な実行。システム負荷)該当なし274 MB / —300
Mastodon1,3023.31 s3.98 s(w=41.20倍遅い238 MB / 878 MB521
Forem1,2504.31 s4.60 s(w=41.07倍遅い260 MB / —691
Discourse1,8047.46 s5.82 s(w=40.78倍(より速い)244 MB / 842 MB1,439
Solidus1,9147.36 s4.91 s(w=40.67倍(より速い)275 MB / —528
Canvas LMS3,24817.32 s11.16 s(w=40.64倍(より速い)272 MB / —3,296
OpenProject6,81718.84 s10.24 s(w=40.54倍(より速い)246 MB / —2,356
GitLab FOSS11,13025.27 s15.43 s(w=80.61倍(より速い)248 MB / 1.30 GB2,982
Publify(シェルのみ)150.66 s(未計測)該当なし243 MB / —0
Diaspora3711.35 s(未計測)該当なし258 MB / —65
Loomio5632.36 s(未計測)該当なし238 MB / —207
tDiary Core(非Rails)2441.61 s(未計測)該当なし254 MB / —111
Dependabot Core(非Rails)1,08913.02 s(未計測)該当なし226 MB / —205

プールのウォール時間クロスオーバーは、Mastodon/Forem(約1.3 Kファイル、プールが遅い)とDiscourse/Solidus(約1.8 Kファイル、プールが1.3〜1.5倍速い)の間に位置します。プールのメモリコストは逐次の3〜5倍です。ADR-15 OQ1の「Ractor毎のキャッシュファサード」が、クロスオーバーをより低く動かしピークRSSを上限化するための道として残っています。

14プロジェクト全てでプール ≡ 逐次が証明された。Phase 4b.xの4つの深いshareabilityフォローアップ(NumericCatalog、CANONICAL_NAMES、RegexRefinement::RULES、ShapeDispatch::REFINED_STRING_PROJECTIONS)とCONSTANT_CONSTRUCTORS lambda修正の後、サーベイの全プロジェクト — 2つの非Railsプロジェクト(Dependabot CoreとtDiary Core)を含む — が逐次とプールモードの間でバイト単位で同一の診断ストリームを生成します。一掃した31,840ファイルにわたってIsolationErrorはゼロ。

サーベイ中に蓄えたエンジン修正(コミット642cf28 + Discourse修正)。

  1. プールの深いshareabilityギャップ(合計4箇所): NumericCatalog#@catalogType::Refined::CANONICAL_NAMESBuiltins::RegexRefinement::RULESMethodDispatcher::ShapeDispatch::REFINED_STRING_PROJECTIONS
  2. if cond && (var = expr)の狭化(Inference::Narrowing#analyseに4つの新しい書き込みノードケース)。

4つのshareability箇所は全て同じ形 — 外側のコンテナは浅く凍結されたが内部の行はそうでなかったネストされた配列のHash/Array — を共有しています。監査specには4つそれぞれに対する明示的なアサーションがあり、将来の同等の回帰は実世界の対象プロジェクトをクラッシュさせるのではなく監査を失敗させます。

診断の表面はRails拡張の不在によって支配される。4プロジェクト全体でcall.undefined-methodが全診断の64〜90%を占め、トップセレクタは一様にActiveSupport::Durationの数値強制(#days#hours#minutes)、Inflector/Stringコア拡張(#demodulize#underscore#squish#html_safe#constantize)、Array.wrapHashコア拡張(#deep_dup#symbolize_keys#stringify_keys)、Time.currentTime.zoneです。専用のrigor-activesupport-core-extプラグインがこの表面のほとんどを閉じるでしょう。config側のモンキーパッチ事前評価ノブが、プロジェクト固有の残りを閉じます。

ID状態項目
O1ランディング済み(MVP、v2)plugins/rigor-activesupport-core-ext/ — トップ約50個のActiveSupportコア拡張セレクタをカバーするコミュニティRBSバンドル。signature_paths経由でオプトイン。
O2待機中マクロテンプレート/ヒアドキュメントRuby展開。tDiaryのinstance_evalプラグインパターン(第3ラウンド)は、Railsジェネレータの.rb-as-ERBテンプレートと並ぶ具体的な動機付け事例
O3問題なしnext if x.nil?return if x.nil?は既に狭化済み — サーベイ残存のnilレシーバーはほとんどがObject#blank?#present?#tryのActiveSupport拡張で、O1のRBSバンドルがカバーする。
O4Layer 1+2ランディング済み対象プロジェクトのBundler認識。bundler.bundle_path:(明示)とbundler.auto_detect:.bundle/configvendor/bundle/)が、gem同梱のsig/signature_paths:に自動投入する。auto-skipリストがprism/stdlib競合を防ぐ。Layer 3(Gemfile.lockのパース + gem_rbs_collectionマッチング)はまだ待機中。
O5ランディング済み(ac14c45パラメータバインダーのHash <: Enumerable[[K, V]]サブタイピング。
O6ランディング済み(4698437定数畳み込み/RBSディスパッチ境界(Pathname)でのプール対逐次の精度の乖離。
O7ランディング済み(2026-05-15)signature_paths:のエントリーがstdlibのRBS宣言を重複させると、RBS envビルドのパフォーマンスが崖から落ちる。根本原因はMastodonコントローラー解析での5ラウンドにわたる二分の末に追跡された: gem同梱のprism/sig/prism.rbsPrism::VERSION: Stringを再宣言し、Rigorのバンドル済みstdlib RBS(Ruby 4.0+はprismをコアで提供)と衝突する。RBS::Environment.from_loader(...)...resolve_type_namesRBS::DuplicatedDeclarationErrorを上げる。修正前はRbsLoader#envの`@state[:env]

新しいRBSバンドルにオプトインした後(逐次ウォームキャッシュ。RBSバンドルのv2で、v1の上にcompact_blankexclude?index_withindex_byHash.from_xmlDateTime#utcEnumerableミックスインを追加したもの)。

プロジェクトベースラインO1 v2を入れた状態Δ合計call.undefined-method前 → 後
Redmine389157−232(−60%)243 → 60(−75%)
Discourse1,439423−1,016(−71%)1,078 → 134(−88%)
Mastodon521124−397(−76%)414 → 27(−93%)
GitLab FOSS2,982489−2,493(−84%)2,676 → 207(−92%)
Forem691146−545(−79%)590 → 55
Solidus52842−486(−92%)520 → 33
Chatwoot30019−281(−94%)282 → 6
Canvas LMS3,2961,496−1,800(−55%)2,493 → 766
OpenProject2,356175−2,181(−93%)2,293 → 138
合計12,5023,071−9,431(−75%)10,589 → 1,426(−87%)

残っているcall.undefined-methodのインスタンスはほとんどが次のいずれかです。

  • Canvas LMSが残存を支配する — 3,071件のうち1,496件(49%)。トップセレクタ: []= on Integer(70)、[]= on nil(51)、<< on nil(40) — RBS不足というより狭化の限界 — そしてCanvas固有の拡張(#decimal_megabytesはNumericに対するプロジェクト固有のリファインメント、File.mime_typeはstdlibに無いMarcel/Mimemagic型のヘルパー)。
  • プロジェクト固有のモンキーパッチ。Discourse、Forem、Canvas、GitLabはそれぞれ独自のStringArrayHash拡張を同梱しています。これを閉じるにはO4(プロジェクト側モンキーパッチ事前評価のconfigノブ)が必要です。
  • 解析器のRBS envに無いgem固有のメソッド。対象プロジェクトのGemfile.lock gemはrigorのプロセス外Bundlerコンテキストでロードされません。RBS出荷済みのgemはO4(対象Bundler認識)から恩恵を受けるでしょう。
  • 集中したnilレシーバーパターン。ブロック内での多重代入の後、同じブロック内でガードしてから使う形。まだフロー追跡されていません。
  • バンドルの約50セレクタの範囲外のその他のRailsコア拡張メソッド。RBSバンドルを拡張するPRは歓迎です。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.