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,333 @@
1
+ # Lightweight HKT (JSON.parse and friends)
2
+
3
+ `JSON.parse(str)` returns "some JSON value": `nil`, a bool, a
4
+ number, a string, an array of JSON values, or a hash of JSON
5
+ values. RBS describes that as `untyped` because there is no way
6
+ to spell a recursive sum type without quantifying over a type
7
+ constructor. Most type checkers shrug and let `JSON.parse(str)`
8
+ fade into `Dynamic[top]`.
9
+
10
+ Rigor models it precisely:
11
+
12
+ ```ruby
13
+ parsed = JSON.parse('{"name": "Alice"}')
14
+ assert_type(
15
+ "Array[json::value[String]] | Float | " \
16
+ "Hash[String, json::value[String]] | Integer | " \
17
+ "String | false | nil | true",
18
+ parsed)
19
+ ```
20
+
21
+ The mechanism behind this — and the one that lets you wire the
22
+ same shape for your own DSL or stdlib method — is **Lightweight
23
+ HKT** ([ADR-20](../adr/20-lightweight-hkt.md)), Rigor's
24
+ defunctionalised encoding of higher-kinded types in the
25
+ [Yallop & White 2014](https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf) /
26
+ [fp-ts `URItoKind`](https://github.com/gcanti/fp-ts/blob/master/src/HKT.ts)
27
+ style. This chapter walks through what it does, when to reach
28
+ for it, and how to author your own overlay.
29
+
30
+ This is the most advanced chapter in the handbook. Most
31
+ readers only need the first two sections — what the carrier
32
+ looks like and which stdlib methods are wired out of the box.
33
+ Everything after "Authoring your own overlay" is for the rare
34
+ case where you want to model a recursive sum type of your own.
35
+
36
+ ## The five-second pitch
37
+
38
+ | Concept | Rigor spelling | Where you see it |
39
+ | --- | --- | --- |
40
+ | Type-constructor "tag" | Namespaced Symbol URI (`:json::value`, `:dry_monads::result`) | `%a{rigor:v1:hkt_register: uri=…}` directive |
41
+ | Abstract application `F<A>` | `Type::App[uri, args]` | Carrier in dispatcher output |
42
+ | Type-level definition | `%a{rigor:v1:hkt_define: uri=… params=… body=…}` directive | `.rbs` overlay file |
43
+ | Reducing `App[F, A]` to a real type | `env.hkt_registry.reduce(app)` (or `app.reduce(registry)`) | Called eagerly by the dispatcher tier for known stdlib methods |
44
+ | Hooking it to a method | `Builtins::HktBuiltins::METHOD_RETURN_OVERRIDES` table | Plugin / Rigor-bundled wiring |
45
+
46
+ The next sections show each of these in action.
47
+
48
+ ## What's bundled today
49
+
50
+ Rigor ships two HKT registrations out of the box. The main
51
+ one is **`json::value[K]`**, the recursive JSON-value sum (the
52
+ second, `csv::parsed[K]`, is covered at the end of this
53
+ section). `json::value` has two parts:
54
+
55
+ ```rbs
56
+ # Registration — names the tag, declares its arity, variance,
57
+ # and erasure bound. The bound is what Rigor's RBS round-trip
58
+ # falls back to when reduction is blocked.
59
+ uri=json::value arity=1 variance=out bound=untyped
60
+
61
+ # Definition — the actual body, parameterised on K (the hash
62
+ # key type). Note the self-referential `App[json::value, K]`
63
+ # arms — Rigor's reducer handles recursion with lazy "tying-
64
+ # the-knot" semantics.
65
+ params=K body=
66
+ nil | true | false | Integer | Float | String
67
+ | Array[App[json::value, K]]
68
+ | Hash[K, App[json::value, K]]
69
+ ```
70
+
71
+ Nine stdlib methods route through this:
72
+
73
+ - `JSON.parse` / `JSON.parse!` / `JSON.load` / `JSON.load_file` / `JSON.load_file!`
74
+ - `YAML.safe_load` / `YAML.safe_load_file`
75
+ - `Psych.safe_load` / `Psych.safe_load_file`
76
+
77
+ The HKT-builtin dispatcher tier sits ABOVE the standard RBS
78
+ dispatch, so even though upstream RBS declares
79
+ `JSON.parse: (string, ?options) -> untyped`, Rigor's answer is
80
+ the reduced Union. `YAML.load` / `YAML.unsafe_load` deliberately
81
+ stay out — they can return any Ruby object and have no useful
82
+ HKT envelope.
83
+
84
+ The second bundled registration, **`csv::parsed[K]`**, models
85
+ `CSV.parse` / `CSV.read` as `Array[Array[K | nil]]` — the
86
+ no-headers shape. Calls passing `headers: true` (which return
87
+ a `CSV::Table`) and `CSV.foreach` (which yields rather than
88
+ returns) fall through to the upstream RBS type.
89
+
90
+ ## Two kinds of call-site discrimination
91
+
92
+ The bundled overrides are not just `(receiver, method) → fixed
93
+ type`. Two **discriminators** look at the call's actual
94
+ arguments:
95
+
96
+ ### `symbolize_names: true` swaps K
97
+
98
+ ```ruby
99
+ JSON.parse(str)
100
+ # parsed: ... | Hash[String, json::value[String]] | ...
101
+
102
+ JSON.parse(str, symbolize_names: true)
103
+ # parsed: ... | Hash[Symbol, json::value[Symbol]] | ...
104
+ ```
105
+
106
+ The `:json_symbolize_names` discriminator inspects the call's
107
+ second-argument `HashShape` for a literal `symbolize_names: true`
108
+ entry. Match swaps `K = String` for `K = Symbol` before the
109
+ reducer runs. Non-literal `symbolize_names: x` (a variable, a
110
+ non-`Constant<true>` value) stays on the default `String`
111
+ branch.
112
+
113
+ ### `permitted_classes:` unions extra arms
114
+
115
+ ```ruby
116
+ require "date"
117
+ parsed = YAML.safe_load(str, permitted_classes: [Date])
118
+ # parsed: ... | Date | ...
119
+ ```
120
+
121
+ The `:yaml_permitted_classes` **post-reduce hook** runs after the
122
+ reducer and augments the result. It walks the second-argument
123
+ `HashShape` for a `permitted_classes:` key whose value is a
124
+ literal `Tuple` or `Array` of Singleton classes, maps each to a
125
+ `Nominal`, and unions them with the base `json::value` Union.
126
+ `[Date, Symbol]` adds both arms.
127
+
128
+ Non-literal `permitted_classes:` values (a variable, a `Dynamic`,
129
+ a non-Singleton element) silently no-op so Rigor never invents
130
+ classes it can't statically see.
131
+
132
+ ## Authoring your own overlay
133
+
134
+ You can register your own HKT URIs in a `.rbs` file under your
135
+ `signature_paths:`. The annotations attach to a class or module
136
+ declaration (RBS's annotation grammar requires that):
137
+
138
+ ```rbs
139
+ %a{rigor:v1:hkt_register: uri=my_app::box arity=1 variance=out bound=untyped}
140
+ %a{rigor:v1:hkt_define: uri=my_app::box params=K body=K | nil}
141
+ class MyAppBoxOverlay
142
+ end
143
+ ```
144
+
145
+ A few rules:
146
+
147
+ - **URIs MUST be namespaced** (`<author>::<name>`). The `::`
148
+ separator prevents cross-plugin collisions per ADR-20 WD1.
149
+ - **The payload format is space-separated `key=value` pairs.**
150
+ RBS's `%a{...}` annotation grammar rejects quotes, so JSON
151
+ payload won't work — the kv-form is what RBS will deliver.
152
+ - **`body=` is special-cased to gobble everything to the end** of
153
+ the payload, so the body string can contain spaces, `|`, `[]`
154
+ etc. without escaping.
155
+ - **`params=` is a comma-separated list** of UCName identifiers
156
+ (`params=K` or `params=T,E`).
157
+ - **`bound=` accepts `untyped` (default) or a bare class name**.
158
+ Richer bound forms (parameterised generics, unions,
159
+ refinements) wait for a follow-up slice's expression parser.
160
+
161
+ When `Environment.for_project` builds the env, it scans the
162
+ loaded RBS for these annotations and merges them into
163
+ `env.hkt_registry` on top of the bundled builtins. Last-write-
164
+ wins on URI collisions so an overlay can override `json::value`
165
+ if you want to.
166
+
167
+ ## The body grammar
168
+
169
+ `body=` is parsed by `HktBodyParser` into a tree the reducer
170
+ walks. The grammar covers ADR-20 § D3 in full:
171
+
172
+ | Form | Example | Meaning |
173
+ | --- | --- | --- |
174
+ | Atom | `nil` / `true` / `false` / `bool` / `untyped` | Constants and the `Dynamic[top]` carrier |
175
+ | Nominal class | `Integer` / `String` / `Foo::Bar` / `::String` | `Nominal[class_name]` |
176
+ | Param reference | `K`, `T`, `E` (when in `params`) | Substituted at reduction time |
177
+ | Parameterised nominal | `Array[K]`, `Hash[K, V]` | `Nominal[..., type_args: [...]]` |
178
+ | Lightweight HKT application | `App[json::value, K]` | Another `Type::App` carrier, reduced lazily |
179
+ | Union | `A \| B \| C` | `Type::Union` (normalised) |
180
+ | **Conditional** | `(K <: String ? Integer : Float)` | Branches on a test verdict |
181
+
182
+ Disambiguation: a UCName matching one of `params` becomes a
183
+ `Param` node, **unless** it's followed by `::` (qualified class
184
+ continuation) or `[` (parameterised app), in which case it's
185
+ treated as a nominal. So `K` is a param ref, `K[X]` is the
186
+ class `K` applied to `X`.
187
+
188
+ ### Conditional types (§ D3)
189
+
190
+ Conditional types let the body branch on the bound type — useful
191
+ for shape-driven discriminators inside a single registration:
192
+
193
+ ```rbs
194
+ %a{rigor:v1:hkt_define: uri=my_app::result params=K body=
195
+ (K <: String ? Integer : Float)
196
+ }
197
+ ```
198
+
199
+ Three test operators:
200
+
201
+ | Test | Example | Meaning |
202
+ | --- | --- | --- |
203
+ | `<:` (subtype) | `K <: String` | True when `K`'s reduced type is a subtype of `String` |
204
+ | `==` (structural equality) | `K == :symbol` | True when `K`'s reduced type structurally equals the right side |
205
+ | `in [...]` (membership) | `K in [String, Symbol]` | True when `K`'s reduced type structurally equals any option |
206
+
207
+ The reducer's verdict policy is **trinary**:
208
+
209
+ - `:yes` → reduce the `then_branch`.
210
+ - `:no` → reduce the `else_branch`.
211
+ - `:maybe` (undecided — e.g. `Dynamic[T]` on either side) → widen
212
+ to the union of both reduced branches (per ADR-20 WD7 /
213
+ robustness principle — Rigor stays conservative when it can't
214
+ prove which arm fires).
215
+
216
+ Verdict policy at the current slice: structural equality → `:yes`;
217
+ disjoint nominals (different `class_name`) or disjoint constants
218
+ (different `value`) → `:no`; everything else → `:maybe`.
219
+
220
+ Branches accept unions and nested conditionals:
221
+
222
+ ```rbs
223
+ %a{rigor:v1:hkt_define: uri=my_app::numeric params=E body=
224
+ (E <: Integer ? Integer
225
+ : (E <: Float ? Float
226
+ : (E <: String ? Integer | Float | nil
227
+ : untyped)))
228
+ }
229
+ ```
230
+
231
+ Test sides themselves are single arms (no union directly on a
232
+ test side — wrap in `App[my_union, ...]` if you need a union
233
+ there).
234
+
235
+ ## Reduction semantics — lazy "tying-the-knot"
236
+
237
+ The interesting part: `json::value`'s body contains
238
+ `Array[App[json::value, K]]` — a SELF-REFERENCE. A naive
239
+ recursive reducer would infinite-loop.
240
+
241
+ Rigor's reducer carries an **in-progress stack** keyed on
242
+ `(uri, reduced_args)`. When evaluating an `AppRef` whose
243
+ `(uri, args)` matches something already on the stack, it
244
+ returns the in-progress `Type::App` carrier as-is — lazily,
245
+ without unfolding. The standard fix-point trick for recursive
246
+ type aliases.
247
+
248
+ So reducing `App[json::value, [String]]` produces:
249
+
250
+ ```
251
+ Union[ nil, true, false, Integer, Float, String,
252
+ Array[ Type::App[json::value, [String]] ], ← carrier left intact
253
+ Hash[ String, Type::App[json::value, [String]] ] ]
254
+ ```
255
+
256
+ The nested `Type::App` is a normal Rigor type; downstream
257
+ consumers (acceptance, narrowing, dispatch) handle it by
258
+ delegating to its `bound` (default `Dynamic[top]`). If they
259
+ need one more level of unfolding, they call
260
+ `app.reduce(env.hkt_registry)` again — but the typical
261
+ consumer doesn't need to.
262
+
263
+ A **fuel budget** (default 64 reduction steps per call-site
264
+ evaluation) bounds runaway expansion. Exhaustion unwinds to
265
+ `app.bound`.
266
+
267
+ ## What it doesn't do (yet)
268
+
269
+ Lightweight HKT is, well, lightweight. Conscious non-goals:
270
+
271
+ - **Pattern-matching with binder extraction**
272
+ (`E <: [:if, _, A, B] ? lisp_type[A] | lisp_type[B] : ...`).
273
+ The conditional grammar described above tests yes/no/maybe
274
+ but does not bind new type variables out of the pattern.
275
+ `rigor-lisp-eval` needs binder extraction for full
276
+ AST-shape discrimination; it stays on the diagnostic-emitter
277
+ path until pattern bindings land.
278
+ - **Multi-arg HKTs for non-recursive containers**
279
+ (`Result[T, E]` / `Maybe[T]`) — the registry supports
280
+ multi-arg URIs, but Rigor's existing carriers don't have
281
+ the sealed-union shape `Result` needs (ADR-3 amendment is
282
+ the gating piece).
283
+ - **Sugar syntax**. The explicit `%a{rigor:v1:hkt_register /
284
+ hkt_define}` pair is the canonical form. A recursive
285
+ `type alias` shorthand is a future option, gated on user
286
+ feedback that the explicit form is too verbose.
287
+ - **Plugin-side resolver hookup**. Plugins can't yet register
288
+ HKT URIs through their manifests; today only Rigor-bundled
289
+ registrations and user `.rbs` overlays populate the
290
+ registry.
291
+
292
+ If you hit one of these, ADR-20's § Implementation slicing
293
+ menu names the slice that addresses it.
294
+
295
+ ## Where to look in the code
296
+
297
+ | Layer | Location |
298
+ | --- | --- |
299
+ | Carrier | [`lib/rigor/type/app.rb`](../../lib/rigor/type/app.rb) |
300
+ | Registry value objects | [`lib/rigor/inference/hkt_registry.rb`](../../lib/rigor/inference/hkt_registry.rb) |
301
+ | Body tree node types | [`lib/rigor/inference/hkt_body.rb`](../../lib/rigor/inference/hkt_body.rb) |
302
+ | Reducer (lazy self-ref + fuel) | [`lib/rigor/inference/hkt_reducer.rb`](../../lib/rigor/inference/hkt_reducer.rb) |
303
+ | Body-string grammar parser | [`lib/rigor/inference/hkt_body_parser.rb`](../../lib/rigor/inference/hkt_body_parser.rb) |
304
+ | Directive parser (`hkt_register` / `hkt_define`) | [`lib/rigor/rbs_extended/hkt_directives.rb`](../../lib/rigor/rbs_extended/hkt_directives.rb) |
305
+ | Bundled `json::value` + `METHOD_RETURN_OVERRIDES` | [`lib/rigor/builtins/hkt_builtins.rb`](../../lib/rigor/builtins/hkt_builtins.rb) |
306
+ | Dispatcher tier | [`lib/rigor/inference/method_dispatcher.rb`](../../lib/rigor/inference/method_dispatcher.rb) (`try_hkt_builtin_return`) |
307
+ | Environment integration | [`lib/rigor/environment.rb`](../../lib/rigor/environment.rb) (`#hkt_registry` + `HktRegistryHolder`) |
308
+ | RBS scan | [`lib/rigor/environment/rbs_loader.rb`](../../lib/rigor/environment/rbs_loader.rb) (`each_class_decl_annotation`) |
309
+
310
+ ## What's next
311
+
312
+ If you came here from a "where does JSON.parse get its type
313
+ from?" question, the rest of the handbook covers the surrounding
314
+ machinery:
315
+
316
+ - [Chapter 2 — Everyday types](02-everyday-types.md) for the
317
+ carrier zoo the reducer outputs.
318
+ - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
319
+ for the broader annotation grammar (`%a{rigor:v1:return:}`,
320
+ `%a{rigor:v1:predicate-if-true}`, …) the HKT directives
321
+ sit alongside.
322
+ - [Appendix — Connections to type theory](appendix-type-theory.md)
323
+ § "What Rigor does NOT model" for the formal-type-theory
324
+ context that explains why Rigor adopted the lightweight
325
+ encoding rather than real HKT.
326
+
327
+ If you want to author your own overlay end-to-end, the
328
+ worked example in
329
+ [`spec/rigor/environment_spec.rb`](../../spec/rigor/environment_spec.rb)
330
+ ("ADR-20 HKT registry scan" context) is the smallest viable
331
+ reference — a fixture `.rbs` file with the directive pair, a
332
+ class declaration to anchor them on, and an `Environment.for_project`
333
+ call that surfaces the registration through `env.hkt_registry`.
@@ -0,0 +1,275 @@
1
+ # The Rigor Handbook
2
+
3
+ A walkthrough of Rigor's type model written for Ruby
4
+ programmers — no prior static-typing background assumed. Read
5
+ top to bottom for the first pass; come back to individual
6
+ chapters for reference once you know what you are looking for.
7
+
8
+ ## Who this is for
9
+
10
+ You write Ruby for a living, you have run into a `NoMethodError`
11
+ on `nil` more than once, and you want to know:
12
+
13
+ - What does `rigor check` actually look at?
14
+ - Why did it flag this expression — or, more often, why
15
+ didn't it flag the one I expected it to?
16
+ - When inference falls short, how do I push it further
17
+ without writing annotations all over my `.rb` files?
18
+
19
+ The handbook answers those questions. It does **not** try to
20
+ replace the [normative type
21
+ specification](../type-specification/README.md) — that lives
22
+ in `docs/type-specification/` and is the binding source when
23
+ this handbook disagrees.
24
+
25
+ Operational topics — installation, the CLI command reference,
26
+ configuration keys, baselines, CI — live in the
27
+ [User Manual](../manual/README.md). Reach for this handbook to
28
+ understand what a type *means*; reach for the manual to look
29
+ up the flag, key, or command that *acts* on it.
30
+
31
+ ## Table of contents
32
+
33
+ 1. [**Getting started**](01-getting-started.md) — running
34
+ `rigor check`, reading diagnostics, the "no annotations
35
+ needed" stance.
36
+ 2. [**Everyday types**](02-everyday-types.md) — the carrier
37
+ zoo. Constants, integer ranges, refinements, unions,
38
+ `Dynamic[top]`. The shortest path to "now I see what
39
+ Rigor sees."
40
+ 3. [**Narrowing**](03-narrowing.md) — how `if`, `case`, and
41
+ predicate methods sharpen a variable's type along the
42
+ branch.
43
+ 4. [**Tuples and hash shapes**](04-tuples-and-shapes.md) — the
44
+ structural carriers Ruby's `[a, b, c]` literals and
45
+ `{key: value}` hashes get when Rigor can prove their layout.
46
+ Includes the **shape-projection functions** (`pick_of` /
47
+ `omit_of` / `partial_of` / `required_of` / `readonly_of`)
48
+ that mirror TypeScript's `Pick` / `Omit` / `Partial` /
49
+ `Required` / `Readonly` utility types.
50
+ 5. [**Methods and blocks**](05-methods-and-blocks.md) — argument
51
+ typing, return-type inference, block parameters, arity.
52
+ 6. [**Classes**](06-classes.md) — instance-side vs class-side,
53
+ `self`, `attr_accessor`, `Data.define`.
54
+ 7. [**RBS and RBS::Extended**](07-rbs-and-extended.md) — when
55
+ inference cannot prove what the runtime actually returns,
56
+ how to nudge it through `.rbs` files and `%a{rigor:v1:…}`
57
+ directives.
58
+ 8. [**Understanding errors**](08-understanding-errors.md) —
59
+ the rule catalogue (`call.undefined-method`,
60
+ `call.argument-type-mismatch`, `flow.always-raises`, …),
61
+ severity profiles, and `# rigor:disable` suppression.
62
+ 9. [**Plugins**](09-plugins.md) — when to author one,
63
+ pointer to the [examples/](../../examples/README.md)
64
+ landing page.
65
+ 10. [**Coexisting with Sorbet**](10-sorbet.md) — for users
66
+ arriving from a Sorbet-using project: the
67
+ [`rigor-sorbet`](../../plugins/rigor-sorbet/) adapter
68
+ reads `sig { ... }` blocks, RBI files, and
69
+ `T.let` / `T.cast` / `T.must` / `T.unsafe` assertions
70
+ as type sources without rewriting in RBS.
71
+ 11. [**Generating RBS with rigor sig-gen**](11-sig-gen.md)
72
+ — emitting RBS from Rigor's inference results, the
73
+ `new-file` / `new-method` / `tighter-return`
74
+ classification model, the `--print` / `--diff` /
75
+ `--write` modes, the `--params` policy and ADR-5
76
+ trade-off, RSpec-aware observations.
77
+ 12. [**Lightweight HKT (JSON.parse and friends)**](12-lightweight-hkt.md)
78
+ — Rigor's defunctionalised higher-kinded type encoding
79
+ ([ADR-20](../adr/20-lightweight-hkt.md), Yallop & White
80
+ 2014 / fp-ts shape). Covers the bundled `json::value`
81
+ registration backing `JSON.parse` / `YAML.safe_load`,
82
+ the `symbolize_names: true` + `permitted_classes: [...]`
83
+ call-site discriminators, how to author your own URI
84
+ overlay in `.rbs`, the body grammar, the reducer's
85
+ lazy "tying-the-knot" handling for recursive sums, and
86
+ the conscious non-goals (no conditional bodies, no
87
+ multi-arg containers yet, no plugin manifest hookup).
88
+
89
+ ### Appendix — Coming from another type checker
90
+
91
+ A short cross-language reference for readers whose mental
92
+ model of "static type checker" was set by another tool.
93
+ Each page maps Rigor's vocabulary onto the concepts you
94
+ already know — type carriers, narrowing primitives,
95
+ configuration shape, severity model, suppression — and
96
+ calls out the places where the two systems make different
97
+ choices.
98
+
99
+ - [**Coming from TypeScript**](appendix-typescript.md) —
100
+ the structural-vs-nominal-with-refinements split, `unknown`
101
+ / `any` / `never` ↔ `Top` / `Dynamic[top]` / `Bot`,
102
+ type guards ↔ `predicate-if-true` directives, what
103
+ conditional / mapped types do not have a Rigor analogue.
104
+ - [**Coming from PHPStan**](appendix-phpstan.md) — the
105
+ closest peer in spirit. Identical refinement vocabulary
106
+ (`non-empty-string`, `int<min, max>`, `numeric-string`,
107
+ `literal-string`), `@phpstan-assert*` ↔ `RBS::Extended`,
108
+ Type-Specifying Extensions ↔ plugins, baseline diffing.
109
+ - [**Coming from mypy / Pyright**](appendix-mypy.md) — gradual
110
+ typing parallels, `Literal` ↔ `Constant`, `TypeGuard` /
111
+ `TypeIs` ↔ `predicate-if-true` / `predicate-if-false`,
112
+ `Protocol` ↔ RBS `interface`, `LiteralString` ↔
113
+ `literal-string`.
114
+ - [**Coming from Steep**](appendix-steep.md) — Ruby's other
115
+ RBS-driven static checker. Both consume the same `.rbs`
116
+ files; this page covers the layer each tool adds on top
117
+ and the coexistence pattern for projects that want to run
118
+ both.
119
+ - [**Coming from TypeProf**](appendix-typeprof.md) — Ruby's
120
+ official type *inference* tool. Both infer without
121
+ annotations; this page covers the whole-program-vs-local
122
+ analysis trade, why `rigor sig-gen` is the direct analogue
123
+ to the `typeprof` CLI, and the diagnostics-vs-RBS-output
124
+ split.
125
+ - [**Coming from Java or C#**](appendix-java-csharp.md) — the
126
+ two nominal, statically-typed languages share enough reflexes
127
+ (annotate-everything, generics, records, sealed
128
+ hierarchies, pattern-matching `switch`) to share one page.
129
+ Covers the inference-first vs annotate-first inversion,
130
+ records ↔ `Data.define`, C#'s `string?` / Java's
131
+ `Optional<T>` ↔ `T?`, declaration-site (C#) vs use-site
132
+ (Java) variance ↔ RBS, `dynamic` ↔ `Dynamic[top]`, and why
133
+ Rigor reports *unreachable* `case` clauses rather than
134
+ enforcing sealed-type exhaustiveness.
135
+ - [**Coming from Rust**](appendix-rust.md) — the sum-type and
136
+ exhaustive-`match` peer. `Option<T>` ↔ `T?`, `Result<T, E>`
137
+ ↔ Ruby's raising model, `enum` variants ↔ a union of
138
+ `Data.define`, `match` ↔ `case`/`in` with the
139
+ enforce-totality vs report-unreachable inversion, traits
140
+ (nominal, coherent) ↔ RBS structural interfaces, and
141
+ refinements ↔ the newtype pattern. Ownership is a noted
142
+ non-goal.
143
+ - [**Coming from Go**](appendix-go.md) — the structural-typing
144
+ cousin. Go's implicitly satisfied `interface` *is* Rigor's
145
+ RBS interface, so the feature Java/C# readers stumble on is
146
+ the one a Go reader already has by reflex. Covers
147
+ `interface{}` / `any` ↔ `Dynamic[top]`, type switch ↔
148
+ `case`/`in`, errors-as-values ↔ raising, and the unions /
149
+ literal types / refinements Go does not have.
150
+ - [**Coming from Elixir**](appendix-elixir.md) — the closest
151
+ *philosophical* match: dynamic origin, gradual typing, and a
152
+ success-typing / no-false-positives stance shared with
153
+ Dialyzer. Covers pattern matching + guards ↔ narrowing (the
154
+ `flow.unreachable-clause` rule was modelled on Elixir's
155
+ clause-reachability work, [ADR-47](../adr/47-narrowing-driven-clause-reachability.md)),
156
+ set-theoretic types ↔ Rigor's unions + `~T` / `T - U`
157
+ operators, atoms ↔ symbols, and protocols / behaviours ↔
158
+ structural interfaces.
159
+
160
+ ### Appendix — Protocols and structural typing
161
+
162
+ A standalone concept page for the question this handbook gets
163
+ from Python and Swift readers alike: *"where is Rigor's
164
+ `Protocol`?"* It untangles the one word that means two
165
+ unrelated things in Rigor — the **interface** (RBS structural
166
+ type, the Python `typing.Protocol` analogue) and the
167
+ **protocol contract** (ADR-28's path-scoped behavioural
168
+ contract) — so you reach for the right one.
169
+
170
+ - [**Protocols, interfaces, and structural typing**](appendix-protocols-and-structural-typing.md)
171
+ — RBS `interface` ↔ `typing.Protocol`, inferred object
172
+ shapes and capability roles, and how all of that differs
173
+ from the plugin-declared, path-scoped protocol contracts of
174
+ [ADR-28](../adr/28-path-scoped-protocol-contracts.md).
175
+ Includes the side-by-side "interface vs protocol contract"
176
+ table and a "which one do I want?" guide.
177
+
178
+ ### Appendix — Connections to type theory
179
+
180
+ A short bridge between Rigor's vocabulary and the formal
181
+ type-theoretic concepts you may have seen in a programming-
182
+ languages textbook or in another type checker's documentation.
183
+ Read this if you came in from a "where does Rigor sit in the
184
+ type-theory landscape" question; the handbook proper stays
185
+ deliberately short on theory.
186
+
187
+ - [**Connections to type theory**](appendix-type-theory.md) —
188
+ the type lattice, subtyping vs gradual consistency, nominal
189
+ vs structural, the polymorphism family (parametric / subtype
190
+ / ad-hoc), variance, refinement / predicate subtyping,
191
+ occurrence typing, gradual typing, effect systems, the
192
+ soundness vs completeness trade-off, and a short list of
193
+ features Rigor deliberately does not model (HKT,
194
+ higher-rank, full dependent types, …) — each with the
195
+ matching Rigor surface and a pointer into the spec corpus.
196
+ - [**The Liskov Substitution Principle**](appendix-liskov.md) —
197
+ why LSP is a *behavioural* discipline that applies to Ruby
198
+ *more* than to a statically-checked language (not less — the
199
+ "Ruby isn't statically typed so LSP is optional" claim gets
200
+ the principle backwards), how Rigor's robustness principle
201
+ (strict returns, lenient parameters) re-derives the LSP
202
+ signature rule (covariant returns, contravariant parameters)
203
+ from Ruby-adoption ergonomics rather than substitutability
204
+ proofs, why that convergence means Rigor's defaults do not
205
+ fight duck typing or the "L" of SOLID, and which
206
+ behavioral-subtyping obligations (cross-hierarchy override
207
+ compatibility, exception rules, Design-by-Contract, the
208
+ history constraint) Rigor does *not* statically enforce. On
209
+ this page only, "LSP" means Liskov — not the Language Server.
210
+
211
+ ## How to read this handbook
212
+
213
+ Each chapter is short on theory and long on examples. Every
214
+ example is real Ruby that runs under MRI as written; the
215
+ prose around it is what `rigor check` would say about that
216
+ code.
217
+
218
+ When you see an `assert_type(...)` line in a snippet, that is
219
+ Rigor's introspection helper, not a runtime check — it pins
220
+ the inferred type at that program point so you can compare
221
+ the prose to the actual analyzer output. `dump_type(...)` is
222
+ the same idea but emits a notice instead of failing on
223
+ mismatch.
224
+
225
+ Snippet conventions:
226
+
227
+ ```ruby
228
+ n = 1 + 2
229
+ assert_type("3", n) # Rigor folds the literal sum
230
+ ```
231
+
232
+ means: at the `assert_type` call, Rigor's inference for `n` is
233
+ `3` — the folded literal value.
234
+
235
+ Wording convention — **"interface"**: Ruby has no `interface`
236
+ keyword, so most readers import the word's meaning from another
237
+ language, and the Java / PHP *nominal* sense (a class declares
238
+ `implements`) dominates. Rigor's RBS `interface` is the opposite —
239
+ *structural*, like Go's `interface` or Python's `Protocol`, satisfied
240
+ by having the methods with no declaration. To avoid the misread,
241
+ qualify the word on first use in any chapter as **"structural
242
+ interface"** or **"RBS interface,"** never the bare "interface." The
243
+ [Protocols and structural typing appendix](appendix-protocols-and-structural-typing.md)
244
+ is the canonical explainer.
245
+
246
+ When a chapter references a more formal document, the link
247
+ takes you out of the handbook into the binding spec corpus or
248
+ ADRs:
249
+
250
+ - [`docs/types.md`](../types.md) — one-page mental model.
251
+ - [`docs/type-specification/`](../type-specification/README.md)
252
+ — normative spec corpus.
253
+ - [`docs/internal-spec/`](../internal-spec/README.md) —
254
+ analyzer-internal contracts (engine surface, type-object
255
+ public API).
256
+ - [`docs/adr/`](../adr/) — architecture decision records.
257
+
258
+ ## Non-goals
259
+
260
+ The handbook is meant to be readable cover-to-cover in a few
261
+ hours. To keep it short:
262
+
263
+ - It does **not** introduce Ruby itself. `def`, `class`,
264
+ blocks, modules, `attr_*`, regex, RBS basics — all assumed.
265
+ - It does **not** cover every edge case. Edge cases live in
266
+ the spec corpus.
267
+ - It does **not** discuss internal contracts (engine surface,
268
+ type-object public API). Those live in
269
+ [`docs/internal-spec/`](../internal-spec/README.md).
270
+ - It does **not** cover plugin **authoring** — that is the
271
+ job of [examples/](../../examples/README.md). Chapter 9 is
272
+ a one-page pointer.
273
+
274
+ If a topic comes up that the handbook does not explain, the
275
+ relevant spec document is one click away.