rigortype 0.2.0 → 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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -20
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/docs/handbook/01-getting-started.md +311 -0
  27. data/docs/handbook/02-everyday-types.md +337 -0
  28. data/docs/handbook/03-narrowing.md +359 -0
  29. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  30. data/docs/handbook/05-methods-and-blocks.md +339 -0
  31. data/docs/handbook/06-classes.md +305 -0
  32. data/docs/handbook/07-rbs-and-extended.md +427 -0
  33. data/docs/handbook/08-understanding-errors.md +373 -0
  34. data/docs/handbook/09-plugins.md +241 -0
  35. data/docs/handbook/10-sorbet.md +347 -0
  36. data/docs/handbook/11-sig-gen.md +312 -0
  37. data/docs/handbook/12-lightweight-hkt.md +333 -0
  38. data/docs/handbook/README.md +275 -0
  39. data/docs/handbook/appendix-elixir.md +370 -0
  40. data/docs/handbook/appendix-go.md +399 -0
  41. data/docs/handbook/appendix-java-csharp.md +470 -0
  42. data/docs/handbook/appendix-liskov.md +580 -0
  43. data/docs/handbook/appendix-mypy.md +370 -0
  44. data/docs/handbook/appendix-phpstan.md +338 -0
  45. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  46. data/docs/handbook/appendix-rust.md +446 -0
  47. data/docs/handbook/appendix-steep.md +336 -0
  48. data/docs/handbook/appendix-type-theory.md +1662 -0
  49. data/docs/handbook/appendix-typeprof.md +416 -0
  50. data/docs/handbook/appendix-typescript.md +332 -0
  51. data/docs/install.md +189 -0
  52. data/docs/llms.txt +72 -0
  53. data/docs/manual/01-installation.md +342 -0
  54. data/docs/manual/02-cli-reference.md +557 -0
  55. data/docs/manual/03-configuration.md +152 -0
  56. data/docs/manual/04-diagnostics.md +206 -0
  57. data/docs/manual/05-inspecting-types.md +109 -0
  58. data/docs/manual/06-baseline.md +104 -0
  59. data/docs/manual/07-plugins.md +92 -0
  60. data/docs/manual/08-skills.md +143 -0
  61. data/docs/manual/09-editor-integration.md +245 -0
  62. data/docs/manual/10-mcp-server.md +532 -0
  63. data/docs/manual/11-ci.md +274 -0
  64. data/docs/manual/12-caching.md +116 -0
  65. data/docs/manual/13-troubleshooting.md +120 -0
  66. data/docs/manual/14-rails-quickstart.md +332 -0
  67. data/docs/manual/15-type-protection-coverage.md +204 -0
  68. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  69. data/docs/manual/17-driving-improvement.md +160 -0
  70. data/docs/manual/README.md +87 -0
  71. data/docs/manual/ci-templates/README.md +58 -0
  72. data/docs/manual/plugins/README.md +86 -0
  73. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  74. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  75. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  76. data/docs/manual/plugins/rigor-activejob.md +58 -0
  77. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  78. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  79. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  80. data/docs/manual/plugins/rigor-devise.md +70 -0
  81. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  82. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  83. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  84. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  85. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  86. data/docs/manual/plugins/rigor-graphql.md +89 -0
  87. data/docs/manual/plugins/rigor-hanami.md +83 -0
  88. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  89. data/docs/manual/plugins/rigor-minitest.md +86 -0
  90. data/docs/manual/plugins/rigor-pundit.md +72 -0
  91. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  92. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  93. data/docs/manual/plugins/rigor-rails.md +44 -0
  94. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  95. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  96. data/docs/manual/plugins/rigor-rspec.md +86 -0
  97. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  98. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  99. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  100. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  101. data/docs/manual/plugins/rigor-statesman.md +75 -0
  102. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  103. data/exe/rigor +1 -1
  104. data/lib/rigor/analysis/incremental_session.rb +4 -2
  105. data/lib/rigor/analysis/run_stats.rb +13 -1
  106. data/lib/rigor/analysis/runner.rb +54 -12
  107. data/lib/rigor/cli/check_command.rb +26 -3
  108. data/lib/rigor/cli/coverage_command.rb +67 -92
  109. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  110. data/lib/rigor/cli/docs_command.rb +248 -0
  111. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  112. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  113. data/lib/rigor/cli/skill_command.rb +103 -41
  114. data/lib/rigor/cli/skill_describe.rb +346 -0
  115. data/lib/rigor/cli.rb +25 -3
  116. data/lib/rigor/config_audit.rb +152 -0
  117. data/lib/rigor/configuration.rb +12 -0
  118. data/lib/rigor/environment/rbs_loader.rb +27 -0
  119. data/lib/rigor/environment.rb +49 -1
  120. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
  121. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  122. data/lib/rigor/inference/scope_indexer.rb +87 -89
  123. data/lib/rigor/inference/statement_evaluator.rb +27 -0
  124. data/lib/rigor/plugin/isolation.rb +5 -5
  125. data/lib/rigor/plugin/loader.rb +4 -2
  126. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  127. data/lib/rigor/protection/mutation_scanner.rb +98 -38
  128. data/lib/rigor/protection/mutator.rb +21 -0
  129. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  130. data/lib/rigor/signature_path_audit.rb +92 -0
  131. data/lib/rigor/version.rb +1 -1
  132. data/skills/rigor-ask/SKILL.md +172 -0
  133. data/skills/rigor-doctor/SKILL.md +87 -0
  134. data/skills/rigor-editor-setup/SKILL.md +114 -0
  135. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  136. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  137. data/skills/rigor-next-steps/SKILL.md +113 -0
  138. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  139. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  140. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  141. data/skills/rigor-upgrade/SKILL.md +79 -0
  142. metadata +120 -1
@@ -0,0 +1,370 @@
1
+ # Appendix — Coming from mypy / Pyright
2
+
3
+ If your static-typing baseline is Python's mypy or Pyright,
4
+ this appendix maps the vocabulary onto Rigor's. The two
5
+ ecosystems share more than they look like at first (gradual
6
+ typing, a "do not break the runtime" philosophy, separate
7
+ type-stub files `.pyi` / `.rbs`), but they make different
8
+ choices about where annotations live and how aggressive
9
+ inference is.
10
+
11
+ ## The five-second pitch
12
+
13
+ | Question | mypy / Pyright | Rigor |
14
+ | --- | --- | --- |
15
+ | Where do annotations live? | In source (`def f(x: int) -> str:`) | In `.rbs` files alongside `.rb` |
16
+ | Stub format | `.pyi` files | `.rbs` files |
17
+ | Default for unannotated code | `Any` everywhere (mypy) / inferred (Pyright) | Inferred precisely or `Dynamic[top]` |
18
+ | Strict mode | `--strict` (mypy) / `strict: true` (Pyright) | `severity_profile: strict` |
19
+ | Suppression | `# type: ignore[error-code]` | `# rigor:disable <rule>` |
20
+ | Identity of types | Nominal + structural (Protocol) | Nominal + structural facets |
21
+ | Narrowing | Flow-sensitive, type guards | Flow-sensitive, predicate methods + RBS::Extended |
22
+
23
+ The Python / Ruby parallels run deeper than syntax: both
24
+ languages were born dynamic, both grew gradual typing
25
+ late, both treat type-checking as advisory, and both ship
26
+ official syntax for type hints (Python's `typing`, Ruby's
27
+ `RBS`). Many of Rigor's design priorities echo the things
28
+ mypy got right.
29
+
30
+ ## Type vocabulary mapping
31
+
32
+ | Python typing | Rigor representation | Notes |
33
+ | --- | --- | --- |
34
+ | `int` | `Integer` | |
35
+ | `float` | `Float` | |
36
+ | `bool` | `bool` (`Constant<true> \| Constant<false>`) | |
37
+ | `str` | `String` | |
38
+ | `bytes` | `String` (with binary encoding) | Ruby has no separate `bytes` type. |
39
+ | `None` | `Constant<nil>` | `nil` is Ruby's sole no-value. |
40
+ | `Any` | `Dynamic[top]` | "Stay silent" carrier. |
41
+ | `object` | `Object` (or `Top`) | `object` is Python's universal supertype (everything, `None` included); Rigor's nearest match is `Top`. |
42
+ | `Never` / `NoReturn` | `Bot` | Empty type. |
43
+ | `Optional[T]` / `T \| None` | `T?` (i.e., `T \| nil`) | |
44
+ | `Union[A, B]` / `A \| B` | `A \| B` | Same display. |
45
+ | `Literal[42]` | `Constant<42>` | Direct match. |
46
+ | `Literal["foo", "bar"]` | `Constant<"foo"> \| Constant<"bar">` | |
47
+ | `Final[T]` | (no analogue) | Rigor does not yet track immutability. |
48
+ | `tuple[int, str]` | `Tuple[Integer, String]` | Same per-position model. |
49
+ | `list[T]` | `Array[T]` | |
50
+ | `dict[K, V]` | `Hash[K, V]` | |
51
+ | `set[T]` | `Set[T]` | |
52
+ | `TypedDict` | `HashShape{...}` | Closed shape with required / optional keys. |
53
+ | `NotRequired[T]` (TypedDict) | optional key in `HashShape` | Covered by Rigor's per-key required/optional flag. |
54
+ | `Callable[[int], str]` | `^(Integer) -> String` (RBS proc/block syntax) | |
55
+ | `TypeVar('T')` | RBS `[T]` type parameter | |
56
+ | `Generic[T]` | RBS `class Foo[T]` | |
57
+ | `Protocol` (PEP 544) | RBS `interface _Foo` | Structural typing. |
58
+ | `runtime_checkable` Protocol | (no analogue) | Rigor does not run `isinstance` against structural protocols. |
59
+ | `Self` (PEP 673) | RBS `self` type | |
60
+ | `ClassVar[T]` | `attr_*` on the singleton side / `self.@var` | |
61
+ | `Annotated[T, "tag"]` | `RBS::Extended` `%a{...}` annotation | Both attach metadata to a type. |
62
+
63
+ ## Refinement carriers vs Python annotation idioms
64
+
65
+ Python's typing system has been adding refinement-shaped
66
+ features one at a time (`Literal`, `LiteralString`, `TypeIs`,
67
+ `Annotated`). Rigor ships a broader catalogue out of the box.
68
+
69
+ | Rigor refinement | Python closest |
70
+ | --- | --- |
71
+ | `non-empty-string` | (no built-in; PEP 675's `LiteralString` is closest in spirit but different in semantics) |
72
+ | `literal-string` | `LiteralString` (PEP 675) — provably built from source-code literals. **Direct match.** |
73
+ | `positive-int` | (no built-in; convention is `Annotated[int, Gt(0)]` with third-party validators) |
74
+ | `int<min, max>` | (no built-in; same `Annotated[int, Range(...)]` convention) |
75
+ | `numeric-string` | (no built-in) |
76
+ | `non-empty-array[T]` | (no built-in; some libraries use `tuple[T, *tuple[T, ...]]`) |
77
+ | `Constant<42>` | `Literal[42]` |
78
+
79
+ `LiteralString` is the deepest equivalence — both
80
+ Python's `LiteralString` and Rigor's `literal-string` carry the
81
+ "this string came from source code, not from runtime input"
82
+ fact, and both compose through formatting / interpolation.
83
+
84
+ ## Narrowing — the part that feels familiar
85
+
86
+ Both checkers are flow-sensitive. The narrowing primitives
87
+ have direct analogues:
88
+
89
+ | Python | Rigor |
90
+ | --- | --- |
91
+ | `if x:` | `if x` — strips `False` / `None` from truthy edge |
92
+ | `if x is None:` | `if x.nil?` |
93
+ | `if x is not None:` | `unless x.nil?` |
94
+ | `isinstance(x, int)` | `x.is_a?(Integer)` |
95
+ | `if isinstance(x, (int, str)):` | `if x.is_a?(Integer) \|\| x.is_a?(String)` |
96
+ | `assert isinstance(x, T)` | `# rigor:assert-type` style via plugin OR `T.cast` via `rigor-sorbet` |
97
+ | `match x: case ...` (PEP 634) | `case x; in ...` (Ruby's pattern matching) |
98
+ | User-defined `TypeGuard[T]` (PEP 647) | `%a{rigor:v1:predicate-if-true x is T}` directive |
99
+ | User-defined `TypeIs[T]` (PEP 742) | Same directive — Rigor's narrowing is symmetric (truthy AND falsey) by default |
100
+ | `assert x is not None; x.upper()` | Same idiom: `unless x.nil?; x.upcase; end` |
101
+ | `cast(int, x)` | `T.cast(x, Integer)` via `rigor-sorbet`, or RBS-side `param:` directive |
102
+
103
+ Notable: Python's `TypeGuard` is one-sided (narrows only the
104
+ truthy edge), while `TypeIs` (PEP 742, accepted) is two-sided.
105
+ Rigor's `predicate-if-true` and `predicate-if-false` directives
106
+ are independent and compose — by default declaring
107
+ `predicate-if-true: x is T` also narrows the falsey edge to
108
+ `x is ~T`, equivalent to `TypeIs`.
109
+
110
+ ## Stubs ↔ RBS
111
+
112
+ Python's `.pyi` files and Rigor's `.rbs` files play the same
113
+ role: declare types for a library that does not ship them
114
+ inline.
115
+
116
+ | Python | Rigor |
117
+ | --- | --- |
118
+ | `.pyi` stubs | `.rbs` files |
119
+ | `typeshed` (community-maintained stubs) | `rbs_collection` + Rigor's bundled stdlib catalogues |
120
+ | `mypy_path` config | `signature_paths:` in `.rigor.yml` |
121
+ | `py.typed` marker | (no analogue — Rigor checks any file under `paths:`) |
122
+ | `from __future__ import annotations` | (no analogue — RBS is always lazy by virtue of file separation) |
123
+ | Reveal type: `reveal_type(x)` | `dump_type(x)` (info diagnostic) / `assert_type("...", x)` |
124
+
125
+ `reveal_type` and `dump_type` are the same tool with different
126
+ names — both emit the inferred type at the call site as a
127
+ diagnostic, both are no-ops at runtime in idiomatic test
128
+ harnesses, both are the canonical "what does the checker see
129
+ here?" probe.
130
+
131
+ ## Severity, suppression, and "strict mode"
132
+
133
+ | Python (mypy) | Rigor |
134
+ | --- | --- |
135
+ | `--strict` | `severity_profile: strict` |
136
+ | `--strict-optional` | Always-on in Rigor (no separate flag) |
137
+ | `--no-implicit-optional` | Always-on in Rigor |
138
+ | `--check-untyped-defs` | Always-on in Rigor |
139
+ | `--disallow-untyped-defs` | (no analogue — Rigor never demands annotations) |
140
+ | `--disallow-any-explicit` | (no analogue) |
141
+ | `# type: ignore` | `# rigor:disable all` |
142
+ | `# type: ignore[error-code]` | `# rigor:disable <rule>` |
143
+ | `# mypy: ignore-errors` (file scope) | `# rigor:disable-file all` |
144
+ | `mypy.ini` / `pyproject.toml` | `.rigor.yml` / `.rigor.dist.yml` |
145
+
146
+ The conceptual gap: mypy's `--disallow-untyped-defs` reflects
147
+ its baseline assumption that annotations should exist
148
+ everywhere. Rigor never demands annotations — inference is
149
+ always the first answer, RBS is the escape hatch. That makes
150
+ adoption smoother: there is no "you must annotate this whole
151
+ module before mypy is useful" stage.
152
+
153
+ ## Pyright vs Rigor
154
+
155
+ Pyright (Microsoft's type checker, the engine behind Pylance)
156
+ is closer to Rigor in spirit than mypy is — both prioritise
157
+ inference depth and pragmatic narrowing over annotation
158
+ completeness.
159
+
160
+ | Pyright | Rigor |
161
+ | --- | --- |
162
+ | `# pyright: ignore[reportError]` | `# rigor:disable <rule>` |
163
+ | `pyright --stats` | (no direct analogue — `rigor check --explain` surfaces gradual fallback decisions) |
164
+ | Inferred return types from body | Same — `def` bodies are walked and the inferred return propagates |
165
+ | Speculative inference (Pyright is fast) | Rigor's type-objects are immutable shared structures; cache-driven recompute is incremental |
166
+ | Strict / basic / off file-level setting | `severity_profile:` is project-wide; per-file via `# rigor:disable-file` |
167
+
168
+ If you have used Pyright's "infer aggressively, then narrow"
169
+ authoring loop, Rigor will feel familiar. The biggest
170
+ adjustment is that Rigor's annotations live in `.rbs` files,
171
+ not in the `.rb` source.
172
+
173
+ ## "No annotations needed" — true here too
174
+
175
+ Take a canonical mypy onboarding example:
176
+
177
+ ```python
178
+ def classify(n: int) -> Literal["zero", "positive", "negative"]:
179
+ if n == 0:
180
+ return "zero"
181
+ if n > 0:
182
+ return "positive"
183
+ return "negative"
184
+
185
+ result = classify(7)
186
+ # mypy: result: Literal['zero', 'positive', 'negative']
187
+ ```
188
+
189
+ The Rigor equivalent — no annotations:
190
+
191
+ ```ruby
192
+ def classify(n)
193
+ return :zero if n.zero?
194
+ return :positive if n.positive?
195
+ :negative
196
+ end
197
+
198
+ result = classify(7)
199
+ ```
200
+
201
+ Same precision; one writes the parameter and return
202
+ annotation, the other does not.
203
+
204
+ When you need a sig — for a public library boundary, for
205
+ parameter validation, for `def.return-type-mismatch` to fire —
206
+ that goes into `sig/<file>.rbs`, not into the `.rb` source.
207
+
208
+ ## Generics
209
+
210
+ Both ecosystems have generics; Rigor's are RBS's.
211
+
212
+ | Python | Rigor (via RBS) |
213
+ | --- | --- |
214
+ | `T = TypeVar('T')` | `[T]` after the method or class name |
215
+ | `def first(xs: list[T]) -> T` | `def first: [T] (Array[T]) -> T` |
216
+ | `Generic[T]` class | `class Foo[T]` |
217
+ | `T = TypeVar('T', bound=Comparable)` | `[T < Comparable]` (RBS bounded type parameters) |
218
+ | `ParamSpec` | (no analogue today) |
219
+ | `TypeVarTuple` | (no analogue today) |
220
+ | `Concatenate[X, P]` | (no analogue today) |
221
+
222
+ Rigor's generics coverage matches RBS's — it is more
223
+ conservative than Python's `typing` ecosystem, but covers the
224
+ common cases (collections, methods over generic containers,
225
+ class-level type parameters).
226
+
227
+ ## Protocols ↔ RBS interfaces
228
+
229
+ Python's PEP 544 introduced structural typing via `Protocol`.
230
+ Ruby's RBS has had structural `interface _Foo` since its first
231
+ release.
232
+
233
+ ```python
234
+ class SupportsClose(Protocol):
235
+ def close(self) -> None: ...
236
+ ```
237
+
238
+ ```rbs
239
+ interface _SupportsClose
240
+ def close: () -> void
241
+ end
242
+ ```
243
+
244
+ A class that defines `close` (with the right signature)
245
+ satisfies both. Neither system requires the class to declare
246
+ inheritance — the structural match is implicit.
247
+
248
+ Rigor reads RBS interfaces from `sig/`. When an RBS-declared
249
+ parameter is `_SupportsClose`, Rigor checks the call site's
250
+ argument structurally, the same way mypy / Pyright check
251
+ against a `Protocol`.
252
+
253
+ One word of warning carried over from Python: in Rigor "protocol"
254
+ does **not** mean this. The structural-type concept is the RBS
255
+ `interface`; "protocol" is reserved for a different,
256
+ plugin-declared feature (path-scoped behavioural contracts). The
257
+ [Protocols and structural typing appendix](appendix-protocols-and-structural-typing.md)
258
+ draws the distinction in full.
259
+
260
+ ## What mypy / Pyright have and Rigor does not
261
+
262
+ - **Variance annotations on TypeVars.** `TypeVar('T',
263
+ covariant=True)`. Rigor relies on RBS's variance, which is
264
+ fixed per the standard library — there is no user-side
265
+ variance authoring.
266
+ - **`Final` / immutability tracking.** Rigor does not yet
267
+ model "this name is never reassigned."
268
+ - **`@overload` stacks.** RBS supports method overloads, but
269
+ the dispatch logic in Rigor's analyzer is more conservative
270
+ than mypy's pattern-based overload resolution.
271
+ - **Decorator-aware type transformation.** Python's typing
272
+ ecosystem has well-developed support for decorators that
273
+ transform a function's type. Ruby's analogue is less common,
274
+ and Rigor does not yet model `Module#prepend` /
275
+ `define_method` transformations.
276
+ - **`async` / `await` types.** Ruby has Fiber and Async, but
277
+ the RBS surface for async types is patchier than Python's
278
+ `Coroutine[T, U, V]`.
279
+
280
+ ## What Rigor has and mypy / Pyright do not
281
+
282
+ - **Constant folding through method calls.** mypy and Pyright
283
+ both fold literals, but neither folds through arbitrary
284
+ built-in methods. Rigor folds through a catalogued set of
285
+ pure methods on `Numeric`, `String`, `Symbol`, `Array`,
286
+ `Hash`.
287
+ - **First-class refinement carriers with narrowing.**
288
+ `non-empty-string`, `positive-int`, `numeric-string`,
289
+ `int<min, max>` — values restricted by predicate, narrowed
290
+ by the corresponding Ruby predicate methods.
291
+ - **No-false-positives stance.** mypy will warn about dynamic
292
+ code unless `--no-warn-unused-ignores` or `--ignore-missing-imports`
293
+ is set; Rigor stays silent on `Dynamic[top]` without
294
+ configuration.
295
+ - **Plugin-side return-type variation by argument shape.**
296
+ Pyright's "type alias narrowing" and mypy's overload stacks
297
+ cover some cases; Rigor's plugin contract gives you full
298
+ Ruby code at the dispatch point. The
299
+ [`rigor-lisp-eval`](../../examples/rigor-lisp-eval/) example
300
+ is the canonical demo — `Lisp.eval([:+, 1, 2])` returns
301
+ `Integer`, `Lisp.eval([:<, 1, 2])` returns `bool`.
302
+
303
+ ## A migration vignette
304
+
305
+ You are porting a mypy-tightened Python module to Ruby. The
306
+ original:
307
+
308
+ ```python
309
+ def classify_input(s: str) -> Literal["empty", "numeric", "text"]:
310
+ if not s:
311
+ return "empty"
312
+ if s.isdigit():
313
+ return "numeric"
314
+ return "text"
315
+
316
+ def shout(s: str) -> str:
317
+ assert s, "expected non-empty"
318
+ return s.upper()
319
+ ```
320
+
321
+ The Rigor port:
322
+
323
+ ```ruby
324
+ # lib/text_utils.rb
325
+ def classify_input(s)
326
+ return :empty if s.empty?
327
+ return :numeric if s.match?(/\A\d+\z/)
328
+ :text
329
+ end
330
+
331
+ def shout(s)
332
+ raise ArgumentError if s.empty?
333
+ s.upcase
334
+ end
335
+ ```
336
+
337
+ ```rbs
338
+ # sig/text_utils.rbs
339
+ %a{rigor:v1:return: Constant<:empty> | Constant<:numeric> | Constant<:text>}
340
+ def classify_input: (String s) -> Symbol
341
+
342
+ %a{rigor:v1:param: s is non-empty-string}
343
+ def shout: (String s) -> non-empty-string
344
+ ```
345
+
346
+ You gain: `s.empty?` is a recognised refinement narrower (no
347
+ need for `assert s`). `match?(/\A\d+\z/)` does not yet narrow
348
+ to `numeric-string` (this is on the v0.1.1 roadmap — see
349
+ [`docs/ROADMAP.md`](../ROADMAP.md)), but the eventual
350
+ behaviour will mirror `s.isdigit()` narrowing in Pyright.
351
+
352
+ ## What's next
353
+
354
+ You probably do not need to read the rest of this appendix
355
+ section sequentially. Three useful pointers:
356
+
357
+ - [Chapter 2 — Everyday types](02-everyday-types.md) for the
358
+ carrier zoo if the refinement vocabulary is new.
359
+ - [Chapter 3 — Narrowing](03-narrowing.md) for the
360
+ flow-sensitive rules — direct analogues to mypy's narrowing.
361
+ - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
362
+ for the directive grammar — `predicate-if-true` is Rigor's
363
+ `TypeGuard` / `TypeIs`.
364
+
365
+ If you want to compare against another tool, the sibling
366
+ appendix pages cover [TypeScript](appendix-typescript.md),
367
+ [PHPStan](appendix-phpstan.md), [Steep](appendix-steep.md),
368
+ [TypeProf](appendix-typeprof.md),
369
+ [Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
370
+ [Go](appendix-go.md), and [Elixir](appendix-elixir.md).