ADR-35 — Override signature compatibility (Liskov signature rule)
ステータス: Accepted, 2026-05-29(スライス1〜4完了、スライス5は保留)。
メソッドのオーバーライド(override)を、それが継承する契約に対してチェックする新しい診断ファミリーを追加するという決定を記録する — クラス/モジュール階層をまたいで適用されたリスコフの置換原則(Liskov Substitution Principle、LSP)のシグネチャ規則(signature rule)である: パラメータは反変(contravariant)でなければならず(オーバーライドはパラメータを狭めて事前条件(precondition)を強化してはならない)、戻り値は共変(covariant)でなければならず(オーバーライドは戻り値を広げて事後条件(postcondition)を弱化してはならない)、可視性(visibility)は縮小してはならない。
これはSteepに着想を得た後続である: ADR-8はdef.return-type-mismatch(メソッド本体対その自身の宣言された戻り値)を出荷した。ADR-35は継承された契約の半分(オーバーライドの宣言されたシグネチャ対その親の宣言されたシグネチャ)を閉じる。両者は相補的であり — 本体対自身の契約、契約対継承された契約 — 合わせてSteepがRuby::MethodBodyTypeMismatchとそのオーバーライド互換性診断のもとでチェックするケースをカバーする。
ハンドブックの付録docs/handbook/appendix-liskov.mdの§「Rigorがチェックしないこと」は、クロス階層のオーバーライド互換性を最初の未出荷のLSP義務として名指ししている。このADRは、その静的に証明可能な部分を、厳格な偽陽性ゲーティングのもとで出荷するという決定である。
コンテキスト
Section titled “コンテキスト”Rubyは静的なオーバーライド規律を一切課さない。サブクラスはより狭いパラメータ、より広い戻り値、縮小された可視性、または異なるアリティ(arity)でメソッドをオーバーライドでき、インタプリタは実行時のパスが非互換にぶつかるまで文句を言わない。それぞれが、上位型に対して書かれた呼び出し元を壊す潜在的なLSP違反である:
class Repository # RBS: def find: (Integer | String) -> Record def find(id) = @store[id]end
class CachedRepository < Repository # RBS: def find: (Integer) -> (Record | nil) def find(id) = @cache[id] # 継承された契約に対する2つの違反: # - パラメーターが Integer|String -> Integer に狭められた(事前条件 # を強化): Repository を保持し String を渡す呼び出し元は、 # CachedRepository を渡されると壊れる。 # - 戻り値が Record -> Record|nil に広げられた(事後条件を弱化): # non-nil な Record を期待する呼び出し元が nil を受け取る。end今日のRigorは両者について沈黙している。def.return-type-mismatch(ADR-8)はこの戻り値の広げを捕捉しない。なぜなら、本体をCachedRepository#findの自身の宣言された-> (Record | nil)に対して比較し — 本体はそれを満たすからである。違反は親の-> Recordに対して相対的にのみ可視であり、エンジン内のどこもその比較を行わない。
ロバストネス原則(robustness principle、ADR-5)はすでに推論されたシグネチャをLSP的に正しい形(寛容なパラメータ = 反変、厳密な戻り値 = 共変)へ偏らせている — 付録の中心的な主張を参照。しかしADR-5は著作されたシグネチャを決して書き換えたりチェックしたりしない。それはRigorが著作するものだけを支配する。ユーザーが親とオーバーライドの両方のシグネチャを手書きしたとき、彼らはRigorが置換可能性について検証できる、そして検証すべき契約を宣言している。それがADR-35が埋めるギャップである。
なぜこれが今や実現可能か
Section titled “なぜこれが今や実現可能か”機構は存在する:
- 祖先解決。ADR-24 WD1が、プロジェクトクラスレジストリ + RBS祖先を通じて「囲むクラス + 祖先」に対するクロスファイル解決を確立した。同じ祖先ウォークが、オーバーライドが影にする親メソッドを見つける。
- 部分型付け /
accepts。三値の<:機構(relations-and-certainty.md)が、パラメータの反変性と戻り値の共変性を直接決める。 - Reflection経由のメソッドごとのシグネチャ。
def.return-type-mismatchはすでにRigor::Reflectionを通じてインスタンスとシングルトンの両方のメソッドシグネチャに到達する。ADR-35は1つではなく2つのメソッド(オーバーライド + 親)について同じサーフェスを必要とする。
メソッド定義が、レシーバークラスの祖先チェーン(上位クラスまたはincludeされたモジュール)を通じて到達可能なメソッドをオーバーライドし、かつオーバーライドと影にされたメソッドの両方が明示的に著作されたシグネチャ(手書きの.rbs、rbs-inline、またはバンドルされたRBS)を持つとき、エンジンは2つを比較し、証明可能な非互換に対して診断をemitする:
| チェック | ルール | 発火条件 |
|---|---|---|
| パラメータの反変性 | def.override-param-narrowed | オーバーライドのパラメータ型が、親のパラメータ型が受け入れるすべての値を受け入れない(parent_param.accepts(override_param)が:no)— 事前条件が強化されている |
| 戻り値の共変性 | def.override-return-widened | オーバーライドの戻り値型が、親の戻り値型に受け入れられない(parent_return.accepts(override_return)が:no)— 事後条件が弱化されている |
| 可視性 | def.override-visibility-reduced | オーバーライドが可視性を縮小する(public → protected / private、またはprotected → private) |
3つすべてがdefファミリーのルールであり(diagnostic-policy.mdの分類)、それぞれがADR-8 / ADR-34と同じようにseverity_profile:を通じて重大度をマップする:
| プロファイル | 3つのdef.override-*ルールのデフォルト |
|---|---|
strict | :error |
balanced | :warning |
lenient | 抑制 |
プロジェクトはseverity_overrides:でルール単位にオーバーライドし、# rigor:disable def.override-return-widened(など)で箇所を沈黙させる。
比較は三値の確実性を使う: :noのみが発火し、:maybeはv1の切り出しでは沈黙を保つ(def.return-type-mismatchの初回切り出しの規律に合わせる)。これが荷重を担う偽陽性の制御である — 両側明示シグネチャのゲートと組み合わさり、ルールは、ユーザーが両方の契約を宣言することでオプトインした、証明可能な契約違反に対してのみ発火する。
作業上の決定
Section titled “作業上の決定”WD1 — 両側明示シグネチャでゲートする
Section titled “WD1 — 両側明示シグネチャでゲートする”ルールは、オーバーライドと影にされた親メソッドのそれぞれが明示的に著作されたシグネチャを持つときにのみ発火する。どちらかの側が推論のみなら、ルールは沈黙する。
Why:偽陽性の規律は最上位のプロジェクト価値である(feedback_false_positive_discipline参照)。Rubyのオーバーライド形の変化は日常的に意図的である(テンプレートメソッドの精緻化、method_missing、DSLの再定義、define_method)。あらゆる形の変化をエラーとして扱えば、膨大な量の動作しているコードを怯えさせることになる。両方の契約が著作されていることを要求するのは、ユーザーがチェック可能な契約にオプトインしたというシグナルである — def.return-type-mismatchが使うのと同じスコープ付け(明示的なRBSのみ)である。それはまたルールをADR-5の領域から明確に切り離す: ADR-5は推論されたものに決して触れないことで著作された形のみを支配し、ADR-35は著作対著作をチェックして推論されたものには決して触れない。両者は決して重ならない。
How to apply:オーバーライド検出パスは、Reflectionに見えるシグネチャを持つメソッドだけを走り、祖先プローブは、それもReflectionに見えるシグネチャを持つ場合にのみ親を受け入れる。どちらかの側でのミスは沈黙へ短絡する。
可視性ルールのための例外(スライス1)。「明示的に著作されたシグネチャ」は、比較される契約がRBS宣言された型である型チェック(def.override-param-narrowed / def.override-return-widened)にとって正しいゲートである。def.override-visibility-reducedでは契約は可視性であり、Rubyのソースはそれを(private / protected / public修飾子で)どんなRBS型の著作とも独立に直接表現する。したがってゲートは「両方の可視性が静的に観測可能」に特殊化される: オーバーライドの可視性はソース発見テーブルから、祖先のものはプロジェクト発見の祖先チェーンからである。出荷されたスライス1は祖先側をユーザーソースのクラス / モジュールにスコープする。RBS既知の祖先(RBSはアクセシビリティをpublic/privateのみとしてモデル化し、protectedを持たない)は保留の後続である。偽陽性の規律は別の方法で保たれる — 両方のファクトが積極的に観測され、オーバーライドが厳密に縮小しなければならない — ので、どちらかの側で可視性が不明なら沈黙を保つ。
WD2 — 1つのdef.override-incompatibleではなく3つのルール
Section titled “WD2 — 1つのdef.override-incompatibleではなく3つのルール”1つのルールに潰すのではなく、違反の種類で分ける。
Why: ADR-34のWD3と、ルールの同一性が重大度ではなく問題の形を追うというプロジェクトのルールファミリー分類を鏡映する。外科的な無効化(戻り値チェックを沈黙させずに# rigor:disable def.override-visibility-reduced)、独立した重大度プロファイルのマッピング、違反固有のメッセージ(paramルールは問題のパラメータと広げを名指しでき、可視性ルールは継承された可視性を名指しできる)のすべてが、別個の同一性を欲する。可視性は特に、偽陽性がほぼゼロで高シグナルなチェックであり、一部のプロジェクトはbalancedでも:errorにしたいだろう(未解決の問題を参照)。
How to apply:診断ポリシーカタログのdefファミリーに3つすべてを登録する。ADR-8のファミリーテーブルが3つの新しいIDを得る。
WD3 — 反変なパラメータ、共変な戻り値 — 方向は対称でない
Section titled “WD3 — 反変なパラメータ、共変な戻り値 — 方向は対称でない”2つの型方向のチェックは反対を指しており、方向を正しく取ることがルールの正しさのすべてである:
- パラメータ:
override_param.accepts(parent_param)が:noのとき発火する。オーバーライドはパラメータを自由に広げてよい(より多く受け入れるのは反変的に安全)。狭めてはならない。方向は、オーバーライドのスロットが親の引数型を受け入れるか — 狭められたオーバーライドのスロットはより広い親の型を受け入れられないので、acceptsは:noを返す。(このADRの初期のドラフトはparent_param.accepts(override_param)と書いていたが、これは逆だ:A.accepts(B)が「BはAに渡せる」(B <: A)を意味するとき、親方向は安全な広げに対して発火し、狭めの違反に対して沈黙する。スライス1の実装がacceptsのセマンティクスを確認し —Inference::Acceptanceを参照 — スライス3はオーバーライド方向を使う。) - 戻り値:
parent_return.accepts(override_return)が:noのとき発火する。オーバーライドは戻り値を自由に狭めてよい(より具体的なものを返すのは共変的に安全)。広げてはならない。ここでは親方向が正しい: 広げられたオーバーライドの戻り値は、親の戻り値が期待される場所に渡せない。
Why:これはLSPのシグネチャ規則であり、ロバストネス原則(ADR-5)が推論されたシグネチャを偏らせるのと同じ非対称性である — 寛容な(広げ可能な)パラメータ、厳密な(狭め可能な)戻り値。付録がその収束を記録する。ADR-35は、その収束が手書きのシグネチャについても成り立つかのチェックである。
How to apply:既存のacceptsクエリを両方向で再利用する。新しい部分型付け機構は不要。self / instance戻り値型はディスパッチサイトごとに解決する(親の-> selfとオーバーライドの-> selfはどちらも共変的に安全であり、発火してはならない — 置換はaccepts比較の前に起こる)。
WD4 — オプショナルパラメータと広げのケースは安全
Section titled “WD4 — オプショナルパラメータと広げのケースは安全”オプショナルなパラメータの追加、必須パラメータの上位型またはケイパビリティ(capability)ロールへの広げ、追加のユニオンメンバーの受け入れは、すべて反変的に安全であり、発火してはならない。
Why:これらはLSP的に正しいオーバーライドの仕方であり、よくある良いRubyである。これらに発火すれば、ルールの意図を反転させ、まさにロバストネス原則が奨励するオーバーライドを罰することになる。
How to apply: WD3のaccepts方向は、型の広げに対してすでに正しい答えを出す。アリティの変化(必須の位置パラメータの追加/削除、キーワードの必須性)は別個の軸であり、v1ではスコープ外である — Rubyのオーバーライド・アリティのパターン(特にsuper、*args、オプショナル末尾の増加を伴うもの)は高い偽陽性リスクを抱えており、独自のスライスに値する。v1は一致するパラメータ位置の型のみを比較する。構造的なアリティの相違は、呼び出しサイトの既存のcall.wrong-arityパスへ落ちる。
WD5 — def.return-type-mismatch(ADR-8)との境界
Section titled “WD5 — def.return-type-mismatch(ADR-8)との境界”2つのルールは相補的で重ならない:
def.return-type-mismatch: メソッド本体の推論された戻り値対そのメソッド自身の宣言された戻り値。def.override-return-widened: メソッドの宣言された戻り値対親の宣言された戻り値。
Why:メソッドは、その宣言された戻り値が継承された契約に違反していても、自身の宣言された戻り値を満たしうる(CachedRepositoryの例)。どちらのルールも他方を包含しない。戻り値側の完全なカバレッジには両方が必要である。
How to apply:両者はdefノードに対して同じCheckRulesパスで走るが、異なる比較対象を参照する。本体が自身の戻り値に失敗するメソッド(def.return-type-mismatch)はそのルールが報告する。ADR-35は、宣言された戻り値が親のものを広げるならば追加で報告する。両方が病的なメソッドに発火することはありうる。それは二重報告のバグではなく正しい(異なる欠陥を記述している)。
WD6 — 祖先のスコープ: 上位クラスチェーン + includeされたモジュール、クロスファイル
Section titled “WD6 — 祖先のスコープ: 上位クラスチェーン + includeされたモジュール、クロスファイル”影にされたメソッドのプローブは、全祖先チェーン — 上位クラスのリンクとinclude/prependされたモジュール — をファイルをまたいで歩き、ADR-24 WD1のレジストリ + RBS祖先解決を再利用する。
Why: LSPの置換可能性は、継承されたメソッドと同じくモジュール供給のメソッドにも適用される(Comparableをincludeして<=>をオーバーライドするクラスは、Comparableの契約に対して置換可能である)。上位クラスに制限すると、より一般的なRubyの形であるmixinのケースを取り逃す。
How to apply:プローブは、同名のメソッドをシグネチャ付きで定義する最初の祖先で止まる。その祖先のシグネチャが比較対象である。最も近い定義する祖先がシグネチャを持たない場合、WD1の両側ゲートがチェックを沈黙させる(より遠いシグネチャ付き祖先へ飛び越えてはならない — 最も近い定義こそオーバーライドされている対象である)。
WD7 — :maybeはv1では沈黙
Section titled “WD7 — :maybeはv1では沈黙”:noの確実性のみが発火する。:maybe(例: 一方の側がDynamic[T]または未解決のジェネリックを持つ)は沈黙を保つ。
Why: def.return-type-mismatchの初回切り出しの規律(ADR-8 § 3)と同一。:maybeはまさに、発火が実行時には十分成り立ちうる契約に対する偽陽性のリスクを冒すケースである。
How to apply: accepts呼び出しはすでに三値を返す。:noのみで分岐する。
WD8 — 既存の診断サーフェスへの追加。プロファイル経由のデフォルトオフの逃げ道
Section titled “WD8 — 既存の診断サーフェスへの追加。プロファイル経由のデフォルトオフの逃げ道”3つのルールは新しい診断の同一性である。lenientでは抑制される。lenient上の既存プロジェクトに変化はない。balanced / strictでは顕在化する — ただし両側著作のオーバーライドに対してのみで、それは大半のコードベースでは小さなサーフェスである。
Why: ADR-34のWD6/WD8を鏡映する — 重大度プロファイルのマッピングは「新しく追加されたルールはどれくらい大声であるべきか」のプロジェクト全体のつまみである。lenient上の新しくオンボーディングされたプロジェクトは影響を受けない。strictにオプトインした(または意図的にRBSを著作した)プロジェクトはチェックを得る。
How to apply: ADR-8の重大度プロファイルテーブルに配線する。新しい機構は不要。
WD9 — 逃げ道: ジェネリクスが第一、本体ナローイングが第二、抑制は最後
Section titled “WD9 — 逃げ道: ジェネリクスが第一、本体ナローイングが第二、抑制は最後”オーバーライドをフラグする診断には、著者が問題ないと分かっているケースのための出口が必要である。PHPStanのジェネリクス設計(Mirtes、Generics in PHP using PHPDocs — 背景研究メモを参照)の教訓は、最良の逃げ道は抑制コメントではなく、コードを真にLSP安全にする、より豊かな型構成だということである。ADR-35は、好みの厳密な順序で3つの段階を提供する。
段階1 — ジェネリクス(原則的で、抑制しない逃げ道)。その記事のワークケースは、consume(SendMailMessage)を望むConsumer::consume(Message) — LSP違反に見えるパラメータの狭め — である。PHPStanの解決は@phpstan-ignoreではない。インターフェースをジェネリックにし(@template T of Message、メソッド@param T)、実装にそれをバインドさせる(@implements Consumer<SendMailMessage>)ことである。ネイティブのパラメータは広いMessageのまま(言語レベルで反変性が満たされる)であり、解析器はジェネリックなバインディングを通じて特殊化を理解する。「Barbara Liskovでさえこれに満足している。」
RBSの対応物はすでに存在する:
interface _Consumer[T < Message] def consume: (T) -> voidend
class SendMailConsumer include _Consumer[SendMailMessage] # def consume: (SendMailMessage) -> void — T = SendMailMessage で # インスタンス化されたインターフェースと整合する; 違反ではない。endオーバーライドの定義するクラスが祖先の型パラメータをバインドするとき(class Sub < Parent[Concrete]またはinclude _Iface[Concrete])、ADR-35の比較はaccepts比較(WD3)の前にそれらの型引数を親のシグネチャへ代入すべきである(SHOULD)。ADR-4のPhase 2dのtype_varsスレッディングを再利用し、「狭め」がインスタンス化された親の契約に一致するようにする。有界ジェネリクス([T < Bound])はすでにRBSのサーフェスにある(ハンドブック付録§「F有界多相とselfType」)。これは正当な特殊化を表現する推奨の方法であり、記事の解決の直接の対応物である。
スライス1/2の所見 — 代入は精度の機能であって、偽陽性安全性の要件ではない。このWDの初期の改訂は、ジェネリックインスタンス化を認識する比較を「正しさの要件」と呼んでいた。スライス2の実装は、それが強すぎることを示した: 未束縛の祖先型パラメータはDynamic[Top]へ翻訳され(ADR-4 — 未束縛のVariableはDynamic[Top]へ退化する)、Dynamic[Top]は漸進的な規則のもとであらゆるものを受け入れる(:yes / :maybe、決して:noではない)。だから型引数が代入されていないジェネリックな祖先の契約は、オーバーライドに対して単に沈黙へ退化する — これは偽陽性に安全な帰結である。したがって代入は偽陽性を防ぐのではなく(退化がすでにそれを行う)、精度を加える。ジェネリックなコンテキスト内部の本物の違反をなおも捕捉できるようにする(例: _Iface[String]をバインドしながらObjectを返すオーバーライド)。param/returnスライスはそれなしで偽陽性に安全に出荷される。代入は後続の精度向上である。
段階2 — 本体ナローイングの二層(日常のケース)。「ここでより狭い型が欲しい」状況の大半は、逃げ道をまったく必要としない。宣言されたパラメータを親の広い型のまま(LSP安全に)保ち、具体的な型は本体内部でoccurrence typingを通じて回復する。これはロバストネス原則の第2句 + ナローイングの組み合わせ(ADR-5)である: 広く宣言されたパラメータがLSP的に正しい契約であり、狭い本体型は無償で回復される。これはPHPStanのネイティブ広 + アノテーション狭の二層に対するRigorの対応物である — ただしRigorの「狭い層」は2つ目のアノテーションではなく推論された本体の型なので、ユーザーは余分に何も書かない。SendMailMessageだけを真に扱うが(Message)と宣言しreturn unless message.is_a?(SendMailMessage)でガードするメソッドは、LSP的に正しく、本体内部で完全に精密である。
段階3 — 抑制(最後の手段)。残余 — 解析器が安全だと証明できず、著者が断言するRubyのパターン(メタプログラミング、意図的な相違)— のために、箇所での# rigor:disable def.override-param-narrowed、またはファミリーレベルのseverity_overrides:エントリー。構造を持たないので、段階1〜2が適用されないときにのみ使う。
Why:この順序付けは、ADR-35が「警告をアノテーションで消し去る」に堕すのを防ぐ。ジェネリクスは特殊化のケースを沈黙させるのではなく安全にする。本体ナローイングは広い契約のケースを安全かつ精密にする。抑制は明示的に鈍い後退手段である。これはWD1の両側著作ゲートが相乗的である理由でもある: 両方のシグネチャを著作したユーザーはすでに「私は契約を宣言している」領域にいるので、親を有界ジェネリックとして書き直す(段階1)のは、外来の押し付けではなく自然な言語内の動きである。
How to apply:段階1は、WD3 / WD6の比較がacceptsの前に祖先のtype_argsを親のシグネチャへ代入することを要求する。未束縛 / 未解決の型パラメータは:maybeへ退化する(WD7に従い沈黙)。決して偽の:noにはならない。段階2〜3は新しい機構を必要としない。段階1と段階3の中間としての構造化されたRBS::Extendedのオプトアウトアノテーション(%a{rigor:v1:override-exempt}形)は保留である — 未解決の問題を参照。
実装スライス
Section titled “実装スライス”推奨される順序。各スライスは独立して出荷可能。
- 可視性ルール。 — ランディング済み(v0.1.x、2026-05-29)。
def.override-visibility-reduced— 最も偽陽性が低く、部分型付けを必要としないチェック。祖先メソッドプローブ(WD6)と可視性比較だけを必要とする。オーバーライド検出パスと新しいファミリー登録を出荷する。実装メモ: WD1の両側著作の型シグネチャゲートはこのルールでは例外とされる — 可視性はRBS型の著作とは独立にソース観測可能なので、ゲートは「両方の可視性が静的に決定可能」である(オーバーライドはソース発見の可視性テーブルから、祖先はプロジェクト発見のチェーンを通じて)。このスライスではユーザーソースの祖先にスコープされる。RBS既知の祖先(RBSはアクセシビリティをpublic/privateのみでモデル化し、protectedなし)は保留の後続。def.return-type-mismatchの:maybe沈黙 / 両側観測可能の規律は保たれる。 - 戻り値の共変性。 — ランディング済み(v0.1.x、2026-05-29)。
def.override-return-widened— スライス1の祖先プローブ(いまや共有のeach_project_ancestorBFS)+ 戻り値方向のacceptsクエリ(WD3)+:noのみの規律(WD7)を再利用する。WD1の本来のゲート(両側著作のRBS): オーバーライド側はdefined_on?(オーバーライドするクラスにRBSが宣言されている、単に継承されているのではない)でゲートされ、親側はそのメソッドをRBSが宣言する最も近いプロジェクト発見の祖先である。self/instance/untyped/未束縛ジェネリックの親戻り値はDynamic[Top]へ退化し沈黙を保つ(WD9の所見に従い偽陽性に安全)。このスライスではユーザーソースの祖先 + インスタンスメソッドにスコープされる。RBSのみの祖先とシングルトン(def self.)メソッドは後続。 - パラメータの反変性。 — ランディング済み(v0.1.x、2026-05-29)。
def.override-param-narrowed— 修正されたパラメータ方向override_param.accepts(parent_param) == :noでの位置ごとの型比較(WD3、WD4)。スライス2のゲート + 祖先プローブを再利用する。位置型のみ(アリティ / キーワードの必須性はスコープ外)、単一メソッド型のみ(オーバーロードのアームは曖昧)。untyped/ 未束縛ジェネリック / インターフェースの親パラメータはDynamic[Top]へ退化しスキップされる。リーチの所見:名前的部分型チェック(Inference::Acceptance#class_subtype_result)はロード済みのRubyクラス(とその祖先)を解決する。Rigorがロードできないユーザーのみのクラス階層は:maybeへ解決されるので、チェックはそれに対して沈黙を保つ(WD7に従い偽陽性に安全)。したがってリーチはコア / stdlib / ロード可能なgemの階層に及ぶ。アプリのみの階層に発火するには、プロジェクトRBSの祖先を認識する部分型パスが必要になる(後続)。WD9の段階1(ジェネリックインスタンス化を認識する比較)はWD9で述べた精度の後続であり — 未束縛ジェネリックがすでにDynamic[Top]へ退化するので、ここでの偽陽性安全性には不要である。 - Mastodonコーパスの偽陽性検証。 — 完了(v0.1.x、2026-05-29)。3つすべてを、強制した
strictプロファイルの下で、Mastodonのapp+lib全体(1219ファイル)に対して実行した。所見(完全な書き起こし:docs/notes/20260529-adr35-mastodon-fp-verification.md):def.override-return-widened/def.override-param-narrowed: 発火0件 — Mastodonはアプリのクラスに著作されたRBSを出荷しないので、両側著作のゲート(WD1)が決して満たされない。ルールはRBSなしで正しく不活性である。def.override-visibility-reduced: 160件発火 → 偽陽性修正後35件。160件は本物の偽陽性クラスタだった: メソッドの可視性がファイル単位でのみ追跡される一方、def-node / 祖先のインデックスはクロスファイルでシードされていたので、兄弟ファイルで宣言された祖先の可視性(Railsのコンサーンパターン —app/controllers/concerns/内のprivateヘルパー)が不明として返り、nil → :publicフォールバックが「public」な親を捏造 → 偽の縮小。修正: メソッドの可視性をクロスファイルでシードし(discovered_def_index_for_paths+merge_project_method_indexes+ ランナーのseed_project_scope)、不明から:publicを決して捏造しない(沈黙へ退避)。残りの35件は真の縮小である(pundit_userがAuthorizationコンサーンで純粋にpublic、pagination_*がApi::Paginationで純粋にprotected、コントローラーでprivateにオーバーライド)—strictの下でのみ顕在化する。Mastodonの実際のlenient設定が顕在化させるのはゼロ件。- キャリブレーションの結果: 現在のマッピング(
lenient → off、balanced → :warning、strict → :error)を維持する。balanced → :errorへの昇格はしない — 残余の真陽性はRailsでスタイル的によくあり、balancedなプロジェクトをエラーにするには値しない。
- (保留)親が著作 + 子が推論の共変性。共変性のみの拡張: 親の戻り値が著作されているが子が推論のみのとき、子の推論された戻り値を親の宣言された戻り値に対してチェックする。より価値が高く、より偽陽性が高い(推論された本体の精度)。スライス4のデータが推論された戻り値の精度が十分良いことを示すことにゲートされる。コミットなし。
検討された代替案
Section titled “検討された代替案”- 1つの
def.override-incompatibleルール。WD2により却下 — 外科的であるべきものを統合し、3つの偽陽性プロファイルにまたがって1つの重大度を強制する(可視性はパラメータの狭めよりはるかに安全)。 - 推論されたシグネチャもチェックする(WD1ゲートを落とす)。v1では却下 — 推論されたシグネチャはロバストネスの広げ / 狭めの対象であり、メタプログラミングの密なRubyでの偽陽性サーフェスは大きい。スライス5が最もリスクの低い隅(親が著作した共変性)をコーパスデータの背後で再訪する。
- 戻り値の広げを
def.return-type-mismatchに畳み込む。WD5により却下 — 比較対象が異なる(継承された契約対自身の契約)。メソッドは一方をパスし他方に失敗しうる。 - コアのルールではなくプラグイン / オプトインの設定キーにする。却下 — 著作されたクラス階層の置換可能性はフレームワーク固有の関心事ではなく、コアの型システムの性質である(cf. ADR-28。これはディレクトリをプロトコルにバインドするのでフレームワークスコープである)。両側著作のゲート + 重大度プロファイルが十分なスコープ付けである。別個の設定軸はプロファイル機構を重複させるだろう。
- LSPの例外規則(オーバーライドに新しい例外を入れない)を強制する。却下 — RBSには比較対象となる広く使われた
raises句がなく、Rubyのrescueイディオムは推論されたraise集合を高偽陽性にする。付録に非目標として記録。ここでは開かない。 - v1でアリティ互換性を強制する。WD4により却下 —
super/*args/ オプショナル末尾のパターンで高偽陽性。需要が現れれば独自のスライスへ保留。
未解決の問題
Section titled “未解決の問題”def.override-visibility-reducedはlenientでも:warningを下限とすべきか? これは偽陽性がほぼゼロで、本物の置換可能性の破れである(いまやprivateになったメソッドを呼ぶ呼び出し元はNoMethodErrorにぶつかる)。反論: サブクラスでのprivateは、ときに意図的な「ここでは内部」のシグナルである。決定はスライス1のドッグフード + スライス4のコーパスデータへ保留。protectedの扱い。protectedの置換可能性はpublic/privateよりも微妙である(呼び出しのレシーバー関係に依存する)。v1はpublic → protectedを縮小として扱う。protected → public(広げ、安全)や同レベルのprotectedオーバーライドが特別な扱いを要するかは保留。- ジェネリック / パラメータ化されたオーバーライド。親の
-> Array[Object]と子の-> Array[String]: 不変な読み取りでの要素共変性は安全だが、Arrayは不変である(ADR-1 / 付録の分散の節)。v1はジェネリクスについて既存のacceptsの答え(宣言された分散を尊重する)に頼る。それが有用であるには:maybeが多すぎるかどうかはスライス4の測定事項。 Data.define/Structの生成メソッドとの相互作用。生成されたアクセサは(著作ではなく)推論されたシグネチャを持つので、WD1がそれらを沈黙させる。将来のスライスがそれらのシグネチャを精密に推論するなら、生成されたアクセサのオーバーライドがスコープに入るべきかを再訪する。- メッセージはロバストネス原則をほのめかすべきか?
def.override-param-narrowedのメッセージは、ADR-5への参照とともに「パラメータを広げるか、親の型を受け入れよ」を指し示せるだろう。コストはゼロ。腐るかもしれない。実装へ保留。メッセージは特殊化のケースについてはWD9の段階1(「親をジェネリックにして型パラメータをバインドせよ」)を等しくほのめかせる。 - 構造化された
%a{rigor:v1:override-exempt}アノテーション(WD9の中間段階)? 段階1のジェネリクスと段階3の素の# rigor:disableの間に、専用のRBS::Extendedアノテーションがあれば、grep可能で、意図を明らかにし、監査可能だろう(「このオーバーライドは意図的に相違し、著者が保証する」と言うのであって、「この行を隠す」ではない)。コスト: rbs-extended文法の新しいアノテーショントークンと、それ自身の保守・文書化サーフェス。暫定的な判断はそれを出荷しないこと — ジェネリクス(段階1)が原則的なケースをカバーし、# rigor:disable(段階3)が残余をカバーするので、中間段階は、ジェネリックで表現できず一度きりの抑制でもない繰り返しのパターンをスライス4のコーパスデータが示す場合にのみ、その働きに見合う。そのデータで判断する。
背景研究メモ
Section titled “背景研究メモ”このADRは、ハンドブック付録docs/handbook/appendix-liskov.mdを生んだ2026-05-29の会話に促されたものである。同付録はLSPの義務をRigorのサーフェスへ写し取り、クロス階層のオーバーライド互換性を最も価値の高い未出荷の義務として名指しする。付録の中心的な主張 — ロバストネス原則がRuby採用のエルゴノミクスから到達したLSPのシグネチャ規則である — こそ、このチェックが外来の押し付けではなく自然な適合である理由である: それは手書きのシグネチャについて、ADR-5がすでに推論されたシグネチャを偏らせるのと同じ性質を検証する。
Ondřej Mirtes, Generics in PHP using PHPDocs(https://medium.com/@ondrejmirtes/generics-in-php-using-phpdocs-14e7301953)は、WD9の直接の駆動因である。その鍵となる教訓 — LSP違反に見えるパラメータの狭めは、警告を抑制するのではなく親をジェネリックにする(@template T of Message + @implements Consumer<SendMailMessage>)ことで解決するのが最良である(「Barbara Liskovでさえこれに満足している」)— こそ、ADR-35が採用する段階1の逃げ道そのものである。RBSの有界ジェネリックのサーフェス(interface _Consumer[T < Message])はすでに同じ構成を表現するので、作業は新しいアノテーションを発明することではなく、ADR-35の比較をジェネリックインスタンス化を認識するようにすること(WD9「How to apply」)である。記事はまた、WD9の段階2がRigorの広く宣言されたパラメータ + 推論された狭い本体の組み合わせで再現する二層(ネイティブ広パラメータ + アノテーション狭精度)もモデル化している。
SteepはRBSに対して類似のオーバーライド互換性チェックを行う。ADR-8(「Steepに着想を得た改善」)はこのADRが続ける系譜である。ADR-28(パススコープのプロトコル契約)は置換可能性の軸での姉妹である — ディレクトリにバインドされたプロトコルのための構造的置換可能性であり、ADR-35は継承された契約のための名前的置換可能性である。
- 2026-05-29 — 初回提案。Liskovハンドブック付録の著作に続くユーザーの質問(「RigorにLSP観点での機能追加の余地はありそう?」)に引き起こされた。意図的に狭くスコープした: 証明可能な違反のみ、両側著作のシグネチャのみ、型方向 + 可視性のみ(アリティと例外規則はスコープ外)、
:maybeは沈黙 — そうしてルールは、診断を追加するすべての変更をゲートする偽陽性の規律を尊重する。 - 2026-05-29 — PHPDocスタイルの逃げ道の提供についてのユーザーの質問に続き、MirtesのGenerics in PHP using PHPDocsを引用してWD9(逃げ道)を追加。段階化された設計(ジェネリクス第一 → 本体ナローイング → 抑制)と、段階1のジェネリックインスタンス化を認識する比較がparam/returnチェックの正しさの要件であり単なるオプトアウトではない — 正当な
Consumer[T < Message]の特殊化が決して偽発火してはならない — という決定を記録する。構造化された%a{rigor:v1:override-exempt}アノテーションは未解決の問題として提示され、暫定的に見送られる。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.