コンテンツにスキップ

ADR-17 — プロジェクト側monkey-patchの事前評価

ステータス: Accepted, 2026-05-16; v0.1.13で実装

pre_eval:設定軸とプロジェクト全体のInference::ProjectPatchedMethodsレジストリが出荷され、境界のある事前パスによって投入され、プラグインと依存ソース推論の間のディスパッチャーティアで参照されるほか、call.undefined-method / call.unresolved-toplevelADR-34)の発行でも参照される。「explicit list MVP」のフロアとglob展開がランディングした;プラグインAPI発見フックとフルプロジェクト2パスは需要駆動のままである。

注記2026-05-29ADR-34がこのメカニズムを「機会的な精度向上」から「トップレベルのunresolved-self-call診断のための正規エスケープハッチ」へと格上げする。ADR-34はADR-17スライス(slice)1+2を実装の必須前提条件として挙げている: pre_eval:レジストリが存在する前にデフォルトのフリップをリリースすることはできない——さもなければ、トップレベルのmonkey-patchを持つプロジェクトにはオプトアウト手段がなくなる。

実世界のRubyプロジェクトは、プロジェクト専用のヘルパーメソッドを追加するためにコア / stdlibクラスをルーチン的に再オープンします。支配的なイディオムはlib/core_ext/またはapp/lib/ext/ディレクトリで、パッチ対象クラスごとに1ファイルを含みます。

lib/core_ext/string_extensions.rb
class String
def to_url
self.gsub(/[^a-z0-9]+/i, "-")
end
end

プロジェクト内の他のファイルは通常のStringに対してs.to_urlを呼び出し、静的解析がその呼び出しを定義済みとして扱うことを期待します。今日のRigorは助けなしにこの期待を満たすことができません。

  • ファイル順序は救済にならない。今日のファイルごとの推論はファイルを独立に歩く。Stringの中でdef to_urlを見つけるウォーカーは他のクラス本体発見と同様に走る — しかし、Stringが今や#to_urlを持つというファクト(fact)は、他のファイルの推論エンジンが「s.to_urlは解決するか?」と問うときに参照される、プロジェクト全体の「パッチ済みメソッドレジストリ」に外向きに伝播しない。
  • RBSバンドルではプロジェクト専用パッチをカバーできない。v0.1.5のplugins/rigor-activesupport-core-ext/バンドルは共通のActiveSupport core_extセレクタをカバーする。プロジェクト専用パッチは定義上、バンドル可能なRBSの外側にある。
  • プラグイン著作は重すぎる。ユーザーは発見されたメソッドをADR-9のflow_contribution_for経由で発行する単発のプラグインを著作できるが、その活性化面(単一プロジェクトのlib/ext/のためのgem形のプラグイン全体)は問題に対して不釣り合い。

Redmineの実世界テストがこれを「Railsのcall.undefined-methodロングテールを閉じる」ワークストリームの欠けた半分として浮上させました。もう半分(RBSバンドル)はv0.1.5でO1として着地;このADRはプロジェクト側の半分を切り出します。

.rigor.ymlに新しいpre_eval:設定軸を追加し、ファイルごとの推論が開始する前にRigorが解析しなければならない(MUST)明示的なファイルを指名します。それらのファイル内で発見されたメソッド定義は、その後の各ファイルごとの解析が参照するプロジェクト全体のパッチ済みメソッドレジストリに登録されます。

.rigor.yml
paths:
- app
- lib
pre_eval:
- lib/core_ext/string_extensions.rb
- lib/core_ext/hash_extensions.rb
- lib/redmine/setting_helpers.rb

MVPの形は明示リストのみです。パターンベースの自動発見とフルプロジェクト事前パスは需要駆動のフォローアップのまま(後述の「実装のスライス分け」を参照)。

Rigorのアナライザーが起動すると次のように動作します。

  1. 事前パスpre_eval:配下の各パスがPrismで解析され、通常のソースファイルとまったく同じように歩かれるが、発見ファセットのみがアクティブ: Inference::ScopeIndexerが走ってdef / define_method / attr_* / Data.define / Struct.new宣言を抽出し、加えてclass Foo; end / module Foo; endの骨格も抽出する。推論エンジン自体は事前評価ファイル上では走らない(呼び出しサイトごとのディスパッチなし、ナローイング(narrowing)なし、診断なし)。

  2. プロジェクト全体レジストリ。発見されたメソッド宣言は、(class_name, method_name, kind)をキーとし、宣言された(あるいはuntypedの)戻り型と定義のソースパス / 行を運ぶ、新しいInference::ProjectPatchedMethodsテーブルにマージされる。

  3. ファイルごとのディスパッチtierMethodDispatcherはプラグインの貢献と依存ソース推論のに参照される新しいtierを得る(プラグインは依然として勝つが、プロジェクト側パッチはgemソースウォークに勝つ)。

    core RBS > RBS::Extended > plugins > ProjectPatchedMethods
    > dependency-source inference > engine fallback
  4. 診断の出自。パッチ済みメソッドレジストリ経由で解決されたメソッドはDiagnostic#source_family:project_patched(新規)を保持し、エンドユーザーがどの呼び出しサイトが事前評価面に乗っているかを監査できる。

契約(contract)は保守的: メソッドがレジストリに記録されるためにはdefキーワード(または上記の認識済みメタプログラミング形式の1つ)として現れなければならない(MUST)。String.define_method(:to_url) { … }のような動的パッチはMVPのスコープ外。

事前評価パスは次のことをしてはならない(MUST NOT)。

  • 任意のプロジェクトRubyコードを実行すること。事前パスは解析+ウォークのみ;ADR-2が成文化するPlugin::Baseの§「プラグインはアプリケーションコードを実行してはならない」ルールがそのまま適用される。
  • プロジェクト境界を越えること。プロジェクトルート(ADR-2のIO境界に従って解決)の外側のファイルは、設定読み込み時に明確なconfiguration-error診断で拒否される。
  • :dependency_source-歩かれたgemコード内で無条件にmonkey-patch可能なレシーバーにパッチを当てること。ソース推論にオプトインしたgem(ADR-10)は独自のディスパッチtierを維持する;プロジェクトパッチはgemソースの自クラスのビューに注入されない。これは一方向のプッシュ: プロジェクト側解析はパッチを見るが、gemソース推論は見ない。

事前評価結果はADR-6の永続化バックエンドを使ってキャッシュされ、新しいCache::Descriptor::PreEvalEntryを持ちます。

  • キャッシュキーは事前評価ファイルごとに(path, content_digest)を含む。
  • 事前評価ファイルへの変更は、そのファイルのパッチ済みメソッドレジストリのスライスを正確に無効化する;他のスライスは残る。
  • pre_eval:自体への変更(パスの追加または削除)は、新たに列挙されたファイルと以前に列挙されたファイルの和集合に加えて、解析中にパッチ済みメソッドレジストリを参照したすべてのファイルを無効化する。デスクリプタは既存のConfigEntry機構経由でこれを追跡する。

事前評価はデフォルトでfail-softです。

  • 事前評価ファイル内のパースエラー:warningpre-eval.parse-error診断を発する;事前評価は残りのファイルで継続する;プロジェクトのファイルごとの解析は、そのファイルのパッチが存在しなかったかのように進む。
  • 事前評価ファイルの欠落 → 設定読み込み時に(解析に先立って):errorpre-eval.file-not-found診断。設定の不一致は大声であるべき。
  • 事前評価発見でのサイクル(事前評価ファイルがpre_eval:にも含まれる別ファイルをrequireする) → 特別な扱いなし;ウォーカーはrequireを辿らないので、サイクルは不活性。
  • 事前評価ファイルが別の事前評価ファイルと同じ(class_name, method_name, kind)を宣言する → 最後に列挙されたものが勝つ;両方のソース位置を指す:infopre-eval.duplicate-declaration診断を発する。ユーザーは標準の# rigor:disable機構経由で抑制できる。

ADR-16のTier D(宣言されたself下の外部Rubyファイルインクルージョン)とこのADRは関連するが別の問題を解決します。

  • Tier Dプラグインが宣言した外部ファイルをレシーバークラスの本体(Redmine webhookペイロード、tDiaryプラグインローダー)にself_typeナローイングと事前バインドされたivarsとともに配線する。プラグイン作者がマニフェストを所有する;ユーザーには直接のつまみがない。
  • ADR-17ユーザーが宣言した外部ファイルをプロジェクト全体のパッチ済みメソッドレジストリself_typeナローイングなしで(パッチ対象クラスが宣言されているクラスそのものなので、ナローイング不要)配線する。ユーザーがpre_eval:を所有する;プラグインは関与しない。

両システムは共存する;両方を使うプロジェクトは上記順序のディスパッチャーtierを通して読む。Tier D合成発行とADR-17事前評価レジストリエントリーの間の(class_name, method)ごとの衝突は登録順に従う(ADR-16 WD11に従い最初の貢献者が勝つ)。

このADRは次を追加します。

  • Rigor::Configuration#pre_eval(新しいattr_reader;絶対パスの凍結されたArray<String>)。
  • Rigor::Inference::ProjectPatchedMethods(新しい名前空間;事前評価パスが構築するインメモリレジストリ)。
  • Rigor::Cache::Descriptor::PreEvalEntry(新しい凍結Data: path:content_digest:)。
  • 新しい診断ルール:
    • pre-eval.parse-error:warning
    • pre-eval.file-not-found:error
    • pre-eval.duplicate-declaration:info
  • 新しいDiagnostic#source_familyシンボル:project_patched
  • .rigor.yml配下の新しい設定スキーマエントリー: pre_eval: [string](設定ファイルに対する相対パス)。

すべての更新はspec/rigor/public_api_drift_spec.rbに、各面を導入する実装スライスと同じコミットで着地します。

推奨順序;各スライスは独立して出荷可能。スライス1〜3でMVP機能を配信;スライス4〜6は需要駆動の拡張。

  1. 設定の配管Configuration#pre_eval、スキーマエントリー、JSON-スキーマ検証、pre-eval.file-not-found :error診断。レジストリはまだなし — pre_eval:を持つ設定の読み込みは成功するが、推論はレジストリなしで進む。
  2. 事前評価ウォーカー + レジストリAnalysis::Runnerが事前評価事前パスを得て、各pre_eval:パスに対してScopeIndexerを駆動しInference::ProjectPatchedMethodsを埋める。MethodDispatcherが依存ソースtierの上に新しいtierを得る。
  3. キャッシュデスクリプタ + 無効化Cache::Descriptor::PreEvalEntryがデスクリプタに着地する。ファイルごとのスライス無効化;pre_eval:設定変更はファイルごとのスライス消費者を無効化する。
  4. パターンベースの自動発見(設計議論のオプションB)pre_eval:がglobパターン(lib/core_ext/**/*.rb)を受け入れる。globの解決は設定読み込み時に行われる;事前評価パスは解決されたファイルリストを見る。需要駆動: ユーザーが望むときにのみ出荷。
  5. イーガーなフルプロジェクト2パス発見(オプションC)discover_patches: true設定つまみが、すべてのプロジェクトファイルを一度歩いてクラス再オープン形状を見つけ、レジストリを埋め、その後2回目のパスを診断のために走らせる。需要駆動;実質的なコストトレードオフ(2倍ウォーク、キャッシュ複雑性)。
  6. プラグイン事前評価フック(オプションD)Plugin::Base#pre_analyze(services)フックにより、プラグインがプログラム的にパッチ済みメソッドエントリーを貢献できる。ユーザーが手動で列挙したくないパッチをプラグインが宣言する必要がある場合に有用。最低優先度 — スライス2の設計から自然に派生する。

WD1 — なぜMVPで明示リストなのか、自動発見ではないのか?

Section titled “WD1 — なぜMVPで明示リストなのか、自動発見ではないのか?”

3つの議論をまとめて。

  1. 予測可能性。ユーザーは正確に何がスコープに入るかを知る。自動発見は、コードベースの形によって異なるプロジェクトレイアウト・ヒューリスティックにrigorの振る舞いが依存することを意味する。
  2. コスト有界。事前評価コストは「列挙されたファイル数 × 解析・ウォークコスト」きっかり。自動発見のコストは開いており、プロジェクトサイズに伴って上昇する。
  3. 可逆性。ユーザーは1ファイルを列挙してMVPを試せる。自動発見もオプトインだが、その失敗モード(誤ったファイルが拾われる)は「このリストが間違っている」よりも診断が難しい。

パターンベース発見(スライス4)は、ユーザーが明示リストの保守コストに気づくほど長く付き合った後の自然なフォローアップ。

WD2 — なぜ専用のディスパッチャーtierなのか(プラグインに混ぜないのか)?

Section titled “WD2 — なぜ専用のディスパッチャーtierなのか(プラグインに混ぜないのか)?”

プラグインは安定したAPIとライフサイクルを持つ著作された契約;プロジェクトパッチはユーザーが所有するレシーバークラスへのアドホックな追加。同じtierでルーティングすると次のいずれかになる。

  • すべてのプロジェクトパッチユーザーにPlugin::Baseサブクラスの著作を強制する(WD1の可逆性を打ち壊す)、または
  • 「匿名」貢献を受け入れるためにプラグイン契約を曲げる(ADR-2の著作プラグイン信頼モデルを破る)。

プラグインと依存ソースの間の専用tierは両方の面をクリーンに保ち、ディスパッチャーのtier順序規則を単純な線形チェーンのままにする。

WD3 — なぜパースエラーでfail-softなのか?

Section titled “WD3 — なぜパースエラーでfail-softなのか?”

単一のpre_eval:ファイル内のパースエラーは、プロジェクトの残りの解析を阻むべきではない。ユーザーは編集中かもしれない;アナライザーはパースエラーを診断として表面化し、残りのファイルのパッチをスコープに保ったまま継続すべき。

対照的にpre-eval.file-not-foundは大声(:error)。なぜならそれは、解析が意味を持つ前にユーザーが修正しなければならない設定ミスを示すから。

WD4 — なぜ事前評価は推論を走らせないのか?

Section titled “WD4 — なぜ事前評価は推論を走らせないのか?”

2つの理由。

  1. コスト。完全な推論を2回 — 事前評価で1回、プロジェクト本体で1回 — 走らせると、MVPでは正しさの利得なしに壁時計時間を倍化する。発見ファセット(パッチ済みメソッドレジストリが必要とするデータ)は推論の厳密な部分集合;発見パスのみを走らせれば、事前評価コストは列挙されたファイル数に比例する。
  2. 循環性。事前評価ファイル自身が、事前評価パスがまだ登録していないパッチ済みメソッドを参照するかもしれない(その場合pre_eval:内のファイル順序が重要)。事前評価内で推論を走らせると、誤った診断が表面化する。推論をスキップすれば問題を完全に回避する。

スライス5(フルプロジェクト2パス)は実際に推論を2回走らせるが、それは需要にゲートされたオプトイン拡張。

WD5 — paths:との境界(重なりなし)

Section titled “WD5 — paths:との境界(重なりなし)”

pre_eval:配下に列挙されたファイルはpaths:にも現れてもよい(MAY)。両方に現れる場合、事前評価パスと通常のファイルごとの推論の両方が同じファイルに対して走る。パッチ済みメソッドレジストリはそのファイルの宣言を見る;ファイルごとの推論はそのファイルの呼び出しサイトと診断を見る。重複排除は不要 — 2つのパスの出力は重ならない。

pre_eval:配下にのみ列挙されpaths:にはない(NOT also under paths:)ファイルはレジストリに貢献するが、診断は得ない。これは、ユーザーが信頼しておりcall.undefined-method等に対して解析を望まないlib/core_ext/ファイルの想定ケース。

WD6 — なぜ既存ファミリーに畳み込むのではなく、新しい診断ファミリーpre-eval.*を作るのか?

Section titled “WD6 — なぜ既存ファミリーに畳み込むのではなく、新しい診断ファミリーpre-eval.*を作るのか?”

pre-eval.*ファミリーは診断の原因を可視化する: ユーザーは# rigor:disable pre-eval.*で事前評価チャネル全体を黙らせることができ、call.* / flow.* / def.*に影響しない。事前評価エラーをconfiguration-error / call.*に畳み込むと、その外科的な無効化が失われる。

WD7 — ロバストネス原則(ADR-5)との境界

Section titled “WD7 — ロバストネス原則(ADR-5)との境界”

パッチ済みメソッドレジストリはプロジェクトのdefが宣言したものをそのまま記録する。メソッドがRBS sigを持たない場合(core_extパッチの一般的なケース)、その戻り型はADR-5の「戻り値については厳格 / パラメータについては寛容」の非対称性に従ってuntyped。呼び出しサイトごとの呼び出し元はDynamic[top]戻り値を見る。

プロジェクトがpre_eval:ファイル内のdefsig/配下のRBS sigと組み合わせる場合、標準のtier順序に従ってRBSがディスパッチで勝つ。プロジェクトパッチの正確な戻り値を望むユーザーは、パッチに対してRBSを書くべき。

  • 構文パターン経由の自動発見(オプションB)。WD1に従いMVPでは却下。スライス4として追跡。
  • フルプロジェクト2パス発見(オプションC)。WD4(コスト)に従いMVPでは却下、スライス5として開いたまま。
  • プラグインAPIフック(オプションD)。有用だがMVPには重すぎる。スライス6として追跡。
  • 事前評価中に(発見だけでなく)推論を走らせる。WD4に従い却下。2パス形態は必要になれば別途着地する。
  • pre_eval:ファイルをpaths:解析診断にとって意味のあるものにする(つまり他のソースファイルと同様に扱う)。WD5に従い却下: ほとんどのユーザーは事前評価ファイルをcall.undefined-methodから免除することを望む。RBSなしのイディオム的なパッチが常にルールに引っかかるから。
  • pre_eval:の順序セマンティクス。2つの事前評価ファイルが同じ(class_name, method_name, kind)を宣言する場合、last-winsは文書化されている。しかし順序は解決セマンティクスにも影響するべきか — 例えば、事前評価メソッドは同じメソッドの後続のRBS宣言を上書きするか? 今日のtier順序ではRBSが勝つ;ADRはそれにコミットするが、実世界のプロジェクトは意図的にstdlibの振る舞いを覆い隠すcore_extスタイルのパッチについて逆を望むかもしれない。決定はスライス2のドッグフードフィードバックに先送り。
  • pre_eval:はディレクトリを受け入れるべきか? ディレクトリをその下のすべての.rbファイルに展開すればpre_eval: [lib/core_ext]が典型的なユースケースの省略形になる。決定はスライス4(自動発見スライス)に先送り、同じglob機構が両方の形態を扱うため。
  • pre_eval:はキャッシュの--cache-stats出力に参加すべきか? おそらくyes — ユーザーはパッチ済みメソッドエントリーがいくつ埋まっているか、スライスごとの無効化アクティビティがどう見えるかを見たい。決定はスライス3実装に先送り。
  • 事前評価はレジストリを検査するCLIフラグを必要とするか? rigor pre-eval --dumpは解決された(class_name, method_name, source_path:line)テーブルをデバッグ用に印刷する。決定は需要に先送り。
  • docs/notes/20260518-matsumoto-2010-cfa-rigor-review.md — 松本&南出2010のSemiRubyに対するセミフローセンシティブなCFAは、ADR-17がエンジニアリング側から攻略するのと同じmonkey-patch問題に対する理論的解である。論文はプログラムポイントごとに「メソッド設定」(このちょうどこの場所で可視なdefはどれか)を追跡し、健全性(soundness)を証明する。一方ADR-17は事前評価の明示的なコストを前払いし、結果として得られる(クラス、メソッド、種別)レジストリをディスパッチャー層に凍結し、解析器の残りはメソッド定義についてフローインセンシティブのままにする。論文は採用しなかった代替の道として読め、明示リストMVPがいつか不十分と判明したときにセミフローセンシティブなメソッド設定が信頼できる将来の精度引き上げパスとして残ることを記録している。
  • 2026-05-16 — 初回提案。v0.1.5リリース後のv0.1.6サイクルスコーピング議論が発端。Redmineの実世界テスト中にActiveSupport core_extワークストリームの欠けた半分として浮上(もう半分はO1のRBSバンドルで、v0.1.5で着地した)。

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