コンテンツにスキップ

Session report — typing the plugin contract (2026-06-03)

ステータス:実装され、6つのコミット(9a4c22c0eed5371c)にわたってランディング済み。検証はすべてグリーン。このレポートは、出荷されたもの、それが実現するモデル、検証の証拠、そして未解決項目 ── フォローアップに先送りされた用語上の問題(protocol)を含む ── を統合します。

これらの判断の背後にある調査ログは姉妹ノート20260603-plugin-contract-self-typing-spike.mdです。規範的な判断はADR-43です。

このセッションは一つの問いから始まりました。RigorのプラグインファイルRigor::Plugin::Base契約(contract)に対して型付け/制約できるか。それによって、契約を誤用または誤実装したプラグインが、実行時ではなく機械的に捕捉されるようにできるか。それはさらに次のように鋭くなりました。(Steepではなく)rigor check自身がそれらの警告を発行できるか。

出荷されたもの ── 三層モデル

Section titled “出荷されたもの ── 三層モデル”

「プラグイン契約を型付けする」は、三つの独立した層へきれいに分解され、すべてランディングしました。

  1. 契約をRBSで記述する ── sig/rigor/plugin/base.rbsを4メソッドから作者向けサーフェスの全体へと完成させた(ADR-37のDSL群、オーバーライドフック、エンジンが実行するディスパッチャー、作成補助ヘルパー)。
  2. 構造的な仕様でオーバーライド適合性を強制する ── すべてのプラグインのフックオーバーライドがエンジンの呼び出しで呼び出し可能なままであることを検査する純RubyのMethod#parameters検査(引数/アリティのLiskov互換性、ADR-5)。
  3. rigor checkから契約の誤用を警告する ── ADR-43の許可リスト化されたRBS完全祖先解決により、プラグインが継承した契約呼び出し(manifest.…io_boundary.…)がBaseのRBSに対して解決されるようになるため、契約が宣言しないメソッドへの呼び出しはcall.undefined-methodを発火させる。make check-pluginsゲートがその能力をCIによる強制へと変える。

要を担う洞察はこれです。Rigorはこれを構造的にSteepより得意とする。Steepはプラグインのサブクラスのselfを素のRBSのBaseとして型付けするため、プラグイン自身のRBS化されていないヘルパーメソッドへのあらゆる呼び出しで偽陽性を出します(実測:rigor-deprecationsだけで3件のFP) ── プラグインツリー全体に対する厳格なSteepターゲットは、プラグインごとのRBSなしでは現実的ではありません。Rigorは各プラグインのdefをソースから読み取り、selfサブクラスとして型付けするため、自身のヘルパー呼び出しを正しく解決します。発火するのは真正な契約の誤用だけです。

コミット要約
9a4c22c01Plugin::BaseのRBSを完成(4 → サーフェス全体)。即座に実在の隙間が顕在化:IoBoundary#cache_descriptor#open_urlBaseから呼ばれていたのにio_boundary.rbsに欠落していた ── 宣言した。
7eccf0c72spec/integration/plugin_contract_conformance_spec.rb ── フックオーバーライドの呼び出し互換性ガード。37個すべてのプラグインでグリーン。注入したナローイングオーバーライドで失敗することを検証済み。
3be9b1df3ADR-43のドラフト(提案中)。
2a1832993ADR-43エンジン:scopeRbsDispatch.lookup_methodへ通す。許可リスト(ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"])による祖先解決。それを完成させると26件のFP(すべてManifest#id#protocol_contracts)が顕在化 → manifest.rbsの22個のリーダーを完成 → 正味のFPはゼロに。回帰テストを追加。
3235e2183ADR-43 WD6:make check-pluginsゲート(make verify+CI内)。既存の16件のツリー診断(Diagnosticシングルトンファクトリー+RBS内のAccessDeniedError < StandardErrorrigor-rspec内の1件のPrism::Node#blockフローナローイング)を解消。歯(実効性)を検証済み。
eed5371cdocsADR-43をdocs/internal-spec/に文書化(inference-engine.mdのディスパッチサーフェス+plugin.mdのBase自己検査ノート)。

エンジン変更(ADR-43)を一段落で

Section titled “エンジン変更(ADR-43)を一段落で”

RbsDispatch.lookup_methodscope引数と限定的な例外規則を得ました。レシーバークラスがRBSに知られていないRubyソースのサブクラスであり、その発見されたスーパークラスチェーン(Scope#superclass_of、ADR-24)が凍結された許可リストALLOWED_RBS_COMPLETE_ANCESTORSRigor::Plugin::Baseで種付けされている)上のクラスに到達するとき、そのメソッドはその祖先のRBSに対して解決されます。許可リストは偽陽性の境界です。包括的なバージョンであれば、class MyController < ActionController::Base部分的なgemのRBSが省略するメソッドを呼ぶたびに偽陽性を出すため、許可リストにない祖先はすべてDynamic[Top]フォールバックを保ちます。scopeはデフォルトでnilになるため、他のすべてのディスパッチ呼び出し元は変わりません。これはADR-26のopen_receivers:の双対です(抑制のために開く対、有効化のために閉じる)。

  • make checkrigor check lib):グリーン。
  • make check-pluginsrigor check plugins/*/lib examples/*/lib、141ファイル):exit 0。歯(実効性):注入したmanifest.bogusによりcall.undefined-methodで非ゼロ終了する。
  • Steep(libターゲット):グリーン。
  • テスト:inference(1852)+analysis/integration(1546)+integration/environment(1334)+rigor-rspecプラグイン(47) ── すべて失敗0件。
  • RuboCop:変更したRubyでクリーン。git diff --check:クリーン。
  • ADR-43 WD4 ── 許可リストのソーシング。許可リストはRigor::Plugin::Baseで種付けされたハードコードの定数です。マニフェスト宣言(ADR-37/ADR-40の宣言的な経路)を介して、サードパーティープラグイン自身のBase風クラスへそれを開放することは、消費者がそれを必要とするまで先送りします。
  • 用語:protocol(軸は決定済み、一部着手済み)。Rigorはこの言葉を四つのものにわたって多重定義していました。(A)RBSのinterface ── 構造的型付けの概念(Pythonのtyping.Protocolの対応物)。正しくinterfaceと名付けられ、衝突なし。(B)不活性なprotocols:マニフェストフィールド(宣言されているが、どこでも消費されていない ── 痕跡的なADR-2のメタデータ)。(C)ADR-28のprotocol_contracts:(パススコープな振る舞い的メソッド契約 ── 実在し消費される機能)。(D)ADR-37の説明文中の「extension protocols」(狭義のプラグインフック)。決定:軸はinterface=構造的型protocol contract=振る舞い的なメソッド要求契約(Smalltalk/Swiftの意味を保持)です。(B)は廃止Manifestサーフェスから削除 ── 振る舞いを伴わないまま、衝突を招きやすい素の「protocol」という言葉を担っていた)。(C)は正確な名のもとで保持。(A)/(D)は変更なし。ユーザー向け文書もランディング済みです。ハンドブック付録プロトコル、インターフェース、構造的型付けがこの区別を鋭く描き(横並びの表+「自分が欲しいのはどちら?」ガイド)、mypy付録から相互リンクされています。先送りのまま残っているのはADR-43 WD4(許可リストのソーシング)だけです。
  • 用語のフォローアップ:素の「interface」という言葉そのものが曖昧である。二つの独立したSonnetサブエージェントを中立的なプロンプト(セッションコンテキストなし)で走らせたところ、高い確信度で一致しました。すなわち、Rubyにはinterfaceキーワードがなく、Ruby人口はJava/PHP寄りに偏っているため、素の「interface」は名前的な(明示的にimplementsする)種類として読まれ、RBSが実際に実装している構造的なRBS/Go/Protocolの種類としては読まれない(RBSの適合は構造的である ── ruby/rbsのdocs/syntax.md、Steep、コミュニティのソースに対して検証済み。RBSのinclude _Fooは省略可能な便宜であって必須ではない)、ということです。決定:初出時に「interface」を限定することにし、「構造的インターフェース」/「RBSインターフェース」とします。実施済み:付録冒頭の一行コールアウト+ハンドブックREADMEの約束事に記した用語の約束。付録が正規の解説です。ハンドブックの残りに対する初出時の広範なスイープは行われていません(先送り ── 優先度低)。

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