rigortype 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -14
  3. data/docs/handbook/01-getting-started.md +311 -0
  4. data/docs/handbook/02-everyday-types.md +337 -0
  5. data/docs/handbook/03-narrowing.md +359 -0
  6. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  7. data/docs/handbook/05-methods-and-blocks.md +339 -0
  8. data/docs/handbook/06-classes.md +305 -0
  9. data/docs/handbook/07-rbs-and-extended.md +427 -0
  10. data/docs/handbook/08-understanding-errors.md +373 -0
  11. data/docs/handbook/09-plugins.md +241 -0
  12. data/docs/handbook/10-sorbet.md +347 -0
  13. data/docs/handbook/11-sig-gen.md +312 -0
  14. data/docs/handbook/12-lightweight-hkt.md +333 -0
  15. data/docs/handbook/README.md +275 -0
  16. data/docs/handbook/appendix-elixir.md +370 -0
  17. data/docs/handbook/appendix-go.md +399 -0
  18. data/docs/handbook/appendix-java-csharp.md +470 -0
  19. data/docs/handbook/appendix-liskov.md +580 -0
  20. data/docs/handbook/appendix-mypy.md +370 -0
  21. data/docs/handbook/appendix-phpstan.md +338 -0
  22. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  23. data/docs/handbook/appendix-rust.md +446 -0
  24. data/docs/handbook/appendix-steep.md +336 -0
  25. data/docs/handbook/appendix-type-theory.md +1662 -0
  26. data/docs/handbook/appendix-typeprof.md +416 -0
  27. data/docs/handbook/appendix-typescript.md +332 -0
  28. data/docs/install.md +189 -0
  29. data/docs/llms.txt +72 -0
  30. data/docs/manual/01-installation.md +342 -0
  31. data/docs/manual/02-cli-reference.md +557 -0
  32. data/docs/manual/03-configuration.md +152 -0
  33. data/docs/manual/04-diagnostics.md +206 -0
  34. data/docs/manual/05-inspecting-types.md +109 -0
  35. data/docs/manual/06-baseline.md +104 -0
  36. data/docs/manual/07-plugins.md +92 -0
  37. data/docs/manual/08-skills.md +143 -0
  38. data/docs/manual/09-editor-integration.md +245 -0
  39. data/docs/manual/10-mcp-server.md +532 -0
  40. data/docs/manual/11-ci.md +274 -0
  41. data/docs/manual/12-caching.md +116 -0
  42. data/docs/manual/13-troubleshooting.md +120 -0
  43. data/docs/manual/14-rails-quickstart.md +332 -0
  44. data/docs/manual/15-type-protection-coverage.md +204 -0
  45. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  46. data/docs/manual/17-driving-improvement.md +160 -0
  47. data/docs/manual/README.md +87 -0
  48. data/docs/manual/ci-templates/README.md +58 -0
  49. data/docs/manual/plugins/README.md +86 -0
  50. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  51. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  52. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  53. data/docs/manual/plugins/rigor-activejob.md +58 -0
  54. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  55. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  56. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  57. data/docs/manual/plugins/rigor-devise.md +70 -0
  58. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  59. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  60. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  61. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  62. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  63. data/docs/manual/plugins/rigor-graphql.md +89 -0
  64. data/docs/manual/plugins/rigor-hanami.md +83 -0
  65. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  66. data/docs/manual/plugins/rigor-minitest.md +86 -0
  67. data/docs/manual/plugins/rigor-pundit.md +72 -0
  68. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  69. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  70. data/docs/manual/plugins/rigor-rails.md +44 -0
  71. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  72. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  73. data/docs/manual/plugins/rigor-rspec.md +86 -0
  74. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  75. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  76. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  77. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  78. data/docs/manual/plugins/rigor-statesman.md +75 -0
  79. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  80. data/exe/rigor +1 -1
  81. data/lib/rigor/analysis/incremental_session.rb +4 -2
  82. data/lib/rigor/analysis/run_stats.rb +13 -1
  83. data/lib/rigor/analysis/runner.rb +54 -12
  84. data/lib/rigor/cli/check_command.rb +1 -1
  85. data/lib/rigor/cli/docs_command.rb +248 -0
  86. data/lib/rigor/cli/skill_command.rb +103 -41
  87. data/lib/rigor/cli/skill_describe.rb +346 -0
  88. data/lib/rigor/cli.rb +25 -3
  89. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  91. data/lib/rigor/inference/scope_indexer.rb +87 -89
  92. data/lib/rigor/plugin/isolation.rb +5 -5
  93. data/lib/rigor/plugin/loader.rb +4 -2
  94. data/lib/rigor/version.rb +1 -1
  95. data/skills/rigor-ask/SKILL.md +172 -0
  96. data/skills/rigor-doctor/SKILL.md +87 -0
  97. data/skills/rigor-editor-setup/SKILL.md +114 -0
  98. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  99. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  100. data/skills/rigor-next-steps/SKILL.md +113 -0
  101. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  102. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  103. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  104. data/skills/rigor-upgrade/SKILL.md +79 -0
  105. metadata +90 -1
@@ -0,0 +1,580 @@
1
+ # Appendix — The Liskov Substitution Principle
2
+
3
+ A bridge between the **Liskov Substitution Principle (LSP)** — the
4
+ classical statement of what it *means* for one type to substitute
5
+ for another — and Rigor's design. The thesis of this page is small,
6
+ and once seen it is hard to un-see:
7
+
8
+ > Rigor's robustness principle (Postel's law for types, strict on
9
+ > returns, lenient on parameters) **is** the LSP signature rule,
10
+ > reached from the opposite direction. LSP derives the
11
+ > wider-parameters / narrower-returns asymmetry *top-down* from
12
+ > "substitutability must preserve correctness." Rigor derives the
13
+ > identical asymmetry *bottom-up* from "minimise call-site friction
14
+ > and maximise downstream precision so Ruby programmers will
15
+ > actually run a type checker." Two motivations, one rule.
16
+
17
+ That convergence matters because it answers a worry a Ruby
18
+ programmer reasonably has about any type checker: *will this thing
19
+ fight my idioms?* It will not. The discipline Rigor applies is the
20
+ same discipline the Ruby community already teaches under the "L" of
21
+ SOLID, and the same one a careful duck-typer follows by instinct.
22
+
23
+ > **A note on the acronym.** Everywhere else in this repository
24
+ > "LSP" means the **Language Server Protocol**
25
+ > ([ADR-19](../adr/19-language-server-packaging.md), `rigor lsp`).
26
+ > On *this* page, and only this page, "LSP" means the **Liskov
27
+ > Substitution Principle**. The collision is unfortunate and
28
+ > entirely conventional; the rest of the corpus keeps the
29
+ > language-server meaning.
30
+
31
+ This page is descriptive, not normative. When the language here
32
+ disagrees with the [type
33
+ specification](../type-specification/README.md), the spec binds.
34
+
35
+ ## Five-second pitch
36
+
37
+ | LSP obligation | Ruby idiom that already honours it | Rigor surface |
38
+ | --- | --- | --- |
39
+ | A subtype may be substituted wherever the supertype is expected | Duck typing — "if it responds to the messages I send, I can use it" | Nominal-first typing + structural facets + capability roles |
40
+ | **Signature rule** — parameters contravariant, returns covariant | "Accept the widest thing you can use; return the most specific thing you can promise" | The **robustness principle** ([ADR-5](../adr/5-robustness-principle.md)) — lenient parameters (clause 2), strict returns (clause 1) |
41
+ | **Preconditions may not be strengthened** in a subtype | An override should not reject inputs the parent accepted | Clause 2: parameters widened to the largest correctness-preserving carrier |
42
+ | **Postconditions may not be weakened** in a subtype | An override should not return something less specific than the parent promised | Clause 1: returns tightened to the smallest correctness-preserving carrier; `def.return-type-mismatch` |
43
+ | **Invariants must be preserved**; **history constraint** | Don't add state transitions that break the parent's invariants; respect `freeze` | Mutation-effect model + fact stability + `frozen?` narrowing (partial; see § "Invariants") |
44
+ | Exception compatibility — no surprising new exceptions | Rescue what the contract documents | Internal exception/non-local-exit effect (not checked across overrides; see § "What Rigor does NOT check") |
45
+
46
+ ## What LSP actually says
47
+
48
+ Barbara Liskov's 1987 OOPSLA keynote (*Data Abstraction and
49
+ Hierarchy*) stated the intuition:
50
+
51
+ > What is wanted here is something like the following substitution
52
+ > property: if for each object `o1` of type `S` there is an object
53
+ > `o2` of type `T` such that for all programs `P` defined in terms
54
+ > of `T`, the behavior of `P` is unchanged when `o1` is substituted
55
+ > for `o2`, then `S` is a subtype of `T`.
56
+
57
+ **Liskov & Wing 1994** (*A Behavioral Notion of Subtyping*, ACM
58
+ TOPLAS) made it precise. Behavioral subtyping `S <: T` requires two
59
+ groups of conditions:
60
+
61
+ ### The signature rule
62
+
63
+ For every method the subtype shares with the supertype:
64
+
65
+ - **Contravariant parameters** — the subtype's method must accept
66
+ *at least* every argument type the supertype's accepted (it may
67
+ accept more).
68
+ - **Covariant return** — the subtype's method must return *at most*
69
+ the supertype's return type (it may return something more
70
+ specific).
71
+ - **Exception rule** — the subtype's method may raise *fewer*
72
+ exception types than the supertype's, never new ones.
73
+
74
+ ### The behavioral (methods) rule
75
+
76
+ Beyond signatures, the *behaviour* must be compatible — this is the
77
+ part most signature-only type checkers do not encode:
78
+
79
+ - **Preconditions may not be strengthened.** A subtype's method may
80
+ require *no more* of its caller than the supertype's did.
81
+ - **Postconditions may not be weakened.** A subtype's method must
82
+ guarantee *at least* what the supertype's did.
83
+ - **Invariants must be preserved.** Properties true of every
84
+ supertype instance stay true of every subtype instance.
85
+ - **History constraint.** The subtype may not permit state changes
86
+ that the supertype's specification forbids over an object's
87
+ lifetime (the rule that rules out, e.g., a mutable `Point`
88
+ subtyping an immutable one).
89
+
90
+ The behavioral rule is the **Design-by-Contract** inheritance
91
+ discipline (Bertrand Meyer, Eiffel) stated as a subtyping law:
92
+ `require` clauses weaken down the hierarchy, `ensure` clauses
93
+ strengthen, `invariant` clauses accumulate.
94
+
95
+ ## LSP is about behaviour, not static types
96
+
97
+ A claim surfaces occasionally: *"Ruby is dynamically typed, so LSP
98
+ is a static-typing rule that does not strictly apply."* This gets
99
+ the principle backwards.
100
+
101
+ LSP is not a rule *about* type checkers. It is a rule about
102
+ **observable behaviour under substitution** — Liskov's own framing
103
+ quantifies over "all programs `P` defined in terms of `T`" and asks
104
+ that their *behaviour* be unchanged when an `S` is substituted. The
105
+ 1994 paper's title is deliberate: *A **Behavioral** Notion of
106
+ Subtyping*. The signature rule is only the type-system-shaped half;
107
+ the load-bearing half is the behavioral rule (preconditions,
108
+ postconditions, invariants, history), and those are statements about
109
+ what code *does at runtime*, not about what a compiler accepts.
110
+
111
+ That behavioral focus is exactly what makes LSP **more** applicable
112
+ to a language like Ruby, not less:
113
+
114
+ - In a nominally-typed language a compiler enforces the signature
115
+ half for you, so LSP feels like "a thing the type checker already
116
+ did." The interesting, un-automated part is the behavioral half.
117
+ - In Ruby there is no compiler enforcing *either* half — so the
118
+ *entire* principle, signature and behaviour both, is a discipline
119
+ the programmer carries. "Subtype" in Ruby is defined by
120
+ substitutability of *messages and behaviour* (duck typing), which
121
+ is precisely the relation LSP is stated over. A language that is
122
+ hard to formalise is the case where a behavioural substitutability
123
+ discipline earns the most: it is the only safety net available
124
+ when a static one is not.
125
+
126
+ So "Ruby is not statically typed" is an argument *for* taking LSP
127
+ seriously, not against. LSP is the tool that lets you reason about
128
+ the *behavioural* safety of substitution in a language whose shape a
129
+ classical type system cannot fully capture. Every working
130
+ duck-typed Ruby program already depends on something LSP-shaped
131
+ holding — the caller assumes the object it received behaves
132
+ compatibly with the contract it was written against.
133
+
134
+ ### Rigor's relationship to this
135
+
136
+ Rigor does **not** provide a direct *static guarantee* that a
137
+ program obeys LSP. It does not prove behavioral subtyping, does not
138
+ check cross-hierarchy override compatibility, and does not verify
139
+ pre/postcondition contracts (§ "What Rigor does NOT check"). On that
140
+ narrow reading, "Rigor is not an LSP checker" is true.
141
+
142
+ But the *ideas* have substantial common ground, and that is the
143
+ point of this whole appendix. Rigor's design is steered by the same
144
+ behavioural-substitutability instinct LSP formalises:
145
+
146
+ - The robustness principle infers the LSP-correct signature shape
147
+ (wide parameters, narrow returns) by default (§ next).
148
+ - Capability roles model substitutability the way duck typing does —
149
+ by behaviour (messages answered), not by nominal identity.
150
+ - The mutation-effect and fact-stability model refuses to keep
151
+ trusting a property a state change could have broken — the
152
+ history-constraint instinct, applied locally.
153
+
154
+ Rigor is therefore best read as *tooling that shares LSP's
155
+ behavioural worldview and mechanises the parts of it that are
156
+ statically provable in Ruby*, while leaving the rest as the
157
+ discipline it has always been. The principle and the tool are
158
+ aligned in spirit even where the tool stops short of a proof.
159
+
160
+ ## The signature rule is Rigor's robustness principle
161
+
162
+ This is the heart of the page. Lay the two rules side by side:
163
+
164
+ | Position | LSP signature rule | Rigor robustness principle |
165
+ | --- | --- | --- |
166
+ | Parameters | **Contravariant** — accept at least as much as the supertype | **Lenient** (clause 2) — widest correctness-preserving carrier |
167
+ | Returns | **Covariant** — return at most what the supertype promised | **Strict** (clause 1) — smallest correctness-preserving carrier |
168
+
169
+ They are the same asymmetry. What differs is *why* each system
170
+ reaches for it.
171
+
172
+ **LSP's derivation is top-down.** Start from "an `S` must be usable
173
+ anywhere a `T` is expected." A caller written against `T` may pass
174
+ any `T`-typed argument, so `S`'s method must accept all of them —
175
+ parameters can only widen. A caller written against `T` will use
176
+ the result as a `T`, so `S`'s method must return something every
177
+ `T`-context can consume — returns can only narrow. The asymmetry
178
+ *falls out* of substitutability; it is not chosen.
179
+
180
+ **Rigor's derivation is bottom-up.** ADR-5 starts from a Ruby-
181
+ adoption problem, not a substitutability proof
182
+ ([`robustness-principle.md`](../type-specification/robustness-principle.md)):
183
+
184
+ - An over-strict **parameter** type forces callers to paste
185
+ defensive coercions (`x.to_s`, `x || ""`, `Array(x)`) at every
186
+ call site. The workarounds become load-bearing and hide intent.
187
+ So Rigor widens parameters to "anything the body can actually
188
+ use" — capability roles, structural interfaces, supertypes.
189
+ - An over-wide **return** type discards precision every downstream
190
+ consumer needs. `Array#size` typed `Integer` rather than
191
+ `non-negative-int` collapses every later `if size > 0`. So Rigor
192
+ tightens returns to "the most specific thing the body provably
193
+ produces."
194
+
195
+ Two completely different starting points (a 1994 substitutability
196
+ theorem and a 2020s "please don't make Ruby programmers write
197
+ `.to_s` everywhere" ergonomics concern) land on the
198
+ *identical* rule. That convergence is the reassurance: Rigor's
199
+ pragmatic, Ruby-first defaults are not a departure from classical
200
+ OO type discipline, they re-derive it.
201
+
202
+ ```ruby
203
+ # Clause 2 / contravariant parameters: the body uses only #upcase,
204
+ # so the parameter widens to "anything that responds to upcase",
205
+ # not the nominal String. An override that accepted String only
206
+ # would be *strengthening* the precondition — an LSP violation,
207
+ # and exactly the over-strict shape clause 2 steers away from.
208
+ def shout(thing)
209
+ thing.upcase
210
+ end
211
+
212
+ # Clause 1 / covariant returns: the body provably returns the
213
+ # receiver, so #dup returns `self` (Array, not the widened Object).
214
+ # Returning Object would *weaken* the postcondition.
215
+ copy = [1, 2, 3].dup
216
+ assert_type("Array", copy)
217
+ ```
218
+
219
+ The connection to the type-theory appendix's **gradual guarantee**
220
+ ([§ "Blame, the gradual guarantee, and trust
221
+ boundaries"](appendix-type-theory.md)) is direct: a system that
222
+ honours the signature rule by construction also satisfies "adding
223
+ an annotation never breaks a previously-passing call site," because
224
+ a correctly-widened parameter and a correctly-narrowed return are
225
+ precisely the annotations that preserve substitutability.
226
+
227
+ ## Variance and the signature rule
228
+
229
+ The signature rule *is* a statement about variance, and Rigor
230
+ inherits RBS's variance vocabulary
231
+ ([§ "Variance"](appendix-type-theory.md#variance) in the
232
+ type-theory appendix):
233
+
234
+ - **Covariant (`out T`)** — producer position; the return side of
235
+ the signature rule.
236
+ - **Contravariant (`in T`)** — consumer position; the parameter
237
+ side of the signature rule.
238
+ - **Invariant** — both at once; mutable storage.
239
+
240
+ Ruby's mutable containers (`Array`, `Hash`, `Set`) are **invariant**
241
+ in their element type, and this is the canonical place LSP and a
242
+ naive intuition collide. The "obvious" idea that `Array[Integer]`
243
+ should substitute for `Array[Numeric]` is *unsound* the moment a
244
+ caller writes `arr.push(1.5)` — the classic
245
+ covariant-arrays-are-broken cautionary tale (Java's `ArrayStoreException`).
246
+ RBS declares these containers invariant; Rigor honours the
247
+ declaration and so never offers the unsound substitution. The
248
+ signature rule, applied to the *mutating* methods (`push` takes the
249
+ element in a contravariant position; `[]` returns it in a covariant
250
+ one), forces invariance, and Rigor gets this for free by trusting
251
+ RBS rather than re-deriving variance.
252
+
253
+ **Self types** are the signature rule's covariant-return case made
254
+ load-bearing for OO Ruby. RBS's `self` keyword (`def dup: () ->
255
+ self`) means "I return *my own* class," so a subtype's inherited
256
+ method returns the subtype — covariant return, honoured
257
+ automatically. See [§ "F-bounded polymorphism and self
258
+ types"](appendix-type-theory.md#f-bounded-polymorphism-and-self-types)
259
+ for the deeper treatment; the LSP reading is that `self` is the
260
+ mechanism that keeps `Sub#dup` returning `Sub`, never widening back
261
+ to the ancestor and thereby weakening the postcondition.
262
+
263
+ ## Preconditions: contravariant parameters and duck typing
264
+
265
+ The behavioral rule "**preconditions may not be strengthened**" is,
266
+ in Ruby, almost a restatement of duck typing. A Ruby method does not
267
+ declare what *class* it wants; it declares what *messages* it sends.
268
+ Any object answering those messages is substitutable — which is
269
+ exactly "do not require more of the caller than necessary."
270
+
271
+ Rigor encodes this with **capability roles** and structural
272
+ interfaces (the clause-2 toolbox):
273
+
274
+ | Body uses | Over-strict (strengthened precondition) | Clause-2 / LSP-friendly |
275
+ | --- | --- | --- |
276
+ | only `#write` | `IO` | `_Writable` (StringIO, Tempfile, mocks all qualify) |
277
+ | only `#upcase` | `String` | a `#upcase`-bearing role |
278
+ | `+`, `-`, `<=>` | `Integer` | `Numeric` |
279
+ | `#to_s`, nil-guarded | `String` | `String \| nil` (body narrows) |
280
+
281
+ ```ruby
282
+ # A subtype override that *narrowed* its parameter to IO would
283
+ # strengthen the precondition and break substitutability. Rigor's
284
+ # inferred parameter for the parent is already _Writable, so the
285
+ # LSP-honouring override is also the one Rigor infers by default.
286
+ def dump(stream)
287
+ stream.write(serialize)
288
+ end
289
+ ```
290
+
291
+ The **open-world assumption** on `Dynamic[T]` /
292
+ `respond_to?`-narrowed receivers
293
+ ([§ "Open-world vs closed-world"](appendix-type-theory.md#open-world-vs-closed-world-assumption))
294
+ is the same instinct at the call site: Rigor does not assume it
295
+ knows the full method set of a dynamic value, so it does not
296
+ manufacture a precondition ("this exact class") the runtime never
297
+ required. Strengthening the precondition would mean firing a false
298
+ positive on working duck-typed code — which collides head-on with
299
+ the project's [false-positive
300
+ discipline](../adr/8-steep-inspired-improvements.md). LSP and "never
301
+ frighten working code" point the same way.
302
+
303
+ ## Postconditions: covariant returns and `self` types
304
+
305
+ "**Postconditions may not be weakened**" is clause 1, and it has a
306
+ concrete enforcement surface: **`def.return-type-mismatch`**
307
+ ([ADR-8](../adr/8-steep-inspired-improvements.md)). When a method
308
+ carries a declared RBS return type and the body's inferred type
309
+ *cannot satisfy* it, Rigor emits an error:
310
+
311
+ ```ruby
312
+ class Repo
313
+ # RBS: def find: (Integer) -> User
314
+ def find(id)
315
+ @cache[id] # inferred: User | nil
316
+ end
317
+ # def.return-type-mismatch — the body can return nil, which
318
+ # weakens the declared "-> User" postcondition.
319
+ end
320
+ ```
321
+
322
+ This is the postcondition rule applied to a single method against
323
+ its *own* declared contract. Note the scope precisely (it matters
324
+ for the next section): `def.return-type-mismatch` checks the body
325
+ against the method's **own** RBS signature, not against a
326
+ superclass's signature. The complementary check — comparing a
327
+ subtype's override against the supertype's declared return to verify
328
+ covariance across the hierarchy — shipped in v0.1.15 as the
329
+ `def.override-*` rule family (§ "Cross-hierarchy override
330
+ compatibility" below). The robustness principle keeps each
331
+ *individually authored* signature honest; the override family then
332
+ verifies that the authored child contract substitutes for the
333
+ authored parent contract.
334
+
335
+ The covariant-return half is also why **self types** earn their
336
+ keep here, mirrored from the variance section: `def dup: () -> self`
337
+ guarantees a subtype's inherited `dup` keeps returning the subtype.
338
+ A signature that widened `dup`'s return to `Object` in a subtype
339
+ would weaken the postcondition; `self` makes the LSP-correct
340
+ behaviour the *only* behaviour expressible.
341
+
342
+ ## Invariants and the history constraint
343
+
344
+ The third and fourth behavioral rules — **invariants preserved** and
345
+ the **history constraint** — are where Rigor's coverage is
346
+ *partial*, and it is worth being precise about what is and is not
347
+ modelled.
348
+
349
+ Rigor has an internal **effect model**
350
+ ([§ "Effect systems"](appendix-type-theory.md#effect-systems)) that
351
+ tracks mutation, non-local exit, and closure escape. The
352
+ user-visible consequences relevant to invariants:
353
+
354
+ - **Mutation invalidates narrowing (fact stability).** A narrowed
355
+ fact about a variable is dropped after a mutating call, because
356
+ the mutation might have broken the property the narrowing relied
357
+ on. This is the *local, within-a-method* analogue of "an
358
+ invariant must survive every operation" — Rigor refuses to keep
359
+ trusting a fact a mutation could have falsified.
360
+ - **`frozen?` narrowing.** Rigor narrows on `frozen?`, and a frozen
361
+ object is one whose state-history is closed — the strongest form
362
+ of the history constraint Ruby offers. Immutable value objects
363
+ (`Data.define`, frozen literals) are the idiom that satisfies the
364
+ history constraint by construction, and Rigor types them
365
+ precisely.
366
+
367
+ What Rigor does **not** do: it does not verify, across a class
368
+ hierarchy, that a subtype's added methods preserve a supertype's
369
+ declared invariant, nor does it enforce the Liskov-Wing history
370
+ constraint as a subtyping check (the rule that forbids a mutable
371
+ `Point` from subtyping an immutable `Point`). Ruby has no surface
372
+ for declaring such invariants, and inferring them is well outside
373
+ the AST-walking, no-false-positives envelope. The history-constraint
374
+ *spirit* is honoured operationally (fact stability, `frozen?`); the
375
+ *subtyping check* is not implemented.
376
+
377
+ ## Behavioral vs nominal subtyping in Ruby
378
+
379
+ LSP is a statement about **behavioral** subtyping, but Ruby's
380
+ runtime dispatch is **nominal-and-mixin**: `is_a?` consults the
381
+ ancestor chain, and `Comparable` / `Enumerable` are behavioral
382
+ contracts delivered *through* a nominal mixin (include the module,
383
+ implement the one required method, inherit the rest). Ruby thus
384
+ blends the two — nominal inheritance for identity, module mixins for
385
+ shared behaviour.
386
+
387
+ Rigor's **nominal-first-with-structural-facets** stance
388
+ ([§ "Nominal vs structural typing"](appendix-type-theory.md#nominal-vs-structural-typing))
389
+ matches this exactly:
390
+
391
+ - Nominal classes are the unit of modelling and the stable
392
+ attachment point for declared contracts.
393
+ - `interface _Comparable`-style structural shapes and capability
394
+ roles capture the behavioral, duck-typed substitutability that
395
+ pure nominal subtyping misses.
396
+
397
+ The Ruby community already teaches LSP without the type theory.
398
+ Sandi Metz's *Practical Object-Oriented Design in Ruby* frames the
399
+ "L" of SOLID as: **subclasses should be substitutable for their
400
+ superclasses**, and the practical test is that callers written
401
+ against the superclass keep working unchanged. That is Liskov's 1987
402
+ sentence in plain Ruby. Rigor's contribution is to make the
403
+ *signature half* of that discipline machine-checkable, and to do so
404
+ without asking the programmer to write a single annotation, because
405
+ the robustness principle infers the LSP-correct parameter and return
406
+ shapes automatically.
407
+
408
+ ## Where Ruby lets you violate LSP — and what Rigor does
409
+
410
+ Ruby imposes **no** static override discipline. You can override a
411
+ method with a different arity, a narrower parameter, an unrelated
412
+ return, or a brand-new exception, and the interpreter will not
413
+ complain until (and unless) a runtime path hits the incompatibility.
414
+ Every one of these is a potential LSP violation:
415
+
416
+ ```ruby
417
+ class Animal
418
+ def speak(volume) = "..." * volume
419
+ end
420
+
421
+ class Mute < Animal
422
+ def speak # arity narrowed: strengthened precondition
423
+ raise NotImplementedError # new exception: exception-rule violation
424
+ end
425
+ end
426
+ ```
427
+
428
+ A *sound* checker would flag the override. Rigor, by its
429
+ [no-false-positives stance](../adr/8-steep-inspired-improvements.md),
430
+ does **not** bark at every override — because in Ruby, overriding
431
+ with a changed shape is frequently *intentional and correct*
432
+ (template-method refinements, `method_missing`, DSL-driven
433
+ redefinition, `define_method`). Treating every shape change as an
434
+ LSP error would frighten enormous amounts of working code.
435
+
436
+ Instead Rigor fires only where it can *prove* a contract is broken:
437
+
438
+ - `call.argument-type-mismatch` when a call site provably passes
439
+ something a declared parameter cannot accept.
440
+ - `def.return-type-mismatch` when a body provably cannot produce its
441
+ *own declared* return.
442
+
443
+ Both are local proofs against an authored contract, never a
444
+ speculative cross-hierarchy substitutability judgment. This is the
445
+ LSP-pragmatic position: enforce the parts you can prove, infer the
446
+ LSP-correct shape everywhere you author one, and stay silent on the
447
+ override-compatibility question that Ruby idioms routinely and
448
+ legitimately blur.
449
+
450
+ The deeper point: because the robustness principle *infers*
451
+ wide parameters and narrow returns by default, the most common
452
+ LSP-correct design is also the *path of least resistance* under
453
+ Rigor. A programmer following Rigor's inferred shapes is, without
454
+ trying, writing overrides that don't strengthen preconditions or
455
+ weaken postconditions. Rigor nudges toward LSP compliance through
456
+ its defaults rather than policing it through diagnostics.
457
+
458
+ ## Cross-hierarchy override compatibility
459
+
460
+ Since v0.1.15 ([ADR-35](../adr/35-override-signature-compatibility.md)),
461
+ Rigor *does* compare an override against the contract it inherits —
462
+ the signature rule applied across a project-defined class/module
463
+ hierarchy. Three rules make up the `def.override-*` family:
464
+
465
+ - **`def.override-param-narrowed`** — parameter contravariance: an
466
+ override may not strengthen its precondition by narrowing a
467
+ parameter type.
468
+ - **`def.override-return-widened`** — return covariance: an override
469
+ may not weaken its postcondition by widening a return type.
470
+ - **`def.override-visibility-reduced`** — an override may not reduce
471
+ inherited visibility (public → protected/private).
472
+
473
+ The family is the load-bearing-discipline counterpart to the
474
+ robustness principle: where the principle *biases inferred*
475
+ signatures toward substitutability, these rules *verify authored*
476
+ ones. They are gated for false-positive discipline — both the
477
+ override and the shadowed ancestor must carry an authored signature
478
+ (hand-written / rbs-inline / bundled RBS; inference-only either side
479
+ stays silent), only a proven (`:no`) violation fires, and severity
480
+ maps through `severity_profile:` (`lenient` → off, `balanced` →
481
+ warning, `strict` → error). The ancestor scope is the superclass
482
+ chain plus included/prepended modules, resolved cross-file.
483
+
484
+ The escape hatch for a *legitimate* specialization that looks like a
485
+ narrowing is **generics first, not suppression**: declare the parent
486
+ with a bounded type parameter (`interface _Consumer[T < Message]`)
487
+ and have the subtype bind it (`include _Consumer[SendMailMessage]`),
488
+ so the override matches the *instantiated* contract — the same
489
+ resolution PHPStan reaches for in [*Generics in PHP using
490
+ PHPDocs*](https://medium.com/@ondrejmirtes/generics-in-php-using-phpdocs-14e7301953)
491
+ ("even Barbara Liskov is happy with it"). Second is keeping the
492
+ declared parameter wide and recovering the narrow type in the body
493
+ via occurrence typing; `# rigor:disable def.override-*` is the last
494
+ resort.
495
+
496
+ ## What Rigor does NOT check (LSP-wise)
497
+
498
+ For completeness, the LSP obligations Rigor does **not** statically
499
+ enforce in v0.1.x — named here so you can stop looking:
500
+
501
+ - **The inferred-side of cross-hierarchy override compatibility.**
502
+ The shipped `def.override-*` family (§ "Cross-hierarchy override
503
+ compatibility" above) gates on *both sides carrying an authored
504
+ signature*. The complementary case — checking a child's *inferred*
505
+ return against an authored parent return ([ADR-35](../adr/35-override-signature-compatibility.md)
506
+ slice 5) — plus RBS-only-ancestor reach and singleton (`def self.`)
507
+ method coverage remain deferred (demand-driven, higher
508
+ false-positive surface).
509
+ - **Exception compatibility.** The signature rule's "no new
510
+ exceptions in a subtype" is not checked. Rigor has an internal
511
+ exception/non-local-exit effect
512
+ ([§ "Effect systems"](appendix-type-theory.md#effect-systems))
513
+ but does not surface a `raise`-set per method or compare it across
514
+ overrides. RBS has no widely-used `raises` clause to check
515
+ against.
516
+ - **Behavioral pre/postcondition formulas (Design by Contract).**
517
+ Meyer-style `require` / `ensure` predicate contracts, and their
518
+ inheritance rules, have no Rigor surface. The closest analogues
519
+ are refinement carriers (`non-empty-string`, `positive-int`, …) at
520
+ the *type* level and `RBS::Extended` predicate directives — neither
521
+ is a general contract formula.
522
+ - **The history constraint as a subtyping check.** Honoured
523
+ operationally via fact stability + `frozen?` (§ "Invariants"); not
524
+ enforced as a hierarchy-level rule.
525
+ - **Invariant inference across a class hierarchy.** No declaration
526
+ surface, outside the AST-walking envelope.
527
+
528
+ If any of these becomes important to the user base, it will be
529
+ discussed in an ADR before any implementation slice — the same
530
+ discipline the type-theory appendix's [§ "What Rigor does NOT
531
+ model"](appendix-type-theory.md#what-rigor-does-not-model) records.
532
+
533
+ ## A short reading list
534
+
535
+ - Liskov, B. "Data Abstraction and Hierarchy." *OOPSLA '87
536
+ Addendum / SIGPLAN Notices*, 1988. The keynote that stated the
537
+ substitution intuition.
538
+ - Liskov, B. & Wing, J.M. "A Behavioral Notion of Subtyping." *ACM
539
+ TOPLAS*, 1994. The precise formulation — signature rule + the
540
+ pre/postcondition/invariant/history behavioral rules. The
541
+ reference for everything on this page.
542
+ - Meyer, B. *Object-Oriented Software Construction*, 2nd ed.
543
+ Prentice Hall, 1997. Design by Contract and the inheritance rules
544
+ for `require` / `ensure` / `invariant` — the behavioral rule
545
+ stated as a contract discipline.
546
+ - Cardelli, L. & Wegner, P. "On Understanding Types, Data
547
+ Abstraction, and Polymorphism." *ACM Computing Surveys*, 1985. The
548
+ variance and subtyping vocabulary the signature rule is phrased
549
+ in.
550
+ - Metz, S. *Practical Object-Oriented Design in Ruby* (POODR).
551
+ Addison-Wesley, 2nd ed. 2018. The Ruby-community statement of the
552
+ "L" in SOLID — substitutability as a practical design test,
553
+ no type theory required.
554
+ - See also the companion [§ "Robustness principle (Postel's law for
555
+ types)"](../type-specification/robustness-principle.md) (normative)
556
+ and [ADR-5](../adr/5-robustness-principle.md) (rationale) — the
557
+ Rigor surface this page maps LSP onto.
558
+
559
+ ## What's next
560
+
561
+ - [Appendix — Connections to type theory](appendix-type-theory.md)
562
+ for the formal scaffolding this page leans on — variance, the
563
+ gradual guarantee, self types / F-bounded polymorphism, the
564
+ soundness vs completeness trade-off.
565
+ - [Chapter 6 — Classes](06-classes.md) for instance-side vs
566
+ class-side typing, `self`, and `Data.define` — the OO surface LSP
567
+ is about.
568
+ - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md) for
569
+ authoring the declared contracts `def.return-type-mismatch` checks
570
+ against.
571
+ - [Chapter 8 — Understanding errors](08-understanding-errors.md) for
572
+ the `def.return-type-mismatch` / `call.argument-type-mismatch`
573
+ rules and severity profiles.
574
+
575
+ If you want to compare against another *tool* rather than the
576
+ *principle*, the sibling appendices cover
577
+ [TypeScript](appendix-typescript.md),
578
+ [PHPStan](appendix-phpstan.md),
579
+ [mypy / Pyright](appendix-mypy.md),
580
+ and [Steep](appendix-steep.md).