コンテンツにスキップ

ADR-7: v0.1.0 スライス4〜6 作業上の決定

ステータス: 承認(作業上の決定)

ADR-2(拡張API)の上に構築され、残りのv0.1.0スライス(slice)(4 — 内部ナローイング(narrowing)を通じたFlowContributionの配線、5 — プラグイン診断出力プロトコル、6 — プラグイン側キャッシュプロデューサー)の未解決の実装選択肢を閉じる。すでにmasterにランドしたスライス1/スライス2/スライス3/スライス5(フォーマッタ)の作業はdocs/CURRENT_WORK.mddocs/ROADMAP.mdに記録されている。このADRは次の実装者がv0.1.0作業を継続する際にコミットする選択を固定する。

ADR-2はv0.1.0の設計サーフェス(surface)(拡張プロトコル、コントリビューションマージ、登録/設定、信頼/I/Oポリシー、キャッシュディスクリプタ)を固定するが、スライス計画が進むにつれて解決する「作業上の決定」にいくつかの実装レベルの問いを延期した。スライス1(登録/ローディング)、スライス2(信頼/I/Oポリシー)、スライス3(コントリビューションマージャー)、スライス5のフォーマッタ半分(修飾ルールテキストレンダリング)がmasterにあり、残りのサーフェス——既存の内部ナローイングへのマージャーの配線、プラグイン診断出力プロトコル、プラグイン側キャッシュプロデューサー——はそれぞれ実装前に決定が必要な個別の設計問いを持っている。

このADRはそれぞれの問いに対して選択された方向を記録する。

スライス4 — 内部ナローイングを通じたFlowContributionの配線

Section titled “スライス4 — 内部ナローイングを通じたFlowContributionの配線”

4-A: バンドルスロット ↔ エンジン内部ファクト変換ルール

Section titled “4-A: バンドルスロット ↔ エンジン内部ファクト変換ルール”

現在エンジンはフローコントリビューションデータ形状を4つ並行して持つ。

  • Rigor::RbsExtended::PredicateEffectrigor:v1:predicate-if-*からの真偽述語ナローイング)
  • Rigor::RbsExtended::AssertEffectrigor:v1:assert*からのアサートスタイルの返り値後ファクト(fact))
  • Rigor::RbsExtended::ParamOverriderigor:v1:param:からのパラメータ型ナローイング)
  • 組み込みナローイングルールの直接ファクト出力(Inference::Narrowing#predicate_scopesとその仲間、中間値オブジェクトなしでScopeを直接編集する)

Rigor::RbsExtended.read_flow_contribution(method_def)はすでに最初の3つを単一のRigor::FlowContributionバンドル(v0.0.9グループD)に折り畳んでいるが、コンシューマーはまだ8つの呼び出しサイト(narrowing.rb × 2、statement_evaluator.rbmethod_parameter_binder.rbcheck_rules.rboverload_selector.rbrbs_dispatch.rbanalysis/check_rules.rb)で型付きEffectキャリア(carrier)を直接読み取っている。

Rigor::FlowContribution::Merger(スライス3)がマージポリシーが動作する単一の統合点——重複排除/等価による重複排除/交差ルールが必要——になるためには、バンドルスロットのペイロードが意味のある比較をサポートしなければならない。組み込みナローイングルールのTypeペイロードと同じターゲット/エッジのRbsExtended PredicateEffectは直接比較できないため、現在のマージャーはADR-2 § 「プラグインコントリビューションマージ」が要求する競合を検出できない。

決定: 正規化された中間ファクト値オブジェクトを導入する。新しいRigor::FlowContribution::Fact = Data.define(:target, :predicate, :type, :edge)(またはスライス4の最初の実装で検証される同様の最小形状)が正規スロットペイロードになる。既存のキャリア(組み込みナローイング、PredicateEffectAssertEffect、将来のプラグインコントリビューション)はそれぞれ境界で正規Factに/から変換する。型付きRbsExtended::*Effectキャリアはパーサ側のステージング型として内部に残るが、read_flow_contributionの下流ではすべてが正規Factとして流れる。

根拠: 4つの非比較可能なキャリアを並行させる長期的な技術的負債は、1回限りの変換コストよりも高い。正規Factはプラグイン作成者(スライス5出力プロトコル、スライス6キャッシュプロデューサー)が書き込む安定したターゲットも提供する——彼らはFactを宣言し、内部Effectクラスを宣言しない。これはレガシーを蓄積しないパスへのユーザーの好みを反映する。

このADRのスコープ外: Rigor::FlowContribution::Factの正確なフィールドセット。スライス4の実装者は既存の呼び出しサイトの実際のナローイングニーズに対してサーフェスを確定する。制約は(a)凍結値オブジェクト、(b)等価/ハッシュをサポートするスロットペイロードとして使用可能、(c)スライス3で定義されたスロットセマンティクスの情報損失なしに既存の各キャリアからラウンドトリップ可能であること。

マージャーは2つの粒度で呼び出せる。

  • 呼び出しサイトごと: 1つ以上のコントリビューションソースを活性化するすべてのPrism::CallNodeが、アクティブなバンドルに対してマージャーを実行する。マージ結果が呼び出しごとのナローイング/返り値型/ミューテーション効果になる。
  • メソッド定義ごと:メソッドのコントリビューションの静的部分(RBS::Extendedディレクティブ、宣言されたミューテーション)はメソッド定義ごとに1回マージされる。呼び出しサイトごとのマージは引数型に結果が依存するプラグインコントリビューションにのみ実行される。

決定: 呼び出しサイトごと。すべての呼び出しサイトマージが組み込み、RBS::Extended、および(将来の)プラグインコントリビューションを一緒に合成する。スライス3のマージャーは以前にそれらのソースのいずれかを参照していたすべてのディスパッチポイントで呼び出される。

根拠:呼び出しサイト粒度は、マージポリシーを将来のコントリビューション種別が適合しなければならない別の「静的部分」コードパスなしに、単一の理解しやすい関数((contributions[]) → MergeResult)に保つ。パフォーマンスは以下で緩和される。

  • 正規Factの等価ベースの重複排除は、モノモーフィックな呼び出しサイト(すべてのコントリビューションのペイロードが呼び出しサイト間で同一の場合)が小さくキャッシュフレンドリーな結果セットにヒットすることを意味する。
  • スライス6のプラグイン側キャッシュプロデューサーは、受信側型/パラメータ型に依存するコントリビューションが解析実行全体で安定している場合に、同じキャッシュレイヤーをマージ結果に拡張する。

この決定に関するユーザーの見解: キャッシュ効率がオーバーヘッドを担わなければならない。スライス4の実装者は、既存のspecに対して呼び出しサイトごとのマージャーがホットパス安価(コントリビューションなし呼び出しごとに割り当てなし)であることを確認する責任がある。bundle exec exe/rigor check libのプロファイリングがスライス4前のタイミングに対して非自明な退行を示す場合、メソッド定義ごとのフォールバックが呼び出しサイトごとの主パスの上に最適化として追加される。メソッド定義ごとは決して主要な統合点にならない。

スライス5 — プラグイン診断出力プロトコル

Section titled “スライス5 — プラグイン診断出力プロトコル”

スライス5のテキストレンダリング半分はコミットef730b2Diagnostic#to_sがデフォルト以外のsource_familyに[<source_family>.<rule>]を追加)でmasterにすでにランドしている。残りの作業はプラグイン側の出力フックだ。

ADR-2 § 「カスタムルール」はPHPStanスタイルのRule<TNode>ノードスコープルールを最終的なサーフェスとして言及する。スライス5のMVPでは、フラットなファイルごとのフック、フルのRule<TNode>機構、またはPHPStanの2段階コレクター+ルールパターンが選択肢だ。

決定: v0.1.0 MVPとしてファイルごとのフラットフック

class Rigor::Plugin::Base
# Override in plugin subclasses. Default returns no
# diagnostics. The `scope` argument is the file-entry scope
# the analyzer reached after `ScopeIndexer` ran; `root` is
# the parsed `Prism::Node`.
def diagnostics_for_file(path:, scope:, root:)
[]
end
end

ランナーはすべてのロードされたプラグインのdiagnostics_for_fileを解析されたファイルごとに1回呼び出し、返された診断を実行結果に折り込む。プラグイン作成者はノード固有のルールが必要な場合にrootの独自の走査を書く。

根拠: v0.1.0 MVPを小さく保ち、プラグインプロトコルのサーフェスを最小にする。Rule<TNode>ノードスコープルールAPIはv0.1.xのためにキューに入れられ、フラットフックの実際のプラグイン作成者の使用に対して設計できる。

スライス4からのMergeResult#conflictsは、ランナーの既存の診断収集パスを通じてルーティングすることで同じ診断ストリームに到達する(以下の5-Cを参照)。独自の出力プロトコルは不要だ。

プラグインはdiagnostics_for_fileからRigor::Analysis::Diagnostic行を返す。source_familyフィールドはプラグイン作成者が設定するか、ランナーが自動的にスタンプできる。

決定: ランナーが自動的にスタンプする。プラグインはsource_familyを設定せず(または任意の値で)Diagnostic行を返す。ランナーは診断を結果に追加する前にsource_family: "plugin.#{plugin.manifest.id}"で上書きする。プラグイン作成者は別のプラグインのidや:builtinの下に誤って診断を公開できない。

根拠:スライス1/スライス2の信頼モデルと一致する——プラグインマニフェストidが信頼された識別子。プラグインに帰属するものすべてが一貫してそれを使用する。

5-C: マージャーConflict → Diagnostic変換

Section titled “5-C: マージャーConflict → Diagnostic変換”

Rigor::FlowContribution::Merger.mergeConflict行を生成する場合、解析器はそれを実行結果のDiagnosticに変換する。

決定: Conflict#to_diagnostic(path:, line:, column:)。変換はRigor::FlowContribution::Conflict自体に置かれる。コンフリクトが値オブジェクトであり、自分自身を診断に変換することが値オブジェクトの責任だ。ランナー(スライス4の配線)は各MergeResultからコンフリクトを収集し、行ごとにto_diagnosticを呼び出して呼び出しサイトのpath/line/columnを添付する。

結果のDiagnosticsource_family: :contribution_mergeとコンフリクトのreasonから導出されたrule(例:return-type-collapseexceptional-disagreementlower-tier-contradiction)を持つ。修飾ルールパスはスライス5のフォーマッタ半分に従って[contribution_merge.return-type-collapse]としてレンダリングされる。

根拠:ユーザーは強い好みを示さなかった。Conflict#to_diagnosticの配置はConflictを診断が何を言うべきか(どの出所、どのメッセージ)の権威あるキャリアとして保つ。外側からランナーが診断を構築する(代替Y)も問題ないが、変換ロジックが分散する。値オブジェクトに置くことで診断形状がConflict形状と共進化する。

スライス6 — プラグイン側キャッシュプロデューサー

Section titled “スライス6 — プラグイン側キャッシュプロデューサー”

プラグインはv0.0.9のCache::Store#fetch_or_compute(producer_id:, params:, descriptor:, serialize:, deserialize:)サーフェスを利用する。プラグイン作成者向けAPIは完全に宣言的、完全に命令的、またはハイブリッドにできる。

決定: DSL宣言+Servicesヘルパーのハイブリッド

class MyRailsPlugin < Rigor::Plugin::Base
manifest(id: "rails", version: "0.1.0")
# Declares an id (and optionally a custom serialiser pair).
# The block defines the producer body — same shape as today's
# built-in producers.
producer :schema_table, serialize: ->(value) { ... },
deserialize: ->(bytes) { ... } do |params|
schema_path = services.io_boundary_for(manifest.id).read_file("db/schema.rb")
parse_schema(schema_path, params)
end
def schema_for(table_name)
# `services.cache_for(:schema_table, params: { table: table_name })`
# returns a callable that hits `Cache::Store#fetch_or_compute`
# with the descriptor automatically built (see 6-B).
services.cache_for(:schema_table, params: { table: table_name }).call
end
end

2つのサーフェス。

  1. Plugin::Base.producer(id, serialize:, deserialize:, &block) — クラスレベルDSL。プロデューサーのidとオプションのカスタムシリアライザペア、プロデューサーボディを宣言する。idは自動的にプレフィックスが付く(6-Cを参照)。serialize/deserializeはデフォルトでスライス1のMarshal.dump/Marshal.loadペア。
  2. Services#cache_for(producer_id, params:) — 結果が欲しいときにプラグインが呼び出すインスタンスヘルパー。ディスクリプタが自動組み立て(6-Bを参照)されてfetch_or_computeラウンドトリップを実行するコーラブルを返す。

根拠:純粋に宣言的なマニフェストはプロデューサーロジックとうまく合成されない(ボディは実際のRubyコードであり、データ行ではない)。純粋に命令的なCache::Store#fetch_or_compute呼び出しはディスクリプタ構築をプラグイン作成者に任せ、ミスを招く(PluginEntryの欠落、誤ったプロデューサーidプレフィックス)。ハイブリッドは宣言を軽量(id+シリアライザ)に保ちつつ、ボディを通常のRubyに置き、ディスクリプタを自動構築させる。

Cache::Descriptor::PluginEntry(id:, version:, config_hash:)はキャッシュ不変条件でプラグインを識別する。無効化が正しく機能するためにすべてのプラグイン側キャッシュスライスに存在しなければならない。

決定: ローダー/Servicesヘルパーが自動的にアタッチする。ローダーがスライス1のLoader.loadを通じてプラグインをインスタンス化する際、結果のServicesコンテナはプラグインごとのディスクリプタテンプレートを記録する。

PluginEntry.new(
id: manifest.id,
version: manifest.version,
config_hash: digest(plugin.config)
)

すべてのServices#cache_for(producer_id, params:)ラウンドトリップは以下からディスクリプタを自動合成する。

  • プラグインのPluginEntryテンプレート(上記)。
  • プラグインのIoBoundary#cache_descriptor(スライス2——境界が読み取ったすべてのファイル)。
  • ユーザーのparams:ハッシュ(v0.0.8 Descriptor#cache_key_forに従ってキャッシュキーに混ぜられる)。

プラグイン作成者はディスクリプタを手動で構築しない。カスタムディスクリプタ拡張(追加のFileEntry/GemEntry/ConfigEntry行)は将来のAPI拡張に乗る。スライス6は自動構築パスのみを出荷する。

根拠:プラグインid+バージョン無効化はプラグイン契約(contract)が依存する不変条件だ。プラグイン作成者に手動で合成させることは不要なリスクだ。ローダーはマニフェストを持ち、ランタイムはIoBoundaryを持ち、自動構築が自然な組み立てだ。

6-C: キャッシュルートサンドボックス境界

Section titled “6-C: キャッシュルートサンドボックス境界”

プラグイン宣言のproducer_idは組み込みプロデューサーとファイルシステムレイアウトを共有する(<root>/<producer_id>/<2-prefix>/<62-suffix>.entry)。producer :rbs_environmentと宣言するプラグイン作成者は組み込みのv0.0.9 RbsEnvironmentキャッシュスライスと衝突する。

決定: ローダー/DSLがプラグインプロデューサーidにplugin.<manifest.id>.を自動的にプレフィックスするmanifest.id = "rails"のプラグインのproducer :schema_table宣言はplugin.rails.schema_tableとして登録・永続化される。このプレフィックスは。

  • 既存のCache::Store::VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/正規表現の範囲内。
  • 組み込みプロデューサーとのid衝突を防ぐ(組み込みは今日rbs.*プレフィックスを使用し、plugin.*を使用しない)。
  • 決定論的で--cache-stats出力で見えるため、キャッシュ帰属が明確。

根拠:ドキュメントではなく構造によるサンドボックス。衝突しやすい独自idを書こうとするプラグイン作成者は、サポートされたパスを通じてキャッシュレイヤーに到達できない。

6-D: メソッドごとのReflectionキャッシュ再試行(v0.0.9のキャリーオーバー)

Section titled “6-D: メソッドごとのReflectionキャッシュ再試行(v0.0.9のキャリーオーバー)”

v0.0.9でのRbsLoader#instance_definition/singleton_definitionのキャッシュ配線の試みは解析器の退行(bundle exec exe/rigor check libでのuninitialized constant Rigor::Cache::RbsDescriptor::Descriptor)を引き起こした。根本原因は特定されていない。元のv0.1.0計画はスライス6と再試行を束ねていた。

決定: スライス6のスコープから外す。メソッドごとのReflectionキャッシュ再試行は別のv0.1.xチケットに分割し、v0.1.0後に所有する。スライス6はプラグイン向けプロデューサーサーフェス(上記の6-A〜6-C)のみを出荷し、既存の組み込みRBS側キャッシュには触れない。

根拠:メソッドごとのキャッシュ再試行は、配線を再適用する前にv0.0.9退行の別個の根本原因調査が必要だ。それをスライス6と束ねることはリスクを追加する(すでに新しいパブリックプラグインAPIを導入するスライスへのエンジン内部退行)がスライス6が必要とするものを何も得ない。Cache::Store#fetch_or_computeコーラブルサーフェスはプラグイン側プロデューサーをランドさせるのに十分であり、メソッドごとのReflectionキャッシュは直交した最適化だ。

  • スライス4のマージャーは単一の正規スロットペイロード型を持つ。4つのコントリビューションソースはすべて境界で変換し、マージャーの交差/重複排除ロジックが均一なサーフェスで動作する。
  • スライス5の出力プロトコルは最小限の実行可能な形状。プラグイン作成者はオーバーライドする明確な単一のフックを得る。
  • スライス6のプロデューサーAPIはプラグイン作成者に宣言的な登録サーフェスを与えながら、ディスクリプタ不変条件(PluginEntryアタッチメント、プレフィックス付きプロデューサーid)をドキュメントではなくローダーが強制する。
  • スライス6のスコープは限定的を保つ。v0.0.9のメソッドごとのReflectionキャッシュ再試行は新しいパブリックAPIと絡まない。
  • 正規Fact値オブジェクトはプラグイン作成者が書き込む新しいパブリック形状サーフェスだ。スライス4の実装者がフィールドセットに責任を持つ。スライス4のコミットと並行してspec/rigor/public_api_drift_spec.rbにドリフト固定がランドする。
  • 呼び出しサイトごとのマージャー粒度はコールドパスのオーバーヘッドが小さいことを保証するためにプロファイリングの規律が必要だ。スライス4はスライス4前のベースライン(baseline)に対してbundle exec exe/rigor check libタイミングで退行がないことを確認する責任を持つ。
  • Rule<TNode>ノードスコープルールAPIは延期されたまま。v0.1.0でノードスコープルールが必要なプラグイン作成者はフラットフックで走査を再実装する。v0.1.xでその制限を解除する。
  • Rigor::FlowContribution::Factの正確なフィールドセット。スライス4の実装者は8つの既存の呼び出しサイトを調査し、すべてのキャリアをラウンドトリップできる最小フィールドセットを選ぶ。ドリフトspecが偶発的な変更を検出する。
  • Plugin::Base.producer DSLのserialize:/deserialize:コーラブルの形状。スライス6の実装者はv0.0.9のCache::Store#fetch_or_compute(serialize:, deserialize:)コーラブルサーフェスをそのままミラーする。偏差は別のADRになる。
  • Conflict#to_diagnosticの正確なRigor::Analysis::Diagnostic#ruleマッピング。スライス5/スライス4の実装者がkebab-caseのrule文字列を選ぶ。ADR-2 § 「プラグイン診断の出所」はすでに<source_family>.<rule>形状を制約している。
  • プラグイン側キャッシュのクロスマシンキャッシュ共有。ADR-6に従い、キャッシュはシングルマシン。プラグイン側プロデューサーはその制約を継承する。

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