コンテンツにスキップ

オプトイン依存関係ソース推論

ステータス: 安定(v0.1.4で出荷)ADR-10のスライス1、2a、2b-i、2b-ii、3、4、5がすべて着地・出荷され(v0.1.3のエンベロープはv0.1.4としてカット)、ADR-10の実装エンベロープは完了。このドキュメントは提供されたサーフェス(surface)の解析器契約(contract)を固定し、ADR-10 §「オープンクエスチョン」で引き続き追跡されるオープンなフォローアップ(特にgemソースからの呼び出しごとの戻り型精度)を名付けます。

拘束力のある設計サーフェスはADR-10にあります。リリースごとのコミットメントエンベロープはdocs/ROADMAP.mdにあります。この仕様がADR-10と不一致の場合、ADRが拘束力を持ち、このドキュメントは古くなっています。

Rigorのデフォルトの推論境界はRBSです。シグネチャ(RBS / RBS::Inline / 生成スタブ / プラグイン契約)のないクラスのメソッドはDynamic[top]に解決されます——エンジンはサードパーティのソースを辿りません。ADR-10は意図的な例外を切り出します: ユーザーが.rigor.ymldependencies.source_inference:経由でオプトインしたgemは、paths:を辿る同じエンジンによってRuby実装を辿られることがあります。gem境界を越える推論はDynamic[T]でラップされるため、証明は作成済みではなくサードパーティとして扱われます。

オプトインはgemごとで、paths:(ユーザー自身のソース)とsignature_paths: / libraries:(RBS境界)に直交しています。dependencies:以下に列挙されていないgemは既存のデフォルトを保持します。

.rigor.yml
paths:
- lib
dependencies:
source_inference:
- gem: rack
mode: when_missing
- gem: faraday
mode: when_missing
roots: [lib]
- gem: legacy-noop-gem
mode: disabled

この形状はRigor::Configuration::Dependenciesでパースされます。schemas/rigor-config.schema.jsonのJSONスキーマ行がパーサを反映します。

フィールド必須デフォルト注記
gem非空StringyesBundle解決可能なgem名。
modeenumnowhen_missingdisabledwhen_missingfullのいずれか。
rootsArray<String>no["lib"]ウォーカーが訪問してよいgemごとのサブディレクトリ。
dependencies.budget_per_gemIntegerno5000gemごとのカタログキャップ(メソッド定義数)。範囲1250 .. 20000(ADR-10 §「予算のインタラクション」によるデフォルトの0.25×〜4×)。ウォーカーがgemのキャップに達すると収集を停止し、ランナーはdynamic.dependency-source.budget-exceeded警告を発行します。
モード挙動
disabledドキュメント / 将来のトグル便宜のために列挙。{Builder}は解決前にエントリーをスキップ; gemは何も貢献しない。
when_missingレシーバークラス / メソッドペアに利用可能なシグネチャ契約がない場合のみgemソースを辿る。RBS / RBS::Inline / 生成スタブ / プラグイン契約は常に勝つ。推奨デフォルト
fullRBSも存在する場合でも常にgemソースを辿る。ユーザーがgemソースがバンドルされたRBSより正確と判断したケース向け; ADR-10 §「決定」に従い既知のチャーンリスクを伴う。

v0.1.3のディスパッチャーティアはwhen_missingfullを同じように実装します——両方とも同じtry_dependency_sourceサイトを通じます。ディスパッチャーでのモード区別挙動(例: fullがRBSをオーバーライド、RBS競合診断)は後続スライスのためにキューに入れられています;設定サーフェスは今固定されているため、コンシューマーは後で.rigor.ymlの書き直しなしに最終的な区別を表現できます。

列挙されていても、ウォーカーはスキップしなければなりません:

  • Cエクステンションおよびその他の非Rubyソース。ウォーカーは.rbファイルのみをロードします;他のものはカタログに到達できません。
  • トップレベルのspec/ / test/ / bin/ルートDependencySourceInference::Walker::HARD_EXCLUDED_ROOTSによってファイルシステムウォーク実行前にフィルタリングされます。lib/内に深くネストされたspec/ / test/ディレクトリはフィルタリングされません(一部のgemは正当にlib/.../spec/を同梱します)。
  • gemの列挙されたroots:外のファイル。デフォルトはlib/のみ;ユーザーはエントリーごとにこれを広げることができますが、ウォーカーは列挙されたルート外を読み取りません。

除外はローダーに組み込まれており、ユーザーは設定でオーバーライドできません。

パブリックAPIドリフトサーフェス

Section titled “パブリックAPIドリフトサーフェス”

以下のすべての名前空間はspec/rigor/public_api_drift_spec.rbによってロックされています。シグネチャ変更は同じコミットで対応するPublicApiDriftSnapshots::*定数を更新します。

サーフェスモジュールスライス
Rigor::Configuration#dependenciesConfigurationスライス1
Rigor::Configuration::Dependencies値オブジェクトConfigurationスライス1
Rigor::Configuration::Dependencies::Entry Data形状Configurationスライス1
Rigor::Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM / MIN_BUDGET_PER_GEM / MAX_BUDGET_PER_GEMConfigurationスライス4
Rigor::Configuration::Dependencies#budget_per_gemConfigurationスライス4
Rigor::Analysis::DependencySourceInference名前空間Analysisスライス2a
Rigor::Analysis::DependencySourceInference::GemResolver.resolveAnalysisスライス2a
Rigor::Analysis::DependencySourceInference::IndexAnalysisスライス2a / 2b-i
Rigor::Analysis::DependencySourceInference::Index#budget_exceededAnalysisスライス4
Rigor::Analysis::DependencySourceInference::Builder.buildAnalysisスライス2a
Rigor::Analysis::DependencySourceInference::Walker.walk(budget:)Analysisスライス2b-i / 4
Rigor::Analysis::DependencySourceInference::Walker::Outcome Data形状Analysisスライス4
Rigor::Environment#dependency_source_indexEnvironmentスライス2b-ii
Rigor::Cache::Descriptor::DependencyEntryCacheスライス3
Rigor::Cache::Descriptor#dependenciesスロットCacheスライス3
Rigor::Analysis::DependencySourceInference::Index#cache_descriptorAnalysisスライス3

解決とインデックス化(スライス2a)

Section titled “解決とインデックス化(スライス2a)”

Analysis::Runner#runはプラグインローディング後、ファイルごとの反復前に、実行ごとに一度DependencySourceInference::Indexをビルドします:

Configuration::Dependencies ─┐
Builder.build(dependencies) ▼
GemResolverを通じてエントリーを解決
Walkerを通じて解決されたgemを辿る(スライス2b-i)
フリーズされたIndexを返す

GemResolver.resolve(entry)はまずGem.loaded_specs[name]を参照し、Gem::Specification.find_by_name(name)にフォールバックします。

結果意味
Resolved(gem_name:, version:, gem_dir:, mode:, roots:)RubyGemsがスペックを特定した; versionはキャッシュディスクリプタに往復するためStringとしてのスペックバージョン。
Unresolvable(gem_name:, reason: :not_in_bundle)スペックが不在。ランナーはdynamic.dependency-source.gem-not-found :warning診断を表面化し、gemは実行に何も貢献しない。

Builder.buildはエントリーを分割します: Resolved行はウォーカーにフィード、Unresolvable行は上記の診断として表面化。mode: :disabledのエントリーは解決前にスキップされます(意図的に列挙-オフされたgemに対して欠落gemの診断なし)。

Indexは以下を公開します:

  • #resolved_gemsResolvedの配列。
  • #unresolvableUnresolvableの配列。
  • #method_catalog — ウォーカーによって設定されたフラットなHash{[class_name, method_name] => :instance | :singleton}(スライス2b-i)。
  • #contribution_for(class_name:, method_name:) — 記録された種類またはnilを返す。
  • #empty? — 解決されたgemが登録されていない場合にtrue。
  • #cache_descriptor — 解決されたgemごとに1つのDependencyEntryを持つフリーズされたCache::Descriptor(スライス3;以下の「キャッシュスライス」参照)。

Index::EMPTYはgemがオプトインしていない場合に使用されるシングルトンのフリーズされた空のインデックスです。

DependencySourceInference::Walker.walk(gem_dir:, roots:)は各受け入れられたルート以下のすべての*.rbファイルをパースし、(class_name, method_name)をメソッドの種類にマッピングするフラットなカタログを返します。ウォーカーはgem-source推論がスコープコンテキストなしで実行されるため、Inference::Scopeから切り離されています。

認識ルール:

  • class Foo / module Barは修飾名プレフィックスにFoo / Barをプッシュし、ボディに再帰。
  • class << self(のみ——その他のexprに対するclass << exprは不透明として扱われる)はシングルトンスコープフラグをプッシュ。
  • def foo(Class, :foo, :instance)(またはシングルトンスコープフラグ下では:singleton)を記録。
  • def self.fooは周囲のフラグに関係なく(Class, :foo, :singleton)を記録。
  • クラスごとの先着書き込みが勝つ。異なる種類の同じクラスの同名メソッド(稀;主にプライベートAPI)は、クラスごとの最初のウォークで勝った種類を持つ。

ファイルごとのエラーはサイレントに「このファイルからの貢献なし」に降格します:

  • Prismがパースできないファイル。
  • Prism.parse_file中に例外が発生するファイル。
  • gemの列挙されたroots:外のファイル。

辿れないgemソースはユーザー向けの診断ストリームを汚染してはなりません——ユーザーはファイルを作成しておらず修正できません。

ディスパッチャーティア(スライス2b-ii)

Section titled “ディスパッチャーティア(スライス2b-ii)”

Inference::MethodDispatcher.dispatchはRBSディスパッチが失敗した後、ユーザークラスフォールバック前にインデックスを参照します:

定数フォールドティア
シェイプ / カーネル / イテレータ / ブロックフォールド精度ティア
RbsDispatch.try_dispatch ── RBS / RBS::Inline / スタブ / プラグイン
↓ (契約なし)
try_dependency_source(receiver_type, method_name) ── ADR-10(このティア)
↓ (エントリーなし)
try_user_class_fallback ── Kernel / Module組み込み
call.undefined-method ── 最終

try_dependency_sourceはレシーバーがカタログエントリーとクラス名 + メソッド名がマッチするType::Nominal / Type::Singletonを持つ場合、Type::Combinator.untyped(つまりDynamic[top])を返します。このティアはプラグインより厳密に下位に座ります: プラグイン契約はADR-10 WD6に従って競合時に勝ちます(プラグインは作成済み契約; gem-source推論は日和見的)。

スライス2b-iiはDynamic[top]で意図的に停止します。メソッドごとの戻り型精度(つまり非top静的ファセットを持つDynamic[T])は後のスライスにキューに入れられており、まだtry_dependency_sourceエンベロープを通じて表面化しません。現在の可視のペイオフは、オプトインgemメソッドコール上のcall.undefined-methodの不在です(RigorがNominal[T]でレシーバーを認識できる場合、通常ユーザーがRBSスケルトンを作成したか、RBSがコンストラクタコールを解決したため)。

ADR-10 §「予算のインタラクション」に従い、各オプトインgemは別の予算プールを取得するため、境界の悪いgemがユーザー自身の解析を飢えさせることができません。

単位はカタログに収集されたメソッド定義数です。デフォルト5000はすべての現実的なオプトインターゲット(Rack≈1500、Faraday≈500、Sidekiq≈800)をカバーしつつ、ユーザーがRBSを同梱するかgemをリスト解除すべきActiveSupport規模のライブラリ(〜10,000+メソッド)に対して診断を表面化します。設定値はMIN_BUDGET_PER_GEM1250、デフォルトの0.25×)とMAX_BUDGET_PER_GEM20000、4×)定数によって制限されます。

ウォーカー側キャップ(セマンティクスα)

Section titled “ウォーカー側キャップ(セマンティクスα)”

Walker.walk(gem_dir:, roots:, budget:)が単一gemに対してbudgetカタログエントリーに達すると、収集を停止します:

  • 現在のファイルの残りのdefノードは記録されません。
  • 同じgemの後続のファイル(とルート)は訪問されません。
  • ウォーカーはキャップに達したことを示すためにOutcome.new(catalog: ..., truncated: true)を返します。

累積されたカタログは有効なまま;ただしgemを完全にカバーしていません。キャップ前に収集されたメソッドについては、ディスパッチャーティアは他のカタログヒットと全く同じように動作します(Dynamic[top]を返す)。収集されなかったメソッド——つまりキャップを過ぎたもの——については、ディスパッチャーは既存のユーザークラスフォールバックパスに落ちます: レシーバークラスがRBS既知だがメソッドが違う場合は通常call.undefined-method

これはADR-10 WD4の(α)セマンティクス: 予算は収集をキャップし、ディスパッチはキャップしません。より豊富な(β)セマンティクス(「予算超過gemのクラスへのコールはカタログヒットに関係なくDynamic[top]を返す」)は、{Index}上のクラスからgemへの逆インデックスとそれを参照するディスパッチャーブランチが必要になります;そのフォローアップは(α)の経験が具体的なニーズを表面化した場合の後のスライスにキューに入れられています。

Index#budget_exceededはビルド中にキャップをトリップしたgem名のフリーズされた配列です。ランナーは#dependency_source_budget_diagnosticsを通じて実行ごとに一度このリストを消費し、列挙されたgemごとに1つのdynamic.dependency-source.budget-exceeded :warningを発行します。診断メッセージはgem名、設定されたキャップを名付け、3つの修正方法(RBSを同梱、mode:fullからwhen_missingに減らす、リスト解除)をユーザーに示します。

重複排除はコールサイトごとではなくgemごとです。記録されていないメソッドが何百もある予算超過gemは正確に1つの警告を生成します;ユーザーは数十の同一メッセージを抑制する必要がありません。

キャッシュスライス(スライス3)

Section titled “キャッシュスライス(スライス3)”

Rigor::Cache::DescriptorDependencyEntry行を持つトップレベルのdependencies:スロットを取得します:

Rigor::Cache::Descriptor::DependencyEntry.new(
gem_name: "rack",
gem_version: "3.0.0",
mode: :when_missing
)
フィールド注記
gem_nameStringエントリーで宣言されたBundle解決可能な名前。
gem_versionString実行のResolved.versionGem::VersionをStringにレンダリング)。
mode:disabled / :when_missing / :full{Configuration::Dependencies::VALID_MODES}を反映。

合成(Cache::Descriptor.compose)はgem_nameでグループ化し、2つの貢献者がgem_versionまたはmodeで不一致の場合にConflictを発生させます。有効なデプロイメントではBundlerはgemごとに1バージョンをインストールし、パーサはgemごとに1エントリーを生成するため、競合パスは例外的です。

Index#cache_descriptorはすべてのResolved行をDependencyEntryに変換し、dependencies:スロットが設定されたフリーズされたCache::Descriptorを返します。ADR-10推論出力を観察するキャッシュプロデューサーはこのディスクリプタを自身のもの(RbsDescriptor、プラグインディスクリプタ、ファイルダイジェスト)とCache::Descriptor.composeを通じて合成するため、列挙されたgemのbundle updateがそのgemのスライスだけを無効化し、キャッシュの残りをホットのままにします。

Unresolvableエントリーは何も貢献しません——キーにするバージョンがなく、ランナーはすでにそれらをdynamic.dependency-source.gem-not-found診断として表面化しています。解決済み-無効エントリーは{Builder}によって上流でフィルタリングされ、インデックスに到達しません。

Cache::Descriptor::SCHEMA_VERSIONはこのスライスで2にバンプされました。正規ハッシュ形状にトップレベルスロットを追加することは定数の文書化された契約に従って非互換な変更であるため、バンプはCache::Store#ensure_schema_version!をトリガーして古い形状のエントリーが孤立として残らないよう最初の実行後にキャッシュルートを消去します。

スライス3エンベロープはgemバージョンごとの無効化のプリミティブを着地させます。ADR-10推論をStore#fetch_or_compute経由でルーティングするキャッシュプロデューサーはgemごとの予算機械と並行してスライス4にキューに入れられます。

依存関係ソースパスで発行されるすべての診断はdocs/type-specification/diagnostic-policy.md §「診断識別子の分類」に従ってdynamic.dependency-source.*プレフィックスを使用します。

ルール重大度(作成)ステータス意味
dynamic.dependency-source.gem-not-found:warningライブ(スライス2a)列挙されたgemがRubyGemsで解決不能だった。実行は継続; gemは何も貢献しない。
dynamic.dependency-source.budget-exceeded:warningライブ(スライス4)gemごとの予算がトリップした。ウォーカーはdependencies.budget_per_gemメソッド定義で収集を停止した;残りのサイトは既存のRBS-または-Dynamic[top]境界を通じて解決される。実行ごとgemごとに最大1回発行。推奨: RBSを同梱、modeをfullからwhen_missingに減らす、またはgemをリスト解除。
dynamic.dependency-source.boundary-cross:infoライブ(スライス5c)RBSとmode: :fullオプトインgemのソースカタログの両方が同じ(class_name, method_name)について意見を持つ。ディスパッチではRBSが勝つ;診断は純粋に助言的で、ユーザーがRBS契約とgemのソースの間のドリフトを監査できるようにする。(class_name, method_name, gem_name)ごとに重複除去。
dynamic.dependency-source.config-conflict:warningライブ(スライス5d).rigor.yml includes:チェーンが同じgemに対して不一致のmode:を持つ2つのdependencies.source_inference[]エントリーを生成した。後の(下流インクルード)エントリーが勝つ; roots:はサイレントにユニオン(union、合併型とも)される。競合する(gem, prior-mode, new-mode)トリプルごとに1つの診断。

docs/type-specification/diagnostic-policy.mdの分類行はすでにdynamic.dependency-source.*ファミリーをカバーしています——ファミリー内の新しいルールが出荷されるにつれ仕様変更は不要です。

ADR-2(信頼済みgem信頼モデル)との関係

Section titled “ADR-2(信頼済みgem信頼モデル)との関係”

gemをsource_inference以下に列挙することは読み取り専用の信頼付与です。Rigorはgemのファイルをパースして解析器を通じて実行しますが、コードをロードまたは実行しません。ADR-2 §「プラグインの信頼とI/Oポリシー」の「プラグインはアプリケーションコードを実行してはならない」ルールが逐語的に適用されます。ネットワークアクセスは無効のまま;ファイル読み取りはgemのroots:にスコープされたまま。

docs/type-specification/robustness-principle.mdはRigor作成型が戻り値に対して厳格であることを求めます。Gem-source推論は偶発的に狭い戻り型を生成します——推論された戻り型は今日の実装を反映し、gemの作者がコミットしたであろう契約ではありません。

この緊張は推論された狭い型を作成済みであるかのように公開しないことで解決されます。Gem推論された戻り型はDynamic[T]でラップされます。ラッパーは漸進的(gradual)一貫性セマンティクスを型付き境界を越えて保持しつつ、偶発的に狭い推論への暗黙の依存をブロックします。RBS消去(docs/type-specification/rbs-erasure.md)はDynamic[T]untypedとしてエクスポートします;静的ファセットTは作成済みシグネチャに漏れません。

ADR-9(クロスプラグインAPI)との関係

Section titled “ADR-9(クロスプラグインAPI)との関係”

プラグイン作者は現在、自分が所有するレシーバーに対してgem-source推論を拒否できません。ADR-10 §「オープンクエスチョン」はこれをおそらくのフォローアップとして特定しています——Plugin::Base#owns_receiver?またはmanifestフィールド——しかし少なくとも1つのプラグインがそれを必要とするまで設計を延期します。ディスパッチャーティアの順序付けはとりあえず不在を良性にします: プラグインはdependency-sourceティアの前に参照されるため、レシーバークラスを所有するプラグインは競合時に勝ちます。

「パブリックAPIドリフトサーフェス」で名付けられたサーフェスはv0.1.3のmaster上で未リリースとして安定しており、ドリフトスペックによってロックされています。ADR-10の5スライス実装エンベロープは完了しています;さらなる作業は以下の「オープンクエスチョン」で追跡されています。

ADR-10 §「オープンクエスチョン」で追跡 — 具体的なニーズが表面化するにつれ再検討:

  • レシーバーごとのプラグイン拒否権 — 着地(スライス5a)。プラグインはレシーバークラス(とそのサブクラス、Environment#class_ordering経由)の唯一の所有権を主張するためにmanifest(owns_receivers: ["ActiveRecord::Base"])を宣言します。dependency-source-inferenceティアはカタログを参照する前にレジストリを参照します: 登録されたプラグインが所有するレシーバーは辞退するため、プラグイン貢献は権威を維持します。
  • mode: full独自ディスパッチ — 着地(スライス5cの前提条件)。Indexclass_to_gemgem_modesをチェーンすることでmode_for(class_name) / full_mode?(class_name)を公開し、クラスごとのモード認識を提供します。RBSディスパッチがコールを解決した後、ディスパッチャーは(class_name, method_name)についてインデックスを参照し、レシーバークラスがmode: :fullのgemに属しANDそのgemのソースカタログが同じメソッドを含むときにEnvironment#boundary_cross_reporter上にdynamic.dependency-source.boundary-crossイベントを記録します。RBSが引き続きディスパッチ結果で勝ちます — 診断は純粋に助言的です。
  • キャッシュサイズキャップ(dependencies.cache_size — ADR-10 WD5ごとのキャッシュスライスは(gem、バージョン、モード)ごと;グローバルサイズキャップはドッグフーディング中にキャッシュバックエンドが成長問題を示すまで延期されます。
  • 設定可能なディスパッチャーティア順序付け — 狭いケースでプラグイン出力をgemソースに譲らせたいユーザー向け。デフォルト: いいえ、ただし最初の具体的なユーザーリクエストの後に再検討。
  • より豊富な(β)予算セマンティクス — 着地(スライス5b)。dependencies.budget_overrun_strategy: dependency_silenceは(β)セマンティクスにオプトインします: ウォーカーは引き続きbudget_per_gemでキャップしますが、ディスパッチャーはカタログミス時にIndex#class_to_gem(クラスごとの逆ルックアップテーブル)を追加参照します;レシーバークラスが予算超過gemに属する場合、コールはユーザークラスフォールバックに落ちる代わりにDynamic[top]に解決されます。デフォルトは後方互換性のために:walker_cap(α)のまま。
  • dynamic.dependency-source.boundary-cross診断 — 着地。mode: :fullオプトインgemで定義されたメソッドへのRBSとgem-sourceの重複を表面化します。ディスパッチャーは、RBSが非Dynamic[Top]キャリア(carrier)に解決AND gem-sourceカタログが同じ(class_name, method_name)を持つAND所有gemがmode: :fullであるたびに、Environment#boundary_cross_reporterを介して交差を記録します。RBSがディスパッチで引き続き勝ちます; :info診断は純粋に助言的で、(class_name, method_name, gem_name)ごとに重複除去されるため、多くのファイルから呼ばれるメソッドは1つの診断を生成します。深刻度プロファイルはプロジェクトの好みに応じてルールを再スタンプします。
  • dynamic.dependency-source.config-conflict診断.rigor.ymlパース / マージの不一致(includes:を越えた同じgemに対する2つの互換性のないエントリー)を表面化します。設定ローダーのincludes:監査作業と並行して着地します。

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