rigortype 0.2.1 → 0.2.3

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 (109) 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 +569 -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 +539 -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/triage_command.rb +8 -2
  89. data/lib/rigor/cli/triage_renderer.rb +4 -0
  90. data/lib/rigor/cli.rb +25 -3
  91. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  93. data/lib/rigor/inference/scope_indexer.rb +87 -89
  94. data/lib/rigor/plugin/isolation.rb +5 -5
  95. data/lib/rigor/plugin/loader.rb +4 -2
  96. data/lib/rigor/triage/catalogue.rb +16 -1
  97. data/lib/rigor/triage.rb +30 -7
  98. data/lib/rigor/version.rb +1 -1
  99. data/skills/rigor-ask/SKILL.md +172 -0
  100. data/skills/rigor-doctor/SKILL.md +87 -0
  101. data/skills/rigor-editor-setup/SKILL.md +114 -0
  102. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  103. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  104. data/skills/rigor-next-steps/SKILL.md +113 -0
  105. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  106. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  107. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  108. data/skills/rigor-upgrade/SKILL.md +79 -0
  109. metadata +90 -1
@@ -0,0 +1,427 @@
1
+ # RBS and RBS::Extended
2
+
3
+ When Rigor's inference cannot prove a type, the next escape
4
+ hatch is RBS — Ruby's signature language. When RBS cannot
5
+ express the precise contract you want, `RBS::Extended` adds a
6
+ small annotation surface on top.
7
+
8
+ This chapter covers both, in the order you usually reach for
9
+ them.
10
+
11
+ ## When you need RBS
12
+
13
+ You probably need to add an RBS file when:
14
+
15
+ - The method body's return type depends on an external gem
16
+ Rigor's bundled stdlib does not cover.
17
+ - You want `call.argument-type-mismatch` to fire on
18
+ argument-shape errors (in-source `def` does NOT enforce
19
+ parameter contracts; only RBS-declared methods do).
20
+ - You want `def.return-type-mismatch` to fire when a body's
21
+ inferred return drifts from the declared return.
22
+ - A future RBS-aware tool (Steep, ruby-lsp) will read the
23
+ same file and benefit from the contract.
24
+
25
+ You probably do **not** need RBS when:
26
+
27
+ - The method is private to your project, the body is short,
28
+ and Rigor already infers the right return type.
29
+ - The method is a wrapper around a method that already
30
+ has a sig (Rigor walks the body and propagates).
31
+
32
+ ## A first sig
33
+
34
+ In a fresh project:
35
+
36
+ ```text
37
+ my-app/
38
+ ├── lib/
39
+ │ └── slug.rb
40
+ └── sig/
41
+ └── slug.rbs # ← your sig
42
+ ```
43
+
44
+ ```ruby
45
+ # lib/slug.rb
46
+ class Slug
47
+ def normalise(id)
48
+ id.downcase.gsub(/\s+/, "-")
49
+ end
50
+ end
51
+ ```
52
+
53
+ ```rbs
54
+ # sig/slug.rbs
55
+ class Slug
56
+ def normalise: (String) -> String
57
+ end
58
+ ```
59
+
60
+ Drop the `.rbs` file in `sig/` and Rigor picks it up
61
+ automatically — no `.rigor.yml` change required. The default
62
+ config has `signature_paths: [sig]`.
63
+
64
+ After that, this code:
65
+
66
+ ```ruby
67
+ Slug.new.normalise(42)
68
+ ```
69
+
70
+ fires `call.argument-type-mismatch`: `42` is an Integer, the
71
+ parameter is `String`.
72
+
73
+ ## When the RBS shape is too wide
74
+
75
+ The Slug example's runtime always returns a non-empty,
76
+ lowercase string — but the RBS sig only says `String`. If you
77
+ want Rigor to know the narrower fact, attach an `RBS::Extended`
78
+ annotation:
79
+
80
+ ```rbs
81
+ class Slug
82
+ %a{rigor:v1:return: non-empty-lowercase-string}
83
+ def normalise: (String) -> String
84
+ end
85
+ ```
86
+
87
+ Now:
88
+
89
+ ```ruby
90
+ s = Slug.new.normalise("Hello World")
91
+ # s: non-empty-lowercase-string
92
+ s.empty? # Constant<false> — proven
93
+ s.size # positive-int — proven
94
+ s == "hello-world" # bool — equality narrowing applies
95
+ ```
96
+
97
+ The `.rbs` file is **still valid RBS** — `%a{...}` is the RBS
98
+ annotation syntax. Steep / typeprof / ruby-lsp see a comment;
99
+ Rigor sees a tightening.
100
+
101
+ ## The directive grammar
102
+
103
+ `RBS::Extended` lives at
104
+ [`docs/type-specification/rbs-extended.md`](../type-specification/rbs-extended.md).
105
+ The per-method directives:
106
+
107
+ | Directive | Says |
108
+ | --- | --- |
109
+ | `%a{rigor:v1:return: <type>}` | Tighten the method's return type. |
110
+ | `%a{rigor:v1:param: <name> is <type>}` | Tighten a parameter's accepted type at the call site, AND narrow the local in the body. |
111
+ | `%a{rigor:v1:assert <name> is <type>}` | After this method returns, the named local in the caller's scope is `<type>`. |
112
+ | `%a{rigor:v1:predicate-if-true <name> is <type>}` | When this method returns truthy, the named local in the caller's scope is `<type>`. (Symmetric `predicate-if-false`.) |
113
+ | `%a{rigor:v1:assert-if-true <name> is <type>}` | When this method returns a truthy value, the named local in the caller's scope is `<type>`. (Symmetric `assert-if-false` for `false` / `nil` returns.) |
114
+
115
+ The `<type>` slot accepts:
116
+
117
+ - **RBS class names** — `String`, `Integer`, `::Foo::Bar`.
118
+ - **Imported refinement names** —
119
+ `non-empty-string`, `lowercase-string`, `numeric-string`,
120
+ `int<5, 10>`, `non-empty-array[Integer]`, `literal-string`,
121
+
122
+ - **Negation `~T`** — `~lowercase-string` means
123
+ "non-lowercase-string."
124
+
125
+ ## Refinement names
126
+
127
+ The full catalogue is in
128
+ [`docs/type-specification/imported-built-in-types.md`](../type-specification/imported-built-in-types.md).
129
+ A short reference:
130
+
131
+ | Family | Names |
132
+ | --- | --- |
133
+ | Empty / non-empty | `non-empty-string`, `non-empty-array[T]`, `non-empty-hash[K, V]` |
134
+ | Integer ranges | `positive-int`, `non-negative-int`, `negative-int`, `non-positive-int`, `non-zero-int`, `int<min, max>` |
135
+ | String predicates | `lowercase-string`, `uppercase-string`, `numeric-string`, `decimal-int-string`, `octal-int-string`, `hex-int-string`, `literal-string` |
136
+ | Paired complements | `non-lowercase-string`, `non-uppercase-string`, `non-numeric-string` |
137
+ | Composed | `non-empty-lowercase-string`, `non-empty-uppercase-string`, `non-empty-literal-string` |
138
+ | Shape projections | `pick_of[T, K]`, `omit_of[T, K]`, `partial_of[T]`, `required_of[T]`, `readonly_of[T]` — derive new `HashShape` / `Tuple` carriers from existing ones. See [chapter 4 § "Deriving new shapes"](04-tuples-and-shapes.md#deriving-new-shapes--pick_of--omit_of--partial_of--required_of--readonly_of). |
139
+
140
+ ## Declaring conformance — `conforms-to`
141
+
142
+ The directives above attach to a `def`. One more attaches to a
143
+ `class` / `module` declaration and asserts the whole class
144
+ satisfies a named structural interface, as a checked design
145
+ assertion — whether or not any call site exercises it:
146
+
147
+ ```rbs
148
+ %a{rigor:v1:conforms-to _RewindableStream}
149
+ class MyBuffer
150
+ def read: (Integer) -> String
151
+ def rewind: () -> void
152
+ end
153
+ ```
154
+
155
+ If `MyBuffer` is missing a method the `_RewindableStream`
156
+ interface requires (or the signature is incompatible), Rigor
157
+ reports `rbs_extended.unsatisfied-conformance`; a class that
158
+ satisfies the interface is silent. Multiple `conforms-to`
159
+ directives on one class combine like an intersection of
160
+ interfaces. The directive is purely additive — implicit
161
+ structural compatibility at call sites keeps working with or
162
+ without it.
163
+
164
+ ## Worked example: an assertion gate
165
+
166
+ ```rbs
167
+ class Validator
168
+ %a{rigor:v1:assert x is non-empty-string}
169
+ def assert_non_empty: (String x) -> void
170
+ end
171
+ ```
172
+
173
+ ```ruby
174
+ def configure(host)
175
+ Validator.new.assert_non_empty(host)
176
+ # host: non-empty-string after this call
177
+ host.size # positive-int — proven
178
+ end
179
+ ```
180
+
181
+ The runtime side is whatever `assert_non_empty` does (raise
182
+ on empty, log, ...) — Rigor only reads the directive.
183
+
184
+ ## Worked example: a type predicate
185
+
186
+ ```rbs
187
+ class Range
188
+ %a{rigor:v1:predicate-if-true value is Integer}
189
+ def integer?: (untyped value) -> bool
190
+ end
191
+ ```
192
+
193
+ ```ruby
194
+ def double_if_int(value)
195
+ if (1..10).integer?(value)
196
+ # value: Integer in the truthy branch
197
+ value * 2
198
+ else
199
+ value
200
+ end
201
+ end
202
+ ```
203
+
204
+ This is the supported way to teach Rigor about a custom
205
+ type-predicate method that the engine's built-in `is_a?` /
206
+ `nil?` rules cannot recognise.
207
+
208
+ ## Worked example: parameter override
209
+
210
+ ```rbs
211
+ class Slug
212
+ %a{rigor:v1:param: id is non-empty-string}
213
+ def normalise: (String id) -> String
214
+ end
215
+ ```
216
+
217
+ This has two effects:
218
+
219
+ 1. **Call-site checking.** `Slug.new.normalise("")` is now a
220
+ `call.argument-type-mismatch` because `Constant<"">` does
221
+ not satisfy `non-empty-string`.
222
+ 2. **Body-side narrowing.** Inside the method body of
223
+ `normalise`, the parameter `id` is `non-empty-string`. So
224
+ `id.empty?` reduces to `Constant<false>` and `id.size`
225
+ reduces to `positive-int`.
226
+
227
+ ## When you need a parameter override the runtime cannot enforce
228
+
229
+ Sometimes the runtime function does NOT raise on bad input —
230
+ it returns nil, returns a default, or swallows the error.
231
+ Rigor's `param:` directive still tightens the call-site
232
+ contract:
233
+
234
+ ```rbs
235
+ class FileLoader
236
+ %a{rigor:v1:param: path is non-empty-string}
237
+ def load: (String path) -> String?
238
+ end
239
+ ```
240
+
241
+ `FileLoader.new.load("")` fires `call.argument-type-mismatch`
242
+ even though at runtime `load` would fail gracefully. The
243
+ directive expresses **what callers should pass**, not what
244
+ the body enforces.
245
+
246
+ ## Where annotations belong
247
+
248
+ Put `RBS::Extended` annotations on the same `def` they refine,
249
+ inside the same `.rbs` file. Group them above the method:
250
+
251
+ ```rbs
252
+ class Slug
253
+ %a{rigor:v1:return: non-empty-string}
254
+ %a{rigor:v1:param: id is non-empty-string}
255
+ def normalise: (String id) -> String
256
+ end
257
+ ```
258
+
259
+ You **cannot** put these `%a{rigor:v1:…}` directives inside a
260
+ `.rb` file. The directives only fire when read from RBS —
261
+ that is a design choice (see
262
+ ADR-5, the robustness principle: strict on returns, lenient
263
+ on parameters).
264
+
265
+ ## Inline RBS in Ruby source — the `rigor-rbs-inline` plugin
266
+
267
+ A separate, opt-in plugin lets you write method types directly
268
+ above the `def` in your Ruby file, using the
269
+ [rbs-inline](https://github.com/soutaro/rbs-inline) comment
270
+ vocabulary upstream defines:
271
+
272
+ ```rb
273
+ # rbs_inline: enabled
274
+
275
+ class AscDesc
276
+ # @rbs asc_or_desc: :asc | :desc
277
+ def ascdesc(asc_or_desc)
278
+ asc_or_desc
279
+ end
280
+ end
281
+
282
+ AscDesc.new.ascdesc(:bad)
283
+ # => error: argument type mismatch at parameter `asc_or_desc' of
284
+ # `ascdesc' on AscDesc: expected :asc | :desc, got :bad
285
+ ```
286
+
287
+ The `# @rbs name: T` doc-style annotation, the `#: () -> T`
288
+ inline method-type comment, `# @rbs return: T`, attribute `#:`
289
+ casts, `# @rbs @ivar: T`, `# @rbs override`, and `# @rbs!` raw
290
+ RBS embedding all work — anything upstream rbs-inline accepts
291
+ flows through to Rigor's RBS environment as if you had hand-
292
+ written the equivalent `.rbs` file.
293
+
294
+ This is **not** RBS::Extended. The `# @rbs` comments are
295
+ upstream rbs-inline's grammar; the plugin transcribes them to
296
+ ordinary RBS at env build. RBS::Extended `%a{rigor:v1:…}`
297
+ directives, by contrast, are Rigor-only annotations that live
298
+ in `.rbs` files (see the rest of this chapter for those).
299
+
300
+ To enable it, add the plugin gem to your bundle and list it:
301
+
302
+ ```yaml
303
+ # .rigor.yml
304
+ plugins:
305
+ - rigor-rbs-inline
306
+ ```
307
+
308
+ Per file, opt in with the upstream `# rbs_inline: enabled`
309
+ magic comment at the top — files without it are unaffected.
310
+
311
+ Notes:
312
+
313
+ - The core `rigortype` analyzer stays zero-runtime-dependency
314
+ (ADR-0). The `rbs-inline` upstream library is a dependency
315
+ of the plugin gem, not of the core, so projects that don't
316
+ opt in pay nothing.
317
+ - A bare top-level `def` produces no RBS output through
318
+ upstream rbs-inline. Wrap method definitions in a class or
319
+ module when you need the annotation to take effect.
320
+ - A failed rbs-inline parse surfaces as a
321
+ `source-rbs-synthesis-failed` `:info` diagnostic; the file
322
+ falls back to no inline-RBS contribution and analysis
323
+ continues.
324
+
325
+ Full plugin documentation, configuration options (including
326
+ the `require_magic_comment: false` host-context override the
327
+ browser playground uses), and the caching contract:
328
+ [`plugins/rigor-rbs-inline/README.md`](../../plugins/rigor-rbs-inline/README.md).
329
+
330
+ ## Falling back to `untyped`
331
+
332
+ When a method's signature involves a type RBS cannot express,
333
+ the conservative thing to do is `untyped`:
334
+
335
+ ```rbs
336
+ def deserialize: (String) -> untyped
337
+ ```
338
+
339
+ `untyped` is a contract-free hatch — every method exists on
340
+ it, every argument shape is acceptable. Rigor's diagnostics
341
+ stay silent on `untyped` receivers. Use it for legitimately
342
+ dynamic boundaries (deserialisation, `eval`, plugin entry
343
+ points). The static analysis you lose is made up by the
344
+ honesty of admitting "this could be anything."
345
+
346
+ ## Coming from PHPStan? The `@phpstan-assert` family
347
+
348
+ If you are familiar with PHPStan's PHPDoc annotations,
349
+ Rigor's `RBS::Extended` directives map directly onto the
350
+ post-return / conditional narrowing primitives PHPStan calls
351
+ "asserts" and "type-specifying functions." The behaviour is
352
+ identical:
353
+
354
+ > "After this method returns, the named argument is `T`."
355
+
356
+ That is `@phpstan-assert` in PHPStan and
357
+ `%a{rigor:v1:assert}` in Rigor.
358
+
359
+ | PHPStan PHPDoc | Rigor RBS::Extended | Effect |
360
+ | --- | --- | --- |
361
+ | `@phpstan-assert T $x` | `%a{rigor:v1:assert x is T}` | After this method returns normally, the caller's `x` is `T`. |
362
+ | `@phpstan-assert-if-true T $x` | `%a{rigor:v1:predicate-if-true x is T}` | If this method returns truthy, the caller's `x` is `T`. |
363
+ | `@phpstan-assert-if-false T $x` | `%a{rigor:v1:predicate-if-false x is T}` | If this method returns falsey, the caller's `x` is `T`. |
364
+ | `@phpstan-assert !T $x` | `%a{rigor:v1:assert x is ~T}` | After this method returns, the caller's `x` is **not** `T` (negation form). |
365
+ | `@phpstan-assert-if-true !T $x` | `%a{rigor:v1:predicate-if-true x is ~T}` | Conditional negation. Symmetric with `predicate-if-false`. |
366
+
367
+ Worked example — the canonical "assertNotNull" pattern from
368
+ PHPStan's docs:
369
+
370
+ ```rbs
371
+ # sig/asserts.rbs
372
+ class Asserts
373
+ %a{rigor:v1:assert x is ~nil}
374
+ def self.not_nil: (untyped x) -> void
375
+ end
376
+ ```
377
+
378
+ ```ruby
379
+ # lib/configure.rb
380
+ def configure(maybe)
381
+ Asserts.not_nil(maybe)
382
+ # maybe: (~nil), so .upcase resolves on the narrowed type
383
+ maybe.upcase
384
+ end
385
+ ```
386
+
387
+ Self-targeted forms are supported too — the PHPStan
388
+ analogue would be a method on `$this` that narrows
389
+ `$this`. Name the receiver with `self`:
390
+
391
+ ```rbs
392
+ class Connection
393
+ %a{rigor:v1:assert self is Connected}
394
+ def assert_connected!: () -> void
395
+ end
396
+ ```
397
+
398
+ Rigor's directive grammar covers what PHPStan ships in the
399
+ `@phpstan-assert*` family. The directives **only fire from
400
+ RBS** (per ADR-5: strict on returns, lenient on parameters);
401
+ in PHPStan-land you can also write `@phpstan-assert` in
402
+ PHPDoc directly above the function — Rigor's equivalent is
403
+ the same RBS file's `def` line.
404
+
405
+ If you need plugin-side equivalents (when the assertion is
406
+ recognised by **call shape** rather than by sig — PHPStan's
407
+ "Type-Specifying Extensions"), see
408
+ [Chapter 9](09-plugins.md). The plugin contract surfaces
409
+ the same `Fact(target_kind: :self)` and
410
+ `Fact(target_kind: :parameter)` carriers that the directives
411
+ use, so a plugin author writes the equivalent of a PHPStan
412
+ `StaticMethodTypeSpecifyingExtension` from Ruby.
413
+
414
+ ## When RBS cannot help — the plugin escape hatch
415
+
416
+ When a method's behaviour depends on the **shape of its
417
+ arguments at runtime** (`Lisp.eval([:+, 1, 2])` returns
418
+ Integer, but `Lisp.eval([:<, 1, 2])` returns bool), no RBS sig
419
+ can express the relationship. That is what plugins are for —
420
+ see [Chapter 9](09-plugins.md) and the
421
+ [examples/](../../examples/README.md) directory.
422
+
423
+ ## What's next
424
+
425
+ Chapter 8 covers the rule catalogue — what each diagnostic
426
+ means, when it fires, and how to suppress it when it is wrong
427
+ or noisy.