コンテンツにスキップ

Plugin Registration / Loading (slice 1)

ステータス:v0.1.0スライス(slice)1規範的。プラグイン作成者がプラグインの登録マニフェスト宣言Analysis::Runnerによるロードに関して使用するパブリックサーフェス(surface)を固定します。貢献プロトコル(動的返却・型指定・動的リフレクション)は後続のv0.1.0スライスで追加されるため、ここでは定義しません。

拘束力のある設計サーフェスはADR-2です;v0.1.0の準備状況マップはdocs/design/20260505-v0.1.0-readiness.mdにあります。この仕様がADR-2と矛盾する場合、ADRが優先されます。

パブリックネームスペース(ドリフト固定済み)

Section titled “パブリックネームスペース(ドリフト固定済み)”

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

プラグイン登録のモジュールレベルエントリー。

メソッド目的
Rigor::Plugin.register(plugin_class)プラグインgemがロード時にRigor::Plugin::Baseサブクラスをアドバタイズするために呼び出す。
Rigor::Plugin.registered_for(id)マニフェストidによるローダー側のルックアップ。
Rigor::Plugin.registered凍結された{ id => class }スナップショット。
Rigor::Plugin.unregister!(id = nil)テスト専用リセット。プラグイン契約(contract)はgem作成者にこれを呼び出すことを要求しない。

レジストリはプロセスグローバルでmutexガードされています。同じクラスを2回登録することはno-opです;同じidで異なるクラスを登録するとRigor::Plugin::LoadErrorが発生するため、2つのプラグインが互いをサイレントに上書きすることはできません。

すべてのプラグインがサブクラス化する基底クラス。

class MyPlugin < Rigor::Plugin::Base
manifest(
id: "my-plugin",
version: "0.1.0",
description: "...",
config_schema: { "flag" => :boolean }
)
def init(services)
@reflection = services.reflection
end
end

クラスレベルのmanifest(**fields)はクラス定義時に一度マニフェストを宣言します;引数なしで呼び出すと、キャッシュされたManifestを返します。インスタンスレベルのmanifestはクラスに委譲します。

#initialize(services:, config: {})は注入されたサービスとユーザーのconfigの凍結コピーを格納します。#init(services)はプラグインがサービスコンテナから状態を接続するために使用するオーバーライドフックで、デフォルト実装はno-opです。

Baseの完全なサーフェスはRBS(sig/rigor/plugin/base.rbs)で宣言され、自己チェックされます。すなわちバンドルされたプラグイン/サンプルlibツリーがrigor checkmake verifyとCIにチェーンされたmake check-pluginsゲート)を通ります。プラグインの部分型が継承する契約呼び出し(manifest.…io_boundary.…)をBaseのRBSに対して解決するADR-43のRBS完全祖先解決と組み合わせることで、契約サーフェスを誤用するプラグイン(契約が宣言しないメソッドや、名前変更されたヘルパーを呼ぶプラグイン)はcall.undefined-methodでビルドを失敗させます。補完的な構造スペック(spec/integration/plugin_contract_conformance_spec.rb)がもう半分をカバーします。すなわち各フックのオーバーライド(init / prepare / flow_contribution_for / diagnostics_for_file)はエンジンの呼び出しでMUST呼び出し可能であり続けます ── エンジンが供給するパラメータを落とすナローイングオーバーライドは失敗します(パラメータ/アリティのリスコフ互換性、ADR-5)。

#diagnostics_for_file(path:, scope:, root:)(スライス5)はファイル全体の診断フックです。デフォルトは空の配列を返します。プラグイン作成者はこれをオーバーライドしてroot(解析されたPrism::Node)を自分で走査し、Rigor::Analysis::Diagnostic行の配列を返してもよい(MAY)ですが、ノードスコープのチェックに推奨されるサーフェスはnode_rule(下記)であり、これはエンジンに走査を所有させます。#diagnostics_for_fileは真にファイルスコープな診断——単一のロードエラー行、または解析済みファイル全体を一度に必要とするチェック——のために予約されています。ランナーはADR-7 §「スライス5-B」に従って返されたすべての診断をsource_family: "plugin.<manifest.id>"で再スタンプするため、プラグイン作成者が誤って別のプラグインのidで公開することはありません。フック内のプラグイン例外はrigor checkをクラッシュさせるのではなく、:plugin_loaderruntime-error診断として隔離されます。

ノードスコープのルール — node_rule / #node_rule_diagnostics(ADR-37)

Section titled “ノードスコープのルール — node_rule / #node_rule_diagnostics(ADR-37)”

node_rule(node_type) { |node, scope, path, file_context, context| … }は、ノードスコープの診断ルールを宣言するクラスレベルのDSL(producerスタイルの形状)です。エンジンは解析される各ファイルのASTを一度走査し、node.is_a?(node_type)となるすべてのノードをそのルールへディスパッチします。そのためプラグイン作成者はチェックを書き、走査は決して書きません——これがプラグインから手書きのdef walk / compact_child_nodes.eachの再帰を取り除けるようにするものです。ブロックはinstance_execを通して実行され(そのためselfはプラグインインスタンス——configservicesservices.fact_storediagnosticがすべてスコープ内)、(node, scope, path, file_context, context)を受け取り、Array<Rigor::Analysis::Diagnostic>を返します(何も発火させない場合は空)。node_typePrism::Nodeサブクラスでなければなりません(MUST)。型ごとの複数のルールは宣言順に実行されます。エンジンはそれらをインスタンスメソッド#node_rule_diagnostics(path:, scope:, root:)を通して呼び出し、ランナーは同じplugin.<id>スタンプとプラグインごとの例外隔離のもとで#diagnostics_for_fileと並んでそれを呼びます;ルールを宣言しないプラグインはゼロコストです。

5番目のブロック引数context(ADR-37スライス1d)は、ノードのレキシカルな祖先チェーンを担うRigor::Plugin::NodeContextです——ADR-2が約束したContextInfoです。これは#ancestors(完全なチェーン、最も外側が先、ノード自体を除く)に加えて、便利メソッド#enclosing_def#enclosing_module#enclosing_block(name)を公開します。ルールは、チェックがノードの位置に依存するときにそれを読みます: before_action / renderが属する内包コントローラー(rigor-actionpackは名前空間修飾されたコントローラー名をcontext.ancestorsから再導出する)、マッチャーが配置されるdescribe <Model>rigor-shoulda-matchers)、または遅延t('.key')が展開されるアクション(rigor-rails-i18n)など。より少ないパラメータを取るブロックは、末尾の引数を単に無視します(後方互換)。

node_file_context { |root, scope| … }は2パス(収集してから検証する)プラグインをサポートします。これはいずれのノードルールが発火する前に、ファイルごとに一度(instance_exec経由で)実行され、その戻り値は4番目のブロック引数としてすべてのルールへ渡されます(既存の3パラメータのブロックはそれを無視します)。同一ファイルの収集——参照を検証する前に宣言された名前を集める——はここに属します。なぜなら、エンジンの単一の前方走査は、参照に到達する前に収集を完了できないからです。クロスファイルの収集は代わりに#prepare + services.fact_storeに属します;ノードルールは公開されたファクトを直接読み、ファイルごとのコンテキストを必要としません。

診断の位置決め — #diagnostic(ADR-37作成者ヘルパー)

Section titled “診断の位置決め — #diagnostic(ADR-37作成者ヘルパー)”

#diagnostic(node, path:, message:, severity: :error, rule: nil, location: nil)は、nodeに位置づけられたRigor::Analysis::Diagnosticを構築し、他のすべてのプラグインが手作業で再導出する1始まりのline / start_column + 1の慣習を内部に取り込みます。サブロケーションを指すにはlocation:(Prismのロケーション)——典型的にはnode.message_loc——を渡します。そうすればマッチャー/メソッド名の診断は、レシーバーにまたがる呼び出し全体ではなく、その名前を指します;nillocation:node.locationにフォールバックします。作成者はsource_familyを設定してはなりません(MUST NOT)(ランナーがスタンプします)。基礎となるコンストラクタRigor::Analysis::Diagnostic.from_node(node, …).from_location(location, …)は、コアルールやその他のプロデューサーのためにパブリックです。

Rigor::Plugin::Base.suggest(name, candidates)(ボイラープレート削減計画 §0c)は、共有の「もしかして…?」ヘルパーです: DidYouMean::SpellChecker(エンジンのRuby自身のNoMethodErrorヒントが使うもの)経由でcandidatesのうちnameに最も近いものを返すか、nilを返します。これはクラスメソッドなので、プラグインインスタンスからもAnalyzerモジュール関数からも呼び出せ、プラグインがかつて持っていた手書きのLevenshteinのコピーを置き換えます。これは既に発行された診断の提案テキストにのみ影響し、診断が発火するかどうかには決して影響しません。

#prepare(services)(ADR-9)はプロジェクト全体の事前パスフックで、ファイルごとの解析が始まる前に一度呼ばれます。クロスプラグインファクト(manifest(produces:))を公開するプラグインはこれをオーバーライドしてプロジェクトを走査し、services.fact_store.publish(...)を呼びます;ローダーのトポロジカル順序付けが、プロデューサーのprepareがいずれのコンシューマーのものよりも先に実行されることを保証します。デフォルトはno-opです。

引数リテラルの抽出 — Source::Literals(ボイラープレート計画 §0a)

Section titled “引数リテラルの抽出 — Source::Literals(ボイラープレート計画 §0a)”

Rigor::Source::Literalsは、「このPrism引数ノードはリテラルの:sym / "str"か、もしそうなら何を名指しているか?」——ほぼすべてのDSLウォーカーが問う質問(state :drafthas_one_attached :avatarvalidate_presence_of(:name))——への共有の答えです。手書きのnode.unescaped.to_sym if SymbolNode || StringNodeよりも推奨される抽出器で、パブリックAPIドリフト仕様(SOURCE_LITERALS_SINGLETON)に固定され、public-api.mdの「Rigor::Source::*は内部である」ルールから免除されています。メソッドはmodule_functionなので、それぞれRigor::Source::Literals.symbol(node)として呼び出せます。

単一ノードのサーフェスは2つの軸——どのノード種別を受け入れるか、呼び出し側が何を返してほしいか——にわたるグリッドで、それ以外のいかなるノード(nilを含む)に対してもnilを返します:

受け入れSymbolString
:symのみ.symbol(node).symbol_name(node)
:symまたは"str".symbol_or_string(node).symbol_or_string_name(node)

SymbolNodeのみの形式が存在するのは、state :draftstate "draft"を区別するDSLが、サイレントに拡幅するのではなくその区別を保てるようにするためです。#valueではなく#unescapedが使われるのは、補間のない"foo" / :fooが両方のノード種別で一貫して:foo / "foo"へラウンドトリップするようにするためです。

グリッドの上に2つの呼び出し引数ヘルパーが乗ります:

  • .symbol_arguments(call_node)Array[Symbol] — ソース順のすべてのリテラルなSymbol/String位置引数;非リテラルの引数は捨てられます;呼び出しが引数リストを持たないときは[]
  • .symbol_arg(call_node, index)Symbol? — 位置indexのリテラル、または呼び出しが引数リストを持たない・インデックスが範囲外・その引数がリテラルなSymbol/Stringでないときはnil

戻り型とナローイングの貢献 — dynamic_return / type_specifier(ADR-37スライス2)

Section titled “戻り型とナローイングの貢献 — dynamic_return / type_specifier(ADR-37スライス2)”

flow_contribution_forはちょうど2つのエンジンサイトで参照され、それぞれが返されたバンドルのちょうど1スロットを読んでいました: MethodDispatcher.return_type(呼び出しサイトごとの戻り型)を読み、StatementEvaluator.post_return_facts(アサーションエッジのナローイング)を読みます。ADR-37スライス2は、これら2つの消費サイトを2つの狭く宣言的にゲートされるクラスDSL——producerスタイルの形状なので、ブロックはロジックを担いinstance_execを通して実行されます——へ分割します:

  • dynamic_return(receivers:) { |call_node, scope| Type | nil } — レシーバーのクラスでゲートされた、呼び出しサイトごとの戻り型。エンジンは、呼び出しのレシーバー型のクラスが宣言されたreceivers:エントリーと等しいか、それを継承する場合にのみブロックを呼びます(Environment#class_ordering経由でマッチ);最初の非nilが勝ちます。エンジンはそれを#dynamic_return_type(call_node:, scope:, receiver_type:)を通して呼び出します。rigor-mangrove(アンラップ → 担われたtype_args[0])が実装済みのコンシューマーです。
    • 二項演算子はここでは通常の呼び出しです。Rubyのa + b:+という名前のPrism::CallNodeに解析されるため、他のあらゆる呼び出しと同様にこのフックへ到達します。すなわちdynamic_return(receivers: ["Money"])規則はcall_node.name ∈ {:+, :-, :*, :/, :<=>, …}で分岐して演算子の結果型を返すことができ ── これはself/左オペランドのケースに対するPHPStanのOperatorTypeSpecifyingExtensionのRigor版であり、演算子固有の拡張ポイントを持ちません。spec/integration/plugin_operator_dynamic_return_spec.rbによって確認済みです。注意(coerce方向):ゲートはレシーバーのクラスにかかり、Rubyは1 + moneyIntegerでディスパッチするため、["Money"]規則はそこでは発火しません。その結果はIntegerとして左バイアスで型付けされます(ADR-42を参照)。
  • type_specifier(methods:) { |call_node, scope| facts | nil }call_node.nameが宣言されたmethods:に含まれることでゲートされた、戻り値後のナローイングファクト。エンジンはそれを#type_specifier_facts(call_node:, scope:)を通して呼び出します。rigor-minitest(アサーションナローイング)とrigor-rspecのマッチャーナローイングが実装済みのコンシューマーです。

receivers: / methods:は、rigor plugins --capabilitiesカタログ(ADR-37 §「機械可読なケイパビリティカタログ」)が列挙する、grep可能でインデックス可能なゲートです。

#flow_contribution_for(call_node:, scope:)(ADR-9 / ADR-2)は、元の太い戻り型貢献フックで、狭いDSLと並んで同じ2サイトで参照されます。認識された呼び出しエッジに対してRigor::FlowContribution(精密なreturn_typeおよび/またはナローイングファクトを担う)を返すか、辞退する場合はnilを返します(デフォルト)。これは非推奨の脱出弁であり、狭いDSLが意図的に表現しない2つの貢献形状——メソッドゲートの戻り型(rigor-rspeclet / subjectバインディング;rigor-sorbetのsig駆動の戻り値)と動的なプロジェクトごとのレシーバー集合(発見されたモデルクラス上のrigor-activestorageAttached::One / ::Many)——のために残されています。新しいプラグインはdynamic_return / type_specifierを優先すべきです;flow_contribution_forは文書化された最後の手段です(PHPStanのExpressionTypeResolverExtensionが果たす役割)。

機械可読なケイパビリティカタログ — rigor plugins --capabilities(ADR-37スライス3)

Section titled “機械可読なケイパビリティカタログ — rigor plugins --capabilities(ADR-37スライス3)”

rigor plugins --capabilitiesは、各プラグインが何をするのかをエージェントが学ぶために列挙する、プラグインごとの拡張プロトコルゲートを出力します。ロードされたプラグインのみが現れます(ロードに失敗したプラグインはケイパビリティ(capability)を一切貢献しません)。--format jsonでは出力は次のとおりです:

{
"configuration": "<path to .rigor.yml, or null>",
"capabilities": [
{
"id": "<plugin id>",
"gem": "<gem name>",
"version": "<plugin version>",
"node_rule_types": ["<Prism node class name>", "..."],
"dynamic_return_receivers": ["<receiver class name>", "..."],
"type_specifier_methods": ["<method name>", "..."],
"produces": ["<fact id>", "..."],
"consumes": ["<plugin_id/fact_name>", "..."]
}
]
}

5つのケイパビリティ配列は、まさに上記の狭いプロトコルの宣言的ゲートです: node_rule_typesは各node_ruleノード型から、dynamic_return_receiversdynamic_return(receivers:)から、type_specifier_methodstype_specifier(methods:)から、produces / consumesはADR-9のマニフェストフィールドから来ます。プラグインがそのサーフェスに対して何も宣言しないとき配列は空になり、テキストビューは空のサーフェスを完全に省きます。これは、プラグインコードをロードせずにゲートをgrep可能・インデックス可能に保つ契約です。

ターゲットライブラリの呼び出し — Plugin::Inflector / Plugin::Isolation / Plugin::Box(ADR-39)

Section titled “ターゲットライブラリの呼び出し — Plugin::Inflector / Plugin::Isolation / Plugin::Box(ADR-39)”

ADR-39は、プラグインがターゲットとするライブラリの純粋で許可リストに載ったメソッドを直接呼び出すことを許します(PHPStan拡張が実際のフレームワークを呼び出すことのRuby版)——それらを再実装するのではなく。ライブラリの実際の振る舞いから逸脱する再実装は誤ったファクト、すなわち偽陽性です。このルールは、エンジンの定数畳み込み層が使うのと同じハーネスによって境界づけられます: 明示的な純粋メソッドの許可リスト、Rigor由来の入力、チェックされたデータ結果、そしてライブラリが到達不能なときの辞退(決して近似しない)。これはADR-2の、解析対象のアプリケーション自身のコードを実行することの禁止を緩めるものではありません——ターゲットライブラリは信頼された宣言済みの依存であり、プロジェクトのソースとは区別されます。

  • Rigor::Plugin::Inflector — 実装済みのコンシューマー + Railsファミリープラグインのための共有の語形変化ヘルパー。underscore / camelize / singularize / pluralize / classify / tableizeは実際のActiveSupport::Inflectorに委譲します;これは近似を一切持ちません(gemが到達不能なときは例外を投げるので、呼び出し側は黙らせるために辞退します)。rigor-rails-routes / rigor-activerecord / rigor-actionpack / rigor-actionmailer / rigor-factorybotがこれを使います。
  • Rigor::Plugin::Isolation — 呼び出しのための選択可能な分離戦略で、RIGOR_PLUGIN_ISOLATIONによって選ばれます(exe/rigorランチャーが.rigor.ymlplugins_isolation:をそれにマップします)。3つのバックエンドにわたる1つのcall(feature:, receiver:, method:, args:)インターフェースで、processがデフォルト:
    • process(デフォルト) — 単一のforkされた永続ワーカー(呼び出しごとではなく、一度だけforkして再利用)がライブラリをロード + 呼び出し、Marshalパイプ経由でデータを返します;ワーカーのクラッシュ(SIGSEGVさえも)は封じ込められます——親は辞退して再生成します。forkが利用できない場所ではnoneにフォールバックします。
    • none — メイン空間にロードして直接呼び出します(分離なし;forkなしのフォールバック + 明示的なオプトアウト)。
    • ruby_boxRuby::BoxRigor::Plugin::Box;exe/rigorRUBY_BOX=1のもとで再execします)の内部で呼び出します。モンキーパッチ + バージョンをインプロセスで分離します。実験的;上流Ruby::Box VMのバグでゲート中。
  • Rigor::Plugin::Boxruby_box戦略を支えるRuby::Boxラッパー(enabled? / require_feature / eval)。

ターゲットライブラリのファクトを必要とするプラグインはPlugin::Inflectorを(または、新しいライブラリの場合は独自の許可リストを持つIsolation.callを)呼びます;分離が重要なとき、ターゲットをメイン空間に直接requireすることは決してありません。ターゲットgemへの本番依存は、プラグイン自身のgemspecに属します。

1つのプラグインのアイデンティティを記述する凍結値オブジェクト。フィールド:

フィールド目的
id/\A[a-z][a-z0-9._-]*\z/に一致するString安定した識別子;PluginEntry#idplugin.<id>.<rule>診断プレフィックスとして使用される。
version空でないStringプラグインバージョン;キャッシュ無効化のためPluginEntry#versionに格納される。
descriptionString?人間が読めるサマリー。
config_schema`{ String => Symbol{ kind:, default: } }`

以下の拡張フィールド0.1.xサイクルを通じて追加されました。すべてオプションで、1.0前のプラグイン契約に対して追加的です;これらを1つも宣言しないプラグインはただのファイルごとのアナライザーです:

フィールド目的
producesArray<Symbol>このプラグインが公開するクロスプラグインファクト(ADR-9)。
consumesArray<Consumption>このプラグインが読むクロスプラグインファクト({ plugin_id:, name:, optional: });ローダーのトポロジカル順序付けを駆動する(ADR-9)。
signature_pathsArray<String>プラグインが貢献するRBSシグネチャディレクトリ、プラグインgemルートからの相対;Loaderが解決し環境にマージする(ADR-25)。
owns_receiversArray<String>ディスパッチルーティングのためにこのプラグインが所有するレシーバークラス名。
open_receiversArray<String>call.undefined-methodから免除されるレシーバークラス名(そのメソッド表面が無制限 — 例: ActiveRecord::Relation)(ADR-26)。
type_node_resolversArrayカスタムなRBS型名解決を貢献するPlugin::TypeNodeResolverエントリー(ADR-13)。
protocol_contractsArray<ProtocolContract>パススコープの振る舞い契約(path_glob + method_name + param/return型 + 重大度);provide-and-check(ADR-28)。
source_rbs_synthesizer#call(path) -> String?env構築時にプロジェクトソースファイルからRBSを合成する呼び出し可能オブジェクト(例: rbs-inline取り込み)(ADR-32)。
block_as_methods, heredoc_templates, trait_registries, external_filesArray<Plugin::Macro::*>ADR-16のマクロ / DSL展開基板の4つのティア(A / C / B / D)。値オブジェクトの形状はmacro-substrate.mdで仕様化されています。
nested_class_templatesArray<Plugin::Macro::NestedClassTemplate>enum形状のブロックDSL(variant <Const>, <Type>)からのネストされたサブクラス放出;メソッドだけでなくクラスを生み出すマクロ基板ティア(ADR-36)。macro-substrate.mdで仕様化されています。
hkt_registrations, hkt_definitionsArray軽量HKTの型関数登録(ADR-20)。
additional_initializersArray<AdditionalInitializer>クラス(およびそのサブクラス)上のどのinitialize以外のdef形式メソッドがivar状態も確立するかを宣言する{ receiver_constraint:, methods: }ペアで、ScopeIndexerの書き込み前読み込みnil健全性ゲートに供給する(ADR-38)。

#validate_config(config)はエラー文字列の配列を返します;ローダーは空でない結果をLoadErrorに変換します。各拡張フィールドはManifest#initializeで独自のバリデーションを持ちます。

宣言されたconfigデフォルト — config_schema{ kind:, default: }(ADR-40)

Section titled “宣言されたconfigデフォルト — config_schemaの{ kind:, default: }(ADR-40)”

config_schemaの値は、元の素の種類Symbol/String"flag" => :boolean)でも、kind:(必須)とオプションのdefault:を担うHashでもよい(MAY)です:

config_schema: {
"dsl_method" => :string, # bare kind, no default
"state_method" => { kind: :string, default: "state" }, # kind + declared default
"events" => { kind: :array, default: [] }
}

2つの形式は1つの文法の純粋なスーパーセットです;エンジンは以下の契約を守らなければなりません(MUST):

  • 種類マップは形状が変わらないManifest#config_schema{ String => Symbol }(種類のみ)のままでなければならず(MUST)、そのため#validate_config#to_h#==#hashは、キーがどちらの形式を使ったかに影響されません。{ kind:, default: }エントリーは、素の種類とまったく同じようにkind:をこのマップへ貢献します。
  • Manifest#config_defaultsは、default:を宣言したキーのみを保持する凍結された{ String => value }マップを公開しなければなりません(MUST)。これはパブリックリーダー(パブリックAPIドリフト仕様 + RBS sigに固定)です。宣言されたデフォルトを持たないキーは現れません。
  • 宣言されたdefault:は、マニフェスト構築時にそのkind:に対してバリデートされなければなりません(MUST)#validate_configがユーザー値に適用するのと同じvalue_matches?チェック)。誤った型のデフォルト(kind: :stringのもとでのdefault: 5)は、使用時にサイレントに失敗するのではなく、ロード時にArgumentErrorを発生させなければなりません(MUST)。
  • Plugin::Base#configはユーザーconfigの下にデフォルトをマージします: #initializemanifest.config_defaults.merge(user_config)(凍結済み)を#configとして格納するので、それが設定するいかなるキーでもユーザーconfigが勝ちます。したがってプラグインはconfig.fetch("state_method")(またはconfig["state_method"])を読み、DEFAULT_*定数も2番目のfetch引数もなしに宣言されたデフォルトを得ます;プラグインがなお望む強制変換(.to_symArray(...))は読み取りサイトに留まります。マニフェストなしで宣言されたクラス(テストダブル)は、生のconfigを変更せずに保ちます。

この形式はconfigのエルゴノミクスのみです: ルールも型も変えないので、診断を導入することはできません。これはキャッシュセーフでもあります——デフォルトはプラグインのコード(そのversion)の一部であり、それはCache::Descriptor::PluginEntryキーがすでに捕捉しています;config_defaultsManifest#to_h/#==/#hashに参加しますが、キャッシュキーには決して参加しません。

Rigor::Plugin::TypeNodeResolver(ADR-13)

Section titled “Rigor::Plugin::TypeNodeResolver(ADR-13)”

RBS::Extendedの%a{rigor:v1:…}ペイロードに現れるカスタムな名前的/ジェネリック型語彙——RBS文法に組み込みのないTypeScriptユーティリティスタイルの型関数(Pick[T, K]Omit[T, K])をプラグインがRigorに教えられるようにするサーフェス——の、プラグイン提供のリゾルバのための基底クラスです。リゾルバはマニフェストのtype_node_resolvers:スロット(インスタンスのArray)を通じて登録されます。

サブクラスは1つのメソッドをオーバーライドします:

#resolve(node, scope) -> Rigor::Type::Base | nil
  • nodeはパーサが放出したRigor::TypeNode::IdentifierまたはRigor::TypeNode::Generic——チェーンが尋ねている名前的型またはジェネリック型のヘッドです。
  • scopeは、RBS::Extendedディレクティブパーサが下へ通す、付随するRigor::TypeNode::NameScope(リゾルバチェーン・クラスコンテキスト・型エイリアステーブルを担う)です。
  • メソッドは、ノードがこのリゾルバがカバーする語彙に一致するときRigor::Type::Baseを返さなければならず(MUST)、または次のリゾルバ(最終的には組み込み/RBSフォールバック)へフォールスルーするためにnilを返します。基底実装はnilを返すので、未実装のサブクラスは安全なno-opです。

エンジンは、ロードされたすべてのプラグインのリゾルバを——プラグイン登録順Registry#type_node_resolversがプラグインにわたってflat-mapする)で——単一のRigor::TypeNode::ResolverChainへ集約し、それは順番にそれらを参照して最初の非nilの答えを返します。チェーンはAnalysis::Runner.runごとに一度構成されます;どのプラグインもリゾルバを貢献しないとき、エンジンはショートサーキットする(NameScopeは構築されません)ので、パーサはリゾルバなしのデフォルトとビット単位で同じように振る舞います。リゾルバはステートレスで再入可能であるべきです(SHOULD)——チェーンは同じノードに対してリゾルバを複数回参照してもよい(MAY)です。実装済みのコンシューマーはrigor-typescript-utility-typesPick / Omit)です。

すべてのプラグインの#initialize#init#prepareに渡される凍結DIコンテナ:

サービス
reflectionRigor::Reflection(モジュール)。
typeRigor::Type::Combinator(モジュール)。
configurationRigor::Configuration(読み取り専用のプロジェクトconfig)。
cache_storeRigor::Cache::Storeまたはnil(スライス6がこれを通じてプラグイン側キャッシュプロデューサーを接続する)。
trust_policyRigor::Plugin::TrustPolicy(スライス2;plugin-trust.mdを参照)。
fact_storeRigor::Plugin::FactStore(ADR-9 / v0.1.1)— 実行ごとのクロスプラグインファクトストア;#prepareが公開し、#diagnostics_for_file / #flow_contribution_forが読む。

診断フォーマッタがプログレスチャンネルを持つようになったとき、ロガーサービスがこのリストに追加されます。

単一のAnalysis::Runner.runのためにロードされたプラグインの読み取り専用スナップショット。Rigor::Plugin::Loader.loadによって返され、Analysis::Runner#plugin_registryとして公開されます。

メソッド返り値
#plugins決定論的な順序でロードされたRigor::Plugin::Baseインスタンス。
#ids#pluginsと並行したマニフェストidのArray<String>
#find(id)idによるルックアップ;存在しない場合はnil
#load_errorsロード中に収集されたArray<Rigor::Plugin::LoadError>
#empty? / #any_load_errors?述語。

Registry::EMPTYはプラグインがロードされる前にランナーが使用するシングルトンの凍結空レジストリです。

プラグインエントリーが解決できない場合にローダー内で発生するパブリック例外。plugin_ref(問題のあるgem名またはプラグインid)とcause_class(該当する場合の基底例外クラス)を持ちます。ランナーはそれぞれをsource_family: :plugin_loaderrule: "load-error"を持つRigor::Analysis::Diagnosticに変換します。

内部サーフェス(パブリックではない)

Section titled “内部サーフェス(パブリックではない)”
  • Rigor::Plugin::Loader — ローダーは内部インフラです。プラグイン作成者はそのプライベートヘルパーをサブクラス化したり依存したりすべきではありません;パブリックエントリーポイントはLoader.load(configuration:, services:, requirer:)です。

.rigor.ymlのプラグインエントリー

Section titled “.rigor.ymlのプラグインエントリー”

設定のplugins:フィールドは短縮形と明示形の両方を受け入れます:

plugins:
- rigor-rails # bare gem name
- gem: rigor-rspec
id: rspec # only required when the gem registers > 1 plugin
config:
include_specs: true

Configurationはすべてのエントリーをその2つの形式のどちらかに正規化し、Configuration#pluginsを通じて公開します。

ローダーはユーザーが記述した順序で.rigor.ymlplugins:エントリーを処理します。複数の登録済みプラグインクラスに解決されるエントリー(1つのgemが1つ以上のプラグインを登録している場合)の場合、明示的なid:フィールドが曖昧さを解消します;なければローダーは推測するのではなくLoadErrorを発行します。エントリー間での重複するidはエラーであり、サイレントな重複排除ではありません。

障害の隔離(ADR-2 §「プラグイントラストとI/Oポリシー」に従う)

Section titled “障害の隔離(ADR-2 §「プラグイントラストとI/Oポリシー」に従う)”

ロードはすべてのプラグインエントリーを独立して処理します;1つのエントリーの失敗は他のエントリーを中断しません。各失敗は結果レジストリのLoadErrorとして収集され、次にAnalysis::Runner#runが以下を持つ:errorDiagnosticとして表面化します:

  • path: ".rigor.yml"
  • line: 1
  • column: 1
  • source_family: :plugin_loader
  • rule: "load-error"
  • message: LoadErrorのメッセージ(失敗の種類に応じてgemパス/登録/configスキーマ/#init例外)。

rigor checkは解析を続行します;正常にロードされたプラグインは後のv0.1.0スライスに引き続き参加します。

並行性と値オブジェクトの共有可能性(ADR-15)

Section titled “並行性と値オブジェクトの共有可能性(ADR-15)”

Rigorは並列ワーカーをまたいでファイルを解析します。出荷されているバックエンドはforkされた永続ワーカープール(ADR-15の修正;Ractorプールは延期されたターゲット)ですが、契約はそのターゲットが到達可能なままになるよう、より厳格なRactor境界に対して書かれています。したがってプラグインコードへの永続的な要求は次のとおりです:

  • マニフェストが担うすべての値オブジェクトは、構築時に深く凍結され、Ractor.shareable?でなければなりません(MUST)。これはManifest自体と、それが保持するすべてのネストされたキャリア(carrier)——Macro::*基板ティア(macro-substrate.md)・ProtocolContractAdditionalInitializerConsumption、および作成者が提供するいかなるTypeNodeResolver / source_rbs_synthesizer呼び出し可能オブジェクト(作成者は呼び出し可能オブジェクトが捕捉した状態のスレッドセーフティを所有する)——をカバーします。本仕様全体にわたるクラスごとの「#initialize後にRactor.shareable?がtrueを返す」という注記は、この単一のルールのインスタンスであり、別個の保証ではありません。
  • プラグインのインスタンスはワーカーごとに構築され、決して共有されません。境界を越えるのはRigor::Plugin::Blueprintキャリア(凍結済み、Ractor.shareable?)です: それはプラグインクラスの定数パスString(クラスオブジェクトではない——gemはいずれのワーカーがスポーンする前にメインRactor上でrequireされるので、各ワーカーはObject.const_get経由で同じ定数を解決する)に加えて、深くコピーされ共有可能にされたconfig Hashを保持します。各ワーカーは起動時に一度Blueprint#materialize(services:)を呼び——const_getklass.new(services:, config:)#init(services)で、Loader#instantiateを写し取る——、それからワーカーの生存期間にわたって自身のプラグインインスタンスとそれらの可変な実行ごとのアキュムレータを所有します。したがって可変なプラグイン状態が境界を越えることは決してなく、凍結されたBlueprintだけが越えます。
  • 文書化された例外: Environment::Reflection(パブリックなRigor::Reflectionファサードを支える内部の読み取り側キャリア)は凍結されていますがRactor.shareable?ではありません——その背後のテーブルが共有可能でないRBS::Locationオブジェクトを通すためです(ADR-15 WD6)。その結果、境界をまたいで共有されるのではなく、共有されたCache::Storeからワーカーごとに再構築されます。これはエンジン内部のキャリアであり、プラグインサーフェスではありません(public-api.mdを参照)。

各機能が着地した場所(歴史的スライスマップ)

Section titled “各機能が着地した場所(歴史的スライスマップ)”

v0.1.0のプラグイン契約は6つのスライスで出荷されました;以下はすべていまや整っており、それぞれ独自の仕様で文書化されています:

  • プラグイン貢献の発行FlowContributionバンドル、ケイパビリティ(capability)ロール、動的返却)。スタンドアロンの{Rigor::FlowContribution::Merger}(flow-contribution-merger.md)はスライス3で出荷;Rigor::Plugin::Base上の#flow_contribution_for(戻り値貢献ティア)はスライス4で出荷され、v0.1.1のクロスプラグイン作業(ADR-9)で拡張されました。
  • プラグイン診断来歴。スライス5はプラグインが発行した診断をplugin.<id>.<rule>プレフィックスを持つDiagnostic#source_familyを通じてルーティングします。
  • プラグイントラスト/I/Oポリシー執行。スライス2は宣言的な{Rigor::Plugin::TrustPolicy} + {Rigor::Plugin::IoBoundary}サーフェスを出荷しました;plugin-trust.mdを参照。
  • プラグイン側キャッシュプロデューサー。スライス6はPluginEntryディスクリプタを通じてプラグインにStore#fetch_or_computeを接続します;plugin-cache-producers.mdを参照。
  • クロスプラグインファクト + 事前パス#prepare(services) + services.fact_store + manifest(produces:/consumes:)がv0.1.1で出荷されました(ADR-9)。上記のManifestテーブルの拡張フィールド(signature_paths:open_receivers:protocol_contracts:source_rbs_synthesizer:、マクロ基板、HKT、additional_initializers:)は0.1.xサイクルを通じて堆積しました。
  • インターフェース分離ADR-37、Accepted)。
    • スライス1 / 1c / 1dnode_ruleクラスDSL + #node_rule_diagnostics(エンジン所有のウォーク) + node_file_context(2パスサポート) + NodeContext(レキシカル祖先) + #diagnostic / Diagnostic.from_node / .from_location作成者ヘルパー。これらは#diagnostics_for_fileをファイル全体の脱出弁として再定義します;同梱の診断発行プラグインはすべてnode_ruleへ移行されました——rigor-actionpack(4フェーズ、名前空間修飾に敏感)が最後でした。
    • スライス2#flow_contribution_forの、レシーバーゲートのdynamic_return + メソッドゲートのtype_specifier DSL(上記で文書化)への分割で、今や非推奨の太いフックと並んで参照されます;きれいに収まるコンシューマー(mangrove / minitest / rspec-matcher)は移行され、メソッドゲートの戻り値/動的レシーバーのコンシューマーは設計上、脱出弁に留まります。
    • スライス3FactProvider命名 + 機械可読なrigor plugins --capabilitiesカタログ(プラグインごと: node_ruleノード型、dynamic_returnレシーバー、type_specifierメソッド、生成/消費ファクト)。
  • 書き込み前読み込みnilゲートadditional_initializers:ADR-38)は、プラグインがScopeIndexerinitializeのみのivarシードゲートをフレームワークのライフサイクルメソッド(setupafter_initialize・DIセッター)へ拡張できるようにし、そこで設定され兄弟メソッドで読まれるivarがnilで拡幅されないようにします。
  • ターゲットライブラリの呼び出しADR-39、Accepted)。プラグインは、信頼されたターゲットライブラリの純粋で許可リストに載ったメソッドを直接呼び出せます(実際のActiveSupport::Inflectorの上のPlugin::Inflector;Railsファミリー + factorybotのコンシューマーは手書きの語形変化から移行)。これは選択可能な分離戦略(Plugin::Isolation: processデフォルト / none / ruby_box;上記で文書化)のもとで行われます。ボイラープレート計画の作成者ヘルパーBase.suggest(§0c)とインフレクターが、残りの手書き重複項目をクローズします。

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