コンテンツにスキップ

ADR-37 — Plugin interface segregation (narrow extension protocols)

Status: Accepted, 2026-06-02;スライス1〜3実装済み。同梱プラグインセット全体に対して検証済み: 診断を発行するすべてのプラグインがdiagnostics_for_fileウォーカーからnode_ruleへ移行され(最後にして最も複雑なrigor-actionpack——4フェーズ、名前空間修飾に敏感——はNodeContextの祖先経由で着地)、flow_contribution_forの分割(dynamic_returntype_specifier)はきれいに収まるコンシューマーを担い、機械可読なケイパビリティカタログ(rigor plugins --capabilities)が出荷される。太いフックはサポートされた非推奨の脱出弁として残る。延期(非ゲート、需要駆動): 4つの脱出弁コンシューマー(rspec-let/sorbet/activerecord/activestorage)をflow_contribution_forから移行できるようにするdynamic_returnの一般化、および作成者ヘルパーのボイラープレート削減フォローオン(Base#suggestconfig_schemaデフォルト・Plugin::Inflector——ボイラープレート計画フェーズ0c〜0e)。以下に挙げるインターフェースごとのテストハーネス(NodeRuleTestDynamicReturnTest)も同様に、プラグイン作成者が必要とするまで延期される;ノードごとのテスト可能性は、移行が確立したAnalyzer.*_violations_for分割を通じて既に到達可能である。

スライス1実装済み(2026-06-02): Plugin::Base上のnode_ruleクラスDSL+Base#node_rule_diagnostics(エンジン所有の走査)+ランナー/ワーカーセッション配線。プラグインはnode_rule(Prism::CallNode) { |node, scope, path| … }を宣言します。エンジンは各ファイルのASTを(Source::NodeWalker経由で)一度だけ走査し、到達可能なすべてのノードを、そのnode_typeを満たすルールへディスパッチし、プラグインインスタンス上でinstance_execします。そのため作者は走査を一切手作りしません。これはレガシーなdiagnostics_for_file(今や概念的にはFileRule脱出弁)と並んで動作し、ルールを宣言しないプラグインにとってはゼロコストのno-opであり、同じプラグインごとのrescue境界で隔離されます。スライス1cは2パス(収集してから検証する)プラグイン向けにnode_file_contextを追加します。スライス1のワーキングデシジョンを以下にピン留めします。スライス1dはNodeContext(ノードルールのためのレキシカルコンテキスト)を追加します。スライス2の設計flow_contribution_fordynamic_return + type_specifier)は以下に記録されており、そのエンジンサーフェスは実装済みです(2つのDSL + 2つのランナーメソッド + レシーバークラス/メソッド名のゲーティングが、非推奨のflow_contribution_forファンアウトと並んで両ディスパッチサイトで参照される——完全に後方互換で、既存のすべてのコンシューマーは変更なくグリーン)。スライス2bはきれいに収まるすべてのコンシューマーを移行しました(rigor-mangrovedynamic_return;rigor-minitestrigor-rspecのマッチャーナローイング → type_specifier);残りは正当な理由で非推奨のflow_contribution_for脱出弁に留まります(狭いDSLが表現しない2つの貢献形状——rigor-rspecletバインディング、rigor-sorbetrigor-activerecordrigor-activestorage;スライス2 §「Outcome」を参照)。スライス3(2026-06-02): FactProvider命名 + 機械可読なケイパビリティカタログ(rigor plugins --capabilities、§「機械可読なケイパビリティカタログ」を参照)が着地し、最後にして最も複雑なdiagnostics_for_fileウォーカーであるrigor-actionpacknode_ruleへ移行されたので、同梱の診断発行プラグインはすべてレガシーウォーカーから外れました(14個がnode_rule上)。未完了(非ゲート、需要駆動): 4つの脱出弁コンシューマーを移行できるようにする延期されたdynamic_returnの一般化、および作成者ヘルパーのボイラープレート削減フォローオン(フェーズ0c〜0e)。

ADR-2が着手したインターフェース分離の作業を仕上げる決定を記録します。残る2つの命令型プラグインフック(flow_contribution_fordiagnostics_for_file)を、PHPStanのゲート/ペイロード不変条件に倣って、狭く・マニフェスト登録され・エンジンがゲートする拡張プロトコルの小さな集合へと分割します。肥大化したPlugin::Baseフックは、後方互換性のための非推奨の脱出弁として残します。このADRはADR-2のフックモデルを改訂するものであり、ADR-1が所有するフロー貢献のセマンティクスは変更しません。

根拠となるレビュー: docs/design/20260601-plugin-mechanism-pre-1.0-review.md §6(このADRの動機となったクロスプラグイン監査)。

Rigorのプラグイン契約(contract)には、2つの拡張スタイルが併存するまでに成長しました。

  1. 宣言的なマニフェストフィールド(10個)block_as_methodstrait_registriesheredoc_templatesnested_class_templatestype_node_resolversprotocol_contractshkt_registrationshkt_definitionssource_rbs_synthesizerowns_receiversopen_receivers。それぞれがregistry.pluginsから集約され、エンジン自身がインデックスする構造(SyntheticMethodScannerTypeNode::ResolverChainRegistry#contracts_for_path、HKTオーバーレイ、…)になり、エンジンが動詞/レシーバー/クラス/パスでゲートします。これはすでにPHPStan型です。エンジンがマッチするノードについてのみ参照する、狭いケイパビリティです。

  2. 命令型フック(2個)flow_contribution_for(call_node:, scope:)diagnostics_for_file(path:, scope:, root:)。エンジンはすべての未解決の呼び出しノード/すべてのファイルについてロード済みのすべてのプラグインを呼び出し、各プラグインは内部のif自分自身をゲートします。このディスパッチループはinference/method_dispatcher.rbinference/statement_evaluator.rbにそっくりそのまま複製されています(後者のコメントは「Mirrors … exactly(…をそっくり真似ている)」と認めています)。両方の箇所がrescue StandardError; nilしているため、ゲートを誤ったプラグインは何のシグナルもなく何も貢献しません。

ファンアウトして自己ゲートするスタイルこそが、設計上最もコストのかかる部分であり、そのコストはプラグインアーキテクチャに対してプロジェクトが掲げる目標 — AIエージェントにとっての可読性インターフェース単位のテスト容易性 — にちょうど対応します。

  • AI/人間の理解registry.plugins.each { |p| p.flow_contribution_for(...) }は、どのプラグインが参加するのか、どのレシーバー/メソッドに関心があるのかについて何も語りません。その選択性は各プラグインの自己ゲートするifの内側に存在するため、grepもできず列挙もできません。宣言的なフィールドはその正反対です。マニフェストのフィールド名が、それをどのサブシステムが消費するのかを読者(やツール)にそのまま伝えます。
  • テスト容易性。今日の唯一のハーネスはspec/integration/support/plugin_helpers.rbrun_pluginです。デモファイルを書き、完全なAnalysis::Runnerを実行し、result.diagnosticsをアサートします。「このフックはこのノードに対して型Xを返す」「このルールはここで発火する」を単独でアサートする方法はありません。フック名を冠したスペックでさえ、エンジン全体が生成する下流のcall.undefined-methodメッセージをアサートしています。対照的にPHPStanはRuleTestCase(フィクスチャに対するエラー集合をアサート)とTypeInferenceTestCase(インラインのassertType()で推論された型をアサート)を提供します。インターフェース単位のハーネスは、フックが狭くなって初めて可能になります。
  • ボイラープレートdiagnostics_for_fileが生のrootを渡し、ドキュメントが作者に「rootを自分で走査せよ」と指示している(plugin/base.rb)ため、約25個のプラグインが同じ再帰的なAST走査器を作り直しています。この走査器は、エンジンが走査を所有していないという理由だけで存在します。
  • パフォーマンス(副次的)。コストは事前フィルタなしにplugins × files × nodesにスケールします。レビューのブリーフによれば重要ではなく — キャッシュが緩和します — エンジンがフックをインデックスすれば無償で解決します。

この窓。フックのシグネチャは1.0で公開契約として凍結されます。命令型フックを1.0より後で分割するのは破壊的変更です。低コストでこれを行う最後の機会が今です。

PHPStanが行っていること(移植すべき唯一の不変条件)

Section titled “PHPStanが行っていること(移植すべき唯一の不変条件)”

PHPStanには約50個の拡張インターフェースがありますが、移植可能な不変条件は単一です。

安価なゲート述語(boolを返す/nilで辞退する)が、高コストなペイロードType/エラー/データを返す)から分離されている。エンジンはゲート値(getClass()getNodeType()isMethodSupported())で拡張をインデックスし、ペイロードはマッチするノード/レシーバーに対してのみ呼び出す。

ルール拡張はgetNodeType()を宣言し、エンジンは各ASTノードを、そのクラスに登録されたルールにのみディスパッチします。動的戻り値拡張はgetClass()isMethodSupported()を宣言します。唯一の真のキャッチオール(ExpressionTypeResolverExtension、ゲートなし)は、推奨されない最後の手段として文書化されています。ほぼすべての拡張は1つのインターフェースを実装する1つのクラスであり、フレームワークパッケージが多数を登録します。

2つの命令型フックを狭い拡張プロトコルへと分割します。それぞれは値オブジェクトを運ぶ新しいManifestフィールド(既存の10個の宣言的フィールドと同じ登録形状)を通じて宣言され、それぞれがエンジンのインデックスする1箇所で消費されます。肥大化したフックは非推奨だがサポートされる脱出弁として残し、どのプラグインも一斉に移行を強いられないようにします。

新しいプロトコル集合は意図的に小さく — PHPStanの50個ではなく、4つの新プロトコルとリネーム1件です。

1. DynamicReturnExtensionflow_contribution_forの戻り値スロットから)

Section titled “1. DynamicReturnExtension(flow_contribution_forの戻り値スロットから)”

呼び出し箇所ごとの戻り値型貢献。レシーバーでゲートされます。

class DynamicReturnExtension
# gate — the engine indexes by receiver class (reuses the
# owns_receivers indexing machinery) and by method name
def supported_receivers; end # => Array[String] (class names)
def supports?(method_name); end # => bool (cheap)
# payload — invoked only when both gates pass
def return_type_for(call_node, scope); end # => Rigor::FlowContribution | nil
end

マニフェストフィールド: dynamic_returns:。エンジンの箇所: MethodDispatcher#dispatch(精密なティアとRbsDispatchの間にある既存のプラグインティア)。ただし全プラグインへファンアウトする代わりにレシーバークラスでインデックスされます。

2. TypeSpecifyingExtensionflow_contribution_forのファクトスロットから)

Section titled “2. TypeSpecifyingExtension(flow_contribution_forのファクトスロットから)”

述語/アサーションによるナローイング — truthy_factsfalsey_factspost_return_factsスロット — であり、メソッドでゲートされエッジを意識します。

class TypeSpecifyingExtension
def supported_methods; end # => Array[Symbol] (gate)
# payload — `edge` is :truthy / :falsey / :post_return (assertion)
def specify(call_node, scope, edge); end # => Rigor::FlowContribution | nil
end

マニフェストフィールド: type_specifiers:。エンジンの箇所: StatementEvaluator#apply_plugin_assertions。メソッド名でインデックスされます。これによりディスパッチループの2つ目のコピーが退役します。

3. NodeRulediagnostics_for_fileから) — 要石

Section titled “3. NodeRule(diagnostics_for_fileから) — 要石”

ノードスコープの診断ルール。エンジンが単一のAST走査を所有し、各ノードを、そのノードのクラスに登録されたルールにのみディスパッチします。

class NodeRule
def node_type; end # => Class (Prism::CallNode, …) (gate)
def check(node, scope, context); end # => Array[Diagnostic] (payload)
end

マニフェストフィールド: node_rules:。エンジンの箇所: ランナーが各ファイルのASTを一度だけ走査し、ノードクラス → ルールのインデックスを構築し、マッチするノードごとにcheckを呼びます。これが、約25個の手作りの走査器を削除する変更です — それらが存在する理由(生のrootが渡される)が消えます。contextはADR-2が約束したが結局提供しなかった字句情報(現在のファイル、クラス/モジュール、メソッド、可視性)を運ぶので、ルールは走査によってそれを再導出する必要がありません。

4. FileRule(脱出弁、diagnostics_for_fileから)

Section titled “4. FileRule(脱出弁、diagnostics_for_fileから)”

ノードスコープのルールでは表現できない、真にクロスファイル/インデックス検証のケース(例: 発見されたモデルインデックスをファイルに対して検証する)のためのファイル全体ルール。PHPStanのExpressionTypeResolverExtensionに倣い、最後の手段として文書化されます。

class FileRule
def check(path, root, scope); end # => Array[Diagnostic]
end

マニフェストフィールド: file_rules:。これはレガシーなdiagnostics_for_fileフックの移行先です。古いフックは「名前のない非推奨のFileRule」として捉え直されます。

5. FactProvider(既存のprepareサーフェスのリネーム)

Section titled “5. FactProvider(既存のprepareサーフェスのリネーム)”

prepare(services)produces:consumes:サーフェス(ADR-9)はすでに狭く、トポロジカルに順序付けられています。このADRは対称性と発見容易性のためにそれをプロトコルとして名付けるだけで、挙動の変更はありません。(PHPStanのCollector<TNode, TValue> — ノードごとの構造化されたクロスファイル収集 — は、NodeRuleが導入するエンジン所有の走査の上に積層するFactProviderの自然な将来拡張です。消費側が必要とするまで先送りします。)

flow_contribution_fordiagnostics_for_fileは呼び出し可能なまま、非推奨と明示されます。内部的にローダーはレガシープラグインを次のように適応させます。

  • flow_contribution_for → 単一の全レシーバーDynamicReturnExtension+全メソッドTypeSpecifyingExtension(すなわち、ゲートなしのファンアウト挙動を保存)。
  • diagnostics_for_file → 単一のFileRule

そのため、レガシープラグインはレガシーな(インデックスされない)コストで動き続け、移行済みのプラグインはインデックス化されたテスト可能なサーフェスを得ます。宣言的フィールド(10個)は手つかずです — sinatra/devise/dry-struct/typescript-utility-typesと、hanami/webのRBS/契約の半分はすでに分離されており、変更は不要です。

機械可読なケイパビリティカタログ

Section titled “機械可読なケイパビリティカタログ”

すべての拡張がマニフェスト上で宣言されるようになるため、エンジンはrigor plugins --capabilitiesカタログを出力できます。プラグインごとに、戻り値型を貢献するレシーバー、特定(specify)するメソッド、ルールを当てるノード型、生成/消費するファクトを示します。PHPStanには機械可読なinterface → tagレジストリがありませんが、Rigorにはあります。これはAI可読性の目標に直接寄与します — エージェントは自己ゲートするコードを1行も読まずに、すべてのプラグインが何をするかを列挙できます。

インターフェース単位のテストハーネス

Section titled “インターフェース単位のテストハーネス”

プロトコルごとにテスト基底を提供します。これは狭いインターフェースだけが到達可能にする目標の片割れです。

  • NodeRuleTest — ノード+スコープを与え、返された診断をアサート(PHPStanのRuleTestCaseに相当)。
  • DynamicReturnTestTypeSpecifierTest — 呼び出し+スコープを与え、貢献された型/ファクトをアサート(PHPStanのTypeInferenceTestCaseに相当)。

既存のrun_pluginエンドツーエンドハーネスは、統合カバレッジのために残ります。

  1. NodeRule+エンジン所有の走査(最高のレバレッジ。走査器のボイラープレートを退役させ、NodeRuleTestを解放する)。レガシーなdiagnostics_for_fileFileRuleとして捉え直す。
  2. flow_contribution_forの分割DynamicReturnExtension(レシーバーインデックス、owns_receiversの機構を再利用)+TypeSpecifyingExtensionへ。複製されたディスパッチループを1つのインデックス化されたレジストリへ畳み込む。
  3. FactProviderのネーミング+ケイパビリティカタログコマンド。
  4. 同梱プラグインをレガシーフックから移行する。プラグインファミリーごとに、移行済みのプラグインで作者ヘルパー層(レビュー§1.3)が不要になるにつれてプラグインごとの走査器を削除する。

スライス1〜3はエンジン側であり後方互換です。スライス4は機械的かつ漸進的です。

スライス1のワーキングデシジョン(実装済み)

Section titled “スライス1のワーキングデシジョン(実装済み)”
  • 登録はマニフェストフィールドではなくクラスDSLnode_ruleは既存のproducer DSLを踏襲します。ブロックはinstance_execを通じて走るので、プラグインインスタンス(configservicesio_boundarydiagnosticservices.fact_store)がスコープに入ります。ルールはインスタンスを必要とするロジックを運ぶので、block_as_methodsなどのようにクラスロード時に構築される純粋なマニフェスト値オブジェクトにはできません。
  • エンジンはランナーではなくBase#node_rule_diagnosticsで走査を所有します。それを(Source::NodeWalkerの上で)Baseに置くと、単一のディスパッチ地点がランナーとワーカーセッションの両方からそれぞれ1行の呼び出しで共有され、走査が単独でテスト可能になります。
  • node_typeは(厳密なクラスではなく)node.is_a?(node_type)でマッチします。そのため、ルールはすべてを見るためにPrism::Nodeを登録することも、一般的なケース向けに具体的なクラスを登録することもできます。
  • diagnostics_for_fileに対して加算的です。両方が走ります。レガシーフックはFileRule脱出弁であり、変更されません。どのプラグインも移行を強いられません。
  • ブロックは(node, scope, path)を受け取ります。ADR-2が約束したが結局提供しなかったリッチなContextInfo(字句的なクラス/メソッド/可視性)は先送りのままです — scopeはすでにself_typeとほとんどのルールが必要とするナローイングファクトを運びます。pathdiagnostic(node, path:)のために供給されます。
  • 2パス(収集してから検証する)プラグインスライス1cで解決済み(下記参照)。ノードごとのNodeRuleは、エンジンの単一の前方走査の中でファイルローカルな収集パスを表現できません(参照が宣言に先行しうる)ので、node_file_contextプリパスフックがそれを供給します。

スライス1c — 2パスサポート(node_file_context、実装済み)

Section titled “スライス1c — 2パスサポート(node_file_context、実装済み)”

node_file_context { |root, scope| … }は、いずれのノードルールが発火するよりも前に、ファイルごとに一度(instance_exec経由で)走り、すべてのnode_ruleブロックにその第4引数として通される任意のファイルローカルな値を返します。これこそが、同一ファイルの2パスプラグインに手作りの検証走査を捨てさせるものです。収集パスが閉じた名前空間を一度計算し(検証の前に完了しなければなりません)、エンジンが検証走査を所有します。第4ブロック引数は後方互換です — 既存の3パラメータのブロックはそれを無視します。

2つの2パス形状の間の分割は意図的です。

  • 同一ファイル収集(例: statesmanが宣言された状態を集める) → node_file_contextを使う。収集パス自体が共有のSource::NodeWalkerを使うので、手作りの走査は残りません。
  • クロスファイル収集(例: activerecordのdb/schema.rbapp/modelsからのモデルインデックス) → すでに#prepareservices.fact_storeFactProviderサーフェス)に属します。ノードルールは公開されたファクトを直接読み、ファイルごとのコンテキストを必要としません。

rigor-statesmanが最初の2パス消費側として移行されました。そのcollectnode_file_contextに、validatenode_rule(Prism::CallNode)になり、手作りの走査は両方とも消えました(挙動は不変、統合スペックはグリーン)。

スライス2 — flow_contribution_forの分割(設計)

Section titled “スライス2 — flow_contribution_forの分割(設計)”

2つ目の命令型フックflow_contribution_for(call_node:, scope:)は、スライス1がdiagnostics_for_fileを分割したのと同じやり方で分割されます。すなわち、エンジンがインデックス化する狭く宣言的にゲートされるクラスDSLにし、太いフックは非推奨の脱出弁として残します。

前提となる事実FlowContributionは9つのスロットを持ちますが、エンジンはプラグインのflow_contribution_forをちょうど2箇所で参照し、ちょうど2つのスロットを読みます:

  • Inference::MethodDispatcher#try_plugin_contributionはすべてのプラグインの貢献をマージし、.return_type(呼び出しサイトごとの戻り型、RbsDispatchに先行)のみを使います。
  • Inference::StatementEvaluator#apply_plugin_assertionsはすべてのプラグインの貢献をマージし、.post_return_facts(アサーションエッジのナローイング)のみを使います。

したがって分割はクリーンで、2つの消費サイトと1:1です:

dynamic_return(→ return_type、レシーバーゲート)node_ruleproducerを反映するクラスDSL:

dynamic_return receivers: ["ActiveRecord::Base"] do |call_node, scope|
# self = plugin instance; return a Rigor::Type or nil
end

エンジンは、呼び出しのレシーバー型のクラスが宣言されたreceivers:エントリと等しいか、それを継承する場合にのみブロックを呼びます(Environment#class_ordering——今や標準のメカニズム——経由でマッチ)。メソッド名と型形状の精緻化(例: Mangroveキャリアのtype_args)はブロック内に留まります。Typeを返します(辞退する場合はnil)。receivers:はgrep可能でインデックス可能なゲートです——エンジンはすべての呼び出しについてすべてのプラグインに尋ねる代わりに、拡張をクラスごとにグループ化できます。

type_specifier(→ post_return_facts、メソッドゲート)

type_specifier methods: [:assert_kind_of, :assert_instance_of] do |call_node, scope|
# return an Array of post-return facts, or nil
end

エンジンはcall_node.nameが宣言されたmethods:に含まれる場合にのみブロックを呼びます。マージャーが既に適用しているのと同じpost_return_factsを返します。(truthy_factsfalsey_factsのエッジスロットは、プラグインからの条件エッジナローイングが配線されたときのために同じサーフェスの一部です;今日消費されるのはpost_return_factsのみなので、フロアはそれを対象とします。)

登録とゲーティング。どちらもproducerスタイルのクラスDSLであり(プラグインインスタンスを必要とするロジックを持つため、マニフェストの値オブジェクトではありません)、クラス上に格納され、Plugin::Registryによってレシーバークラスインデックス(dynamic_return)とメソッド名インデックス(type_specifier)へ集約されます。エンジンは、重複したcollect_plugin_contributionsファンアウトの代わりに、既存の2サイトでマッチするサブセットを参照します——そのファンアウトはmethod_dispatcher.rbstatement_evaluator.rbの両方から削除されます(スライス1レビューが指摘した2つのコピー)。

後方互換性flow_contribution_forは非推奨のまま残り、エンジンは依然として両サイトでそれを(太いファンアウトとして)参照し、インデックス化された結果とマージします。そのため未移行のコンシューマー——rigor-sorbetrigor-activerecordrigor-activestoragerigor-mangroverigor-rspecrigor-minitest、およびサンプルプラグイン——は手付かずで動作し続けます;dynamic_returntype_specifierへの移行は、プラグインファミリーごとに1つずつ、それぞれをゴールデンマスター統合スペックでガードしながら段階的に行われます。フロー貢献は型ナローイング(ひいては診断)に供給されるため、すべての移行は着地前に振る舞いを保存することが検証されます——偽陽性フロアが拘束条件です。

コンシューマーのマッピング(各々がどのサーフェスを使うか):

  • dynamic_return: mangrove(アンラップ → 担われたtype_args[0])——移行済み
  • type_specifier: minitest(アサーションナローイング)——移行済み;rspecのマッチャーナローイング——適合するが移行は保留中。

結果/脱出弁は重要な役割を担う。コンシューマーの移行によって、狭いDSLが意図的に表現しない2つの貢献形状がよくあるものだと判明し、それらのコンシューマーは正当な理由で非推奨のflow_contribution_forに留まります(まさにPHPStanが推奨しないExpressionTypeResolverExtensionの総当たりが果たす役割と同じです):

  • メソッドゲートの戻り型。 rspecのlet(:x) { create(:x) }subjectバインディングは、レシーバークラスではなくメソッド名letsubject)でゲートされた呼び出しの戻り型を設定します。dynamic_returnはレシーバーゲートであり、type_specifierは(戻り型ではなく)ファクトを生成するため、どちらも適合しません。sorbetのsig駆動の戻り値も同様です——固定のレシーバークラスではなく、呼ばれたメソッドがsigを持つことをキーとします。
  • 動的レシーバー。 activestorageは、プロジェクトの発見されたモデルクラスにAttached::One::Manyを貢献します——dynamic_returnが宣言する静的なreceivers:リストではなく、プロジェクトごとの集合です。

これらは設計上、脱出弁に留まります。将来のdynamic_returnの一般化(メソッドゲートの戻り値のためのオプションのmethods:ゲート、および/または動的レシーバー述語)がそれらを移行するためのパスであり、需要が狭いサーフェスの拡張を正当化するまで延期されます。

  • ADR-2 — このADRはその肥大化フックモデルを改訂します。Scope/Type/Reflection/FactStore/IoBoundaryのサービス契約と10個の宣言的フィールドは変更されません。「広範な式/演算子フックは先送り」という姿勢は補強されます。FileRuleが唯一のキャッチオールであり、推奨されません。
  • ADR-1 — フロー貢献のセマンティクス(バンドルフィールド、確実性ルール、マージポリシー)は手つかずです。DynamicReturnExtensionTypeSpecifyingExtensionは依然としてFlowContributionバンドルを返し、同じMergerでマージされます。
  • ADR-9FactProviderは既存のprepareサーフェスをリネームしたものです。
  • ADR-16 — マジックメンバー/動的リフレクションのニーズはすでにマクロサブストレートでカバーされています。このADRはリフレクションプロトコルを追加しません。
  • ADR-15 — 狭い拡張オブジェクトは実行ごとの可変ディスパッチ状態を運びません(rigor-sorbetのアイデンティティをキーとするハッシュとは異なります)。これはRactorのワーカーごとのインスタンス化の問題(ADR-2 § Open Questions)を単純化します。
CandidateStatusReason
Keep the two fat hooks as-is for 1.0Rejected機能的には十分だが、シグネチャは1.0で凍結し、目標(AI可読性、インターフェース単位のテスト)は自己ゲートするフックでは到達不能。窓は今しかない。
Port PHPStan’s full ~50-interface catalogueRejected過剰分離。マジックメンバーはADR-16でカバー済み。デッドコード/使用制限はデマンド駆動(レビュー§7)。今日のRigorには4プロトコル+リネームが正しい粒度。
Remove the fat hooks outright (no escape valve)Rejected31プラグインの一斉ビッグバン移行を強い、真のファイル全体ルールの表現手段を残さない。FileRuleがキャッチオールを残すが推奨されない。
A single generic “extension” object with optional methodsRejectedそれは現在の肥大化したPlugin::Baseそのもの。このADRが解決する自己ゲートしてインデックスできない問題を再生産する。
Structured Collector<TNode,TValue> nowDeferredノードごとのクロスファイル収集を消費側が必要としたら、FactProvider+エンジン所有の走査の上に積層する。

肯定的:

  • プラグイン契約が一様に宣言的+エンジンゲート型になります。既存の10フィールドに4つの新プロトコルが加わり、すべてマニフェストから列挙可能です。
  • インターフェース単位のテストハーネスが可能になります。プラグインテストはエンジン全体の下流挙動をアサートするのをやめます。
  • エンジンがAST走査を所有します。約25個の手作り走査器と複製されたディスパッチループが退役します。
  • ケイパビリティカタログが、すべてのプラグインの挙動の完全でgrep可能なマップをAIエージェント(と人間)に与えます — PHPStanに欠けているケイパビリティです。
  • インデックス化によりplugins × files × nodesのファンアウトが除去されます(副次的)。

否定的:

  • 文書化し安定に保つべき公開プロトコルサーフェスがより大きくなります。
  • レガシーと狭いプラグインが併存する移行期間があります(脱出弁アダプタによって管理されます)。
  • ノードクラスとレシーバーのインデックスを構築・維持するエンジン作業が必要です。

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