rigortype 0.1.7 → 0.1.9

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -513
  3. data/lib/rigor/analysis/check_rules.rb +23 -1
  4. data/lib/rigor/analysis/diagnostic.rb +17 -3
  5. data/lib/rigor/analysis/runner.rb +178 -3
  6. data/lib/rigor/analysis/worker_session.rb +14 -3
  7. data/lib/rigor/cli/annotate_command.rb +224 -0
  8. data/lib/rigor/cli/baseline_command.rb +36 -16
  9. data/lib/rigor/cli/prism_colorizer.rb +111 -0
  10. data/lib/rigor/cli/triage_command.rb +83 -0
  11. data/lib/rigor/cli/triage_renderer.rb +77 -0
  12. data/lib/rigor/cli.rb +71 -5
  13. data/lib/rigor/environment.rb +9 -1
  14. data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
  15. data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
  16. data/lib/rigor/inference/expression_typer.rb +300 -18
  17. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
  18. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
  19. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
  20. data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
  21. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
  22. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +33 -8
  23. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
  24. data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
  25. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
  26. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
  27. data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
  28. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
  29. data/lib/rigor/inference/method_dispatcher.rb +179 -4
  30. data/lib/rigor/inference/method_parameter_binder.rb +67 -10
  31. data/lib/rigor/inference/narrowing.rb +29 -10
  32. data/lib/rigor/inference/scope_indexer.rb +156 -6
  33. data/lib/rigor/inference/statement_evaluator.rb +43 -21
  34. data/lib/rigor/plugin/base.rb +39 -0
  35. data/lib/rigor/plugin/loader.rb +22 -1
  36. data/lib/rigor/plugin/manifest.rb +73 -10
  37. data/lib/rigor/plugin/protocol_contract.rb +185 -0
  38. data/lib/rigor/plugin/registry.rb +66 -0
  39. data/lib/rigor/scope.rb +46 -0
  40. data/lib/rigor/triage/catalogue.rb +296 -0
  41. data/lib/rigor/triage/hint.rb +27 -0
  42. data/lib/rigor/triage.rb +89 -0
  43. data/lib/rigor/type/constant.rb +29 -2
  44. data/lib/rigor/version.rb +1 -1
  45. data/sig/rigor/inference.rbs +1 -0
  46. data/sig/rigor/scope.rbs +6 -0
  47. metadata +16 -1
data/README.md CHANGED
@@ -4,106 +4,114 @@
4
4
  [![GitHub License](https://img.shields.io/github/license/rigortype/rigor)](https://github.com/rigortype/rigor/blob/master/LICENSE)
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rigortype/rigor)
6
6
 
7
- **Inference-first static analysis for Ruby.** Add Rigor to your
8
- Gemfile and run `rigor check` over your code — no annotations,
9
- no runtime dependency on the analyzer, no DSL.
10
-
11
- Rigor parses Ruby with [Prism](https://github.com/ruby/prism),
12
- runs a flow-sensitive type-inference engine over each file,
13
- consults RBS signatures and the project's own `sig/` directory
14
- for any class it can find, and reports a small but trustworthy
15
- catalogue of bugs (undefined methods on typed receivers, wrong
16
- positional arity, provable `Integer / 0`, …).
17
-
18
- **Two design commitments drive Rigor.**
19
-
20
- 1. **Types are facts, not wishes.** Hand-written type
21
- annotations drift from the implementation the moment they
22
- are written. Rigor infers from the code itself — every
23
- carrier in its type vocabulary is derived from what your
24
- source actually produces, not from a signature you authored
25
- and might forget to update. When you do want RBS in
26
- `sig/`, [`rigor sig-gen`](docs/adr/14-rbs-sig-generation.md)
27
- emits it from inference results so the written form starts
28
- in sync with reality, and `tighter-return` candidates flag
29
- the cases where an existing `.rbs` is already weaker than
30
- what the implementation provably returns.
31
- 2. **Programmable inference beyond unions.** A plain union
32
- (`Integer | nil`) is not the type story Ruby needs. Rigor
33
- reasons about *what values an expression actually
34
- produces* literal values, integer ranges, refinement
35
- carriers, per-position tuple / hash shapes, bound-method
36
- bindings and exposes a plugin extension API plus an
37
- [ADR-16](docs/adr/16-macro-expansion.md) macro / DSL
38
- expansion substrate so Rails-shape DSLs are first-class
39
- type sources rather than analysis blind spots.
40
-
41
- See **[Beyond `Integer` and `String`](#beyond-integer-and-string-rigors-richer-type-vocabulary)**
42
- for the full type-model story; the carrier-zoo table is the
43
- short pitch.
44
-
45
- When you want tighter types than RBS expresses, refine them
46
- through the
47
- [`RBS::Extended`](docs/type-specification/rbs-extended.md)
48
- annotation surface — `rigor:v1:return:` /
49
- `rigor:v1:param:` / `rigor:v1:assert` directives accept the
50
- imported-built-in refinement names (`non-empty-string`,
51
- `positive-int`, `non-empty-array[Integer]`, `int<5, 10>`,
52
- `literal-string`, `non-lowercase-string`, …) without changing
53
- the underlying RBS.
7
+ **Inference-first static analysis for Ruby.** Run `rigor check` over your
8
+ code no annotations, no runtime dependency, no DSL.
9
+
10
+ Rigor parses Ruby with [Prism](https://github.com/ruby/prism), runs a
11
+ flow-sensitive type-inference engine over each file, and reports a small
12
+ but trustworthy catalogue of bugs: undefined methods on typed receivers,
13
+ wrong-argument-count calls, provable divisions by zero, and more.
14
+
15
+ **Your team never needs to write type annotations** — Rigor derives
16
+ everything from the code itself. An AI Skill gets a project running in
17
+ minutes with a configuration tailored to its stack. When you want to go
18
+ deeper tighter checks, framework-aware analysis, your own refinements —
19
+ Rigor's type system is remarkably powerful, and a Skill takes you there
20
+ step-by-step.
21
+
22
+ **Three design commitments drive Rigor.**
23
+
24
+ 1. **Types are facts, not wishes.** Hand-written annotations drift the
25
+ moment they are written. Rigor infers from the code itself — every
26
+ type is derived from what your source actually produces. When you do
27
+ want RBS in `sig/`, [`rigor sig-gen`](https://rigor.typedduck.fail/reference/adr/14-rbs-sig-generation/)
28
+ emits it from inference results so the written form starts in sync
29
+ with reality.
30
+ 2. **Your specs are types.** Do you really need to write type
31
+ annotations? Your `spec/` is already type information. RSpec
32
+ `expect(x).to be_a(T)` / `eq(literal)` assertions contribute
33
+ narrowing facts from that point forward; `subject` / `let` bindings
34
+ propagate the SUT's type into downstream `it` bodies; factory
35
+ definitions in `spec/factories/` feed attribute shapes back into
36
+ `rigor-activerecord` and `rigor-factorybot`'s cross-plugin channels.
37
+ The test suite you already have becomes a live type oracle.
38
+ 3. **Programmable inference beyond unions.** A plain union
39
+ (`Integer | nil`) is not the type story Ruby needs. Rigor reasons
40
+ about *what values an expression actually produces* — literal values,
41
+ integer ranges, refinement carriers, per-position tuple / hash shapes
42
+ and exposes a plugin API plus a
43
+ [macro / DSL expansion substrate](https://rigor.typedduck.fail/reference/adr/16-macro-expansion/)
44
+ so Rails-shape DSLs become first-class type sources.
54
45
 
55
46
  ## Installation
56
47
 
57
- Add the gem to your application's Gemfile (development group is
58
- typical Rigor is a static analyzer, not a runtime dependency):
48
+ Rigor is a tool, not a library install it independently, **not** in
49
+ your project's `Gemfile`. It runs on Ruby 4.0, regardless of which Ruby
50
+ version your project targets.
59
51
 
60
- ```ruby
61
- group :development do
62
- gem "rigortype", require: false
63
- end
64
- ```
65
-
66
- Install:
52
+ The recommended setup uses [`mise`](https://mise.jdx.dev/), which
53
+ provisions both Ruby 4.0 and Rigor pinned per project:
67
54
 
68
55
  ```sh
69
- bundle install
56
+ mise use ruby@4.0
57
+ mise use gem:rigortype
70
58
  ```
71
59
 
72
- Or, for a one-off install outside Bundler:
60
+ If you already have Ruby 4.0 available, `gem install rigortype` works too.
61
+ The gem is named `rigortype` (the name `rigor` was taken on RubyGems);
62
+ the executable it installs is `rigor`.
63
+
64
+ Full options — `asdf`, dev containers, CI workflow template — are in the
65
+ [installation guide](https://rigor.typedduck.fail/reference/manual/01-installation/)
66
+ and [CI guide](https://rigor.typedduck.fail/reference/manual/10-ci/).
67
+
68
+ ## Getting started with AI Skills
69
+
70
+ The fastest path from zero to a running Rigor setup is the
71
+ **`rigor-project-init` Skill** — an AI agent skill bundled under
72
+ [`skills/`](skills/) that detects your stack, recommends the right plugins,
73
+ and writes `.rigor.dist.yml` for you:
73
74
 
74
- ```sh
75
- gem install rigortype
75
+ ```
76
+ # In Claude Code or any AI assistant that supports Agent Skills:
77
+ Use the rigor-project-init skill
76
78
  ```
77
79
 
78
- The gem ships an executable named `rigor` (gem name is
79
- `rigortype` because `rigor` was already taken on RubyGems).
80
+ The Skill walks your `Gemfile`, proposes a plugin set matched to your
81
+ framework (Rails, Sinatra, dry-rb, …), lets you choose an adoption mode —
82
+ **baseline** (acknowledge existing diagnostics and work them down
83
+ incrementally) or **strict** (zero-diagnostic gate from day one) — then
84
+ commits a ready-to-use configuration. No manual YAML editing required.
80
85
 
81
- **Ruby version.** The gemspec requires `>= 4.0.0, < 4.1`.
86
+ Two companion Skills continue the journey once you are up and running:
82
87
 
83
- ## Quick start
88
+ | Skill | Use when |
89
+ | --- | --- |
90
+ | [`rigor-baseline-reduce`](skills/rigor-baseline-reduce/) | Reducing an existing baseline: `rigor triage` prioritisation → site-by-site classification → fix / `# rigor:disable` / open a Rigor issue |
91
+ | [`rigor-plugin-author`](skills/rigor-plugin-author/) | Teaching Rigor about a project-specific DSL or framework — authors the plugin gem or project-private plugin |
84
92
 
85
- Drop into your project root and run the canonical commands:
93
+ All three Skills follow the [agentskills.io](https://agentskills.io/)
94
+ convention and work with any AI assistant that discovers skills in your
95
+ project directory.
96
+
97
+ ## Quick start
86
98
 
87
99
  ```sh
88
- # Diagnose unknown methods, wrong-arity calls, and other
89
- # rule-driven bugs across `lib/`.
90
- bundle exec rigor check lib
100
+ # Check lib/ for bugs.
101
+ rigor check lib
91
102
 
92
- # Drop a starter .rigor.yml into the project root.
93
- bundle exec rigor init
103
+ # Drop a starter .rigor.yml.
104
+ rigor init
94
105
 
95
106
  # Print the inferred type at a precise FILE:LINE:COL position.
96
- bundle exec rigor type-of lib/foo.rb:10:5
107
+ rigor type-of lib/foo.rb:10:5
97
108
 
98
- # Report Scope#type_of coverage across a tree (handy when
99
- # diagnosing why a particular call site reads as `untyped`).
100
- bundle exec rigor type-scan lib
109
+ # Summarise the diagnostic stream: distribution, hotspots, heuristic hints.
110
+ rigor triage lib
101
111
 
102
- # Emit RBS skeletons from inference results — review with
103
- # `--diff`, write to `sig/` with `--write`. ADR-14 sig-gen.
104
- bundle exec rigor sig-gen --print lib/foo.rb
105
- bundle exec rigor sig-gen --diff lib/foo.rb
106
- bundle exec rigor sig-gen --write lib/foo.rb
112
+ # Emit RBS from inference results — review with --diff, write with --write.
113
+ rigor sig-gen --diff lib/foo.rb
114
+ rigor sig-gen --write lib/foo.rb
107
115
  ```
108
116
 
109
117
  ### Sample output
@@ -113,482 +121,147 @@ $ cat /tmp/demo.rb
113
121
  "hello".no_such_method # undefined method
114
122
  [1, 2, 3].rotate(1, 2) # wrong number of arguments
115
123
 
116
- $ bundle exec rigor check /tmp/demo.rb
124
+ $ rigor check /tmp/demo.rb
117
125
  /tmp/demo.rb:1:9: error: undefined method `no_such_method' for "hello"
118
126
  /tmp/demo.rb:2:11: error: wrong number of arguments to `rotate' on Array (given 2, expected 0..1)
119
127
  ```
120
128
 
121
- The rule catalogue is **deliberately conservative**: a
122
- diagnostic fires only when the receiver type is statically
123
- known and the method set on that class is enumerable through
124
- RBS or in-source `def` / `define_method` discovery. Implicit-
125
- self calls, dynamic receivers, and constant-decl alias classes
126
- (e.g. `YAML` → `Psych`) are skipped to avoid false positives.
129
+ Diagnostics fire only when the receiver type is statically known and the
130
+ evidence is conclusive Rigor never surfaces a warning it cannot prove.
127
131
 
128
132
  ### Faster runs through the cache
129
133
 
130
- Rigor caches expensive RBS work (the loaded `RBS::Environment`,
131
- constant-type translation, class hierarchy, type-parameter
132
- names, known-class set) under `.rigor/cache/` so the second
133
- `rigor check` is significantly faster than the first. The cache
134
- is keyed by your project's `.rbs` file digests + the locked
135
- `rbs` gem version, so a signature change or a gem upgrade
136
- invalidates exactly what it should.
134
+ Rigor caches RBS work (loaded environment, type translation, class
135
+ hierarchy) under `.rigor/cache/` the second `rigor check` is
136
+ significantly faster than the first. Add `.rigor/` to your `.gitignore`.
137
137
 
138
138
  ```sh
139
- # Inspect what is cached on disk and what this run did.
140
- bundle exec rigor check --cache-stats lib
141
-
142
- # Wipe the cache (do this if you suspect staleness).
143
- bundle exec rigor check --clear-cache lib
144
-
145
- # Run with caching disabled.
146
- bundle exec rigor check --no-cache lib
139
+ rigor check --cache-stats lib # inspect cache hit/miss
140
+ rigor check --clear-cache lib # wipe if you suspect staleness
147
141
  ```
148
142
 
149
- Add `.rigor/` to your `.gitignore` — the cache is per-checkout
150
- and contains nothing reproducible to share.
151
-
152
- ## Beyond `Integer` and `String`: Rigor's richer type vocabulary
143
+ ## The type vocabulary
153
144
 
154
- A vanilla static checker answers "what *class* is this object?"
155
- Rigor answers a much narrower question: "what *subset of values*
156
- can this expression actually produce?" That distinction is the
157
- whole point of Rigor types like `Integer` and `String` describe
158
- classes, but real-world code carries far more structure (a count
159
- that's always non-negative, a name that's never empty, a flag
160
- that's one of three Symbols). Rigor reasons about that structure
161
- out of the box, without you writing a single annotation.
162
-
163
- ### The carrier zoo
145
+ A vanilla type checker answers "what *class* is this object?" Rigor
146
+ answers "what *subset of values* can this expression produce?" — tracking
147
+ literal values, integer ranges, refinement carriers, and structural shapes
148
+ through the whole analysis, without a single annotation.
164
149
 
165
150
  | Carrier | What it records | Example |
166
151
  | --- | --- | --- |
167
- | **Literal types** (`Type::Constant`) | A single Ruby value | `Constant<42>`, `Constant<"hello">`, `Constant<:foo>` |
168
- | **Integer ranges** (`Type::IntegerRange`) | A bounded integer interval `int<a, b>` | `positive-int = int<1, max>`, `int<5, 10>` |
169
- | **Refinement types** — split into two halves: `Type::Difference` and `Type::Refined` | A base nominal minus a single value, or a base nominal restricted by a predicate | `non-empty-string = String - ""`, `lowercase-string = String & lowercase?`, `literal-string` |
170
- | **Intersection** (`Type::Intersection`) | Composition of multiple refinements | `non-empty-lowercase-string = non-empty-string lowercase-string` |
171
- | **Tuple / HashShape** | Heterogeneous arrays / known-key hashes that carry per-position / per-key types | `[1, "two", :three]` types as `Tuple[Constant<1>, Constant<"two">, Constant<:three>]`; `{name: "Alice", age: 30}` as `HashShape{name: Constant<"Alice">, age: Constant<30>}` |
172
- | **Union** (`Type::Union`) | "One of these literal values" finite enums Rigor can enumerate | `Constant<:zero> \| Constant<:small> \| Constant<:large>` |
173
- | **`Method` binding** (`Type::BoundMethod`) | The receiver / method-name pair `Object#method(:sym)` produces, so `.call` / `.()` / `[]` recover the precise backing dispatch | `"1".method(:to_i).call` resolves to `Constant<1>` instead of `untyped` |
174
- | **`Dynamic[T]`** | The gradual carrier wraps a static facet with a "could be anything" admission | `Dynamic[Top]` is the conservative fallback Rigor uses when it cannot prove a narrower type |
175
-
176
- Each refinement / range / literal carrier **erases to its base
177
- class** for ordinary RBS interop, so importing Rigor is a
178
- strictly additive change: a method whose RBS sig says
179
- `-> String` keeps that contract, and Rigor's narrower inference
180
- just sits on top.
181
-
182
- ### What this buys you in practice
183
-
184
- ```ruby
185
- # Rigor doesn't just see "Integer", it sees "non-negative integer".
186
- n = ARGV.size # int<0, max> (non-negative-int)
187
- m = n + 1 # int<1, max> (positive-int)
188
- m.zero? # Constant<false> — proven; the
189
- # branch elision can drop the `else`
190
-
191
- # String composition stays as precise as the inputs allow.
192
- greeting = "Hello, " # Constant<"Hello, ">
193
- name = ARGV.first # String? — RBS-declared
194
- hello = "Hello, #{name}!" # literal-string — every part is
195
- # literal-bearing, so the result is
196
- # provably source-derived.
197
-
198
- # Tuple-shaped destructuring stays per-position.
199
- first, _middle, last = [10, 20, 30]
200
- first # Constant<10>
201
- last # Constant<30>
202
-
203
- # Constant folding through user methods.
204
- def is_odd(n) = n.odd?
205
- is_odd(3) # Constant<true> — folded through
206
- # the body, not just typed as `bool`
207
-
208
- # Case/when narrowing produces a literal-set Union.
209
- label = case n
210
- when 0 then :zero
211
- when 1..9 then :small
212
- else :large
213
- end
214
- label # Constant<:zero> | Constant<:small>
215
- # | Constant<:large>
216
-
217
- # Method bindings keep their receiver — `.method(:sym).call`
218
- # round-trips through the original dispatch.
219
- [:to_i, :to_f, :to_sym].map { |m| "1".method(m).call }
220
- # Tuple[Constant<1>, Constant<1.0>, Constant<:"1">]
221
- # — per-element fold + BoundMethod backward fold
222
-
223
- # RBS::Extended directives let you tighten beyond what RBS expresses.
224
- class Slug
225
- %a{rigor:v1:return: non-empty-string}
226
- def normalise: (::String id) -> ::String
227
- end
228
- Slug.new.normalise("foo").size # positive-int — provably ≥ 1
229
- ```
152
+ | **Literal** (`Constant`) | A single Ruby value | `Constant<42>`, `Constant<"hello">`, `Constant<:foo>` |
153
+ | **Integer range** | A bounded interval | `positive-int = int<1, max>`, `int<5, 10>` |
154
+ | **Refinement** | Base type minus a point, or restricted by a predicate | `non-empty-string = String - ""`, `lowercase-string` |
155
+ | **Tuple / HashShape** | Heterogeneous arrays / known-key hashes | `[1, "two"]` `Tuple[Constant<1>, Constant<"two">]` |
156
+ | **Union** | Finite enumerable set of literals | `Constant<:zero> \| Constant<:small> \| Constant<:large>` |
157
+ | **`Dynamic[T]`** | Gradual carrier "could be anything" | `Dynamic[Top]` when proof is unavailable |
158
+
159
+ Every narrower carrier **erases to its base class** for RBS interop, so
160
+ importing Rigor is a strictly additive change.
161
+
162
+ The full type model constant folding, `Method` bindings, LightweightHKT,
163
+ `RBS::Extended` directives is in the
164
+ [handbook](https://rigor.typedduck.fail/reference/handbook/) and
165
+ [type specification](https://rigor.typedduck.fail/reference/type-specification/).
166
+
167
+ ### Unlocking tighter types with `RBS::Extended`
230
168
 
231
- Rigor never invents these answers every narrower carrier is
232
- derived from literals in the source, control-flow narrowing
233
- (`is_a?`, `nil?`, `==` against finite literal sets, integer
234
- comparisons), per-class catalogues for the bundled built-ins,
235
- or `RBS::Extended` directives the user opted into. When the
236
- inference cannot prove a value is in a narrower carrier, it
237
- stays at the wider one (or `Dynamic[Top]`) and Rigor stays
238
- silent — diagnostics fire only when the narrow type is
239
- genuinely proved.
240
-
241
- ### Where the type model is documented
242
-
243
- - **End-user handbook** — chapter-by-chapter walkthrough of
244
- the type model written for Ruby programmers without prior
245
- static-typing background:
246
- [`docs/handbook/`](docs/handbook/README.md). Start here if
247
- you want a guided tour of how Rigor sees your code rather
248
- than a spec deep-dive.
249
- - One-page mental model:
250
- [`docs/types.md`](docs/types.md).
251
- - Binding spec corpus:
252
- [`docs/type-specification/`](docs/type-specification/README.md).
253
- - Imported refinement names (kebab-case catalogue):
254
- [`docs/type-specification/imported-built-in-types.md`](docs/type-specification/imported-built-in-types.md).
255
- - The `RBS::Extended` annotation grammar that opens this
256
- vocabulary up to your own RBS:
257
- [`docs/type-specification/rbs-extended.md`](docs/type-specification/rbs-extended.md).
258
-
259
- ## How Rigor finds your types
260
-
261
- Rigor consults, in order:
262
-
263
- 1. **In-source RBS.** If your project has a `sig/` directory,
264
- Rigor auto-loads it. `rigor init` writes a `.rigor.yml`
265
- that points at `sig/` by default.
266
- 2. **Bundled RBS core + stdlib.** Pathname, OptParse, JSON,
267
- YAML, etc. ship with the analyzer.
268
- 3. **Gem RBS.** RBS files vendored with installed gems
269
- (Prism's own `.rbs`, the `rbs` gem's, …).
270
- 4. **In-source class discovery.** When no RBS is available,
271
- Rigor walks `def` / `define_method` / `attr_*` /
272
- `Data.define(*Symbol)` so user-defined methods on a class
273
- are recognised.
274
- 5. **Opt-in gem-source inference (ADR-10).** Gems listed
275
- under `dependencies.source_inference:` in `.rigor.yml`
276
- have their `lib/` walked the same way project source is,
277
- so methods on those gems' classes resolve even without
278
- RBS. Inferred returns crossing the gem boundary are
279
- wrapped in `Dynamic[T]` so the call site retains the
280
- provenance — RBS / RBS::Inline / generated stubs / plugin
281
- contracts always win on conflict. Default behaviour is
282
- unchanged: gems not listed stay at the
283
- RBS-or-`Dynamic[Top]` boundary.
284
-
285
- If a type cannot be proved, the engine returns `Dynamic[Top]`
286
- (Rigor's gradual carrier) and stays silent — Rigor never invents
287
- diagnostics it cannot prove.
288
-
289
- ## Refining types through `RBS::Extended`
290
-
291
- When the RBS-declared type is too wide, attach a
292
- `%a{rigor:v1:…}` annotation to the relevant method in your
293
- `sig/` file. The annotation is a no-op for ordinary RBS tools
294
- and a tightening signal for Rigor.
169
+ When you *want* to express more than RBS allows, attach a
170
+ `%a{rigor:v1:…}` annotation in your `sig/` file. The annotation is a
171
+ no-op for ordinary RBS tools; Rigor uses it to tighten call-site and
172
+ body types.
295
173
 
296
174
  ```rbs
297
175
  class Slug
298
- # The runtime always returns a non-empty string. The override
299
- # tightens the call-site result to non-empty-string and tells
300
- # the body's `assert_type` that `id` cannot be "".
301
176
  %a{rigor:v1:return: non-empty-string}
302
177
  %a{rigor:v1:param: id is non-empty-string}
303
178
  def normalise: (::String id) -> ::String
304
179
  end
305
180
  ```
306
181
 
307
- Right-hand side accepts:
308
-
309
- - **RBS class names** — `String`, `::Foo::Bar` (with optional
310
- `~T` negation for `assert` / `predicate-if-*`).
311
- - **Imported-built-in refinement names** (kebab-case):
312
- - Point-removal — `non-empty-string`, `non-zero-int`,
313
- `non-empty-array[T]`, `non-empty-hash[K, V]`.
314
- - IntegerRange aliases — `positive-int`, `non-negative-int`,
315
- `negative-int`, `non-positive-int`, `int<min, max>`.
316
- - Predicate refinements — `lowercase-string`,
317
- `uppercase-string`, `numeric-string`, `decimal-int-string`,
318
- `octal-int-string`, `hex-int-string`.
319
- - Paired complements (`~T`-symmetric) —
320
- `non-lowercase-string`, `non-uppercase-string`,
321
- `non-numeric-string`. Writing `~lowercase-string` narrows
322
- `String` to `non-lowercase-string` instead of the generic
323
- `Difference[String, lowercase-string]` fallback.
324
- - Composed shapes — `non-empty-lowercase-string`,
325
- `non-empty-uppercase-string`, `non-empty-literal-string`.
326
- - Flow-tracked source-literal — `literal-string`. Rigor lifts
327
- `"hi #{name}!"`, `"a" + literal_str`, and `literal_str * 3`
328
- to `literal-string` when every operand is itself
329
- literal-bearing.
330
-
331
- The full directive table is in
332
- [`docs/type-specification/rbs-extended.md`](docs/type-specification/rbs-extended.md);
333
- the catalogue of refinement names is in
334
- [`docs/type-specification/imported-built-in-types.md`](docs/type-specification/imported-built-in-types.md).
335
-
336
- ### Example: argument-type-mismatch caught at the call site
182
+ This is entirely optional — Rigor runs without any annotations.
183
+ The directive grammar and the full refinement-name catalogue are in
184
+ [`RBS::Extended`](https://rigor.typedduck.fail/reference/type-specification/rbs-extended/)
185
+ and [imported built-in types](https://rigor.typedduck.fail/reference/type-specification/imported-built-in-types/).
337
186
 
338
- ```rbs
339
- # sig/normaliser.rbs
340
- class Normaliser
341
- %a{rigor:v1:param: id is non-empty-string}
342
- def normalise: (::String id) -> ::String
343
- end
344
- ```
187
+ ## Plugins
345
188
 
346
- ```ruby
347
- # app/normaliser.rb
348
- class Normaliser
349
- def normalise(id)
350
- id.upcase
351
- end
352
- end
189
+ Production plugins ship under [`plugins/`](plugins/) for the most common
190
+ Ruby frameworks and gems. Activate them in `.rigor.yml`:
353
191
 
354
- n = Normaliser.new
355
- n.normalise("hello") # OK
356
- n.normalise("") # rigor flags: argument type mismatch
192
+ ```yaml
193
+ plugins:
194
+ - rigor-activerecord
195
+ - rigor-actionpack
196
+ - rigor-rspec
357
197
  ```
358
198
 
359
- `rigor check` reports the second call as an
360
- `argument-type-mismatch` because the literal `""` does not
361
- satisfy `non-empty-string`. Inside the method body, Rigor also
362
- sees `id` as `non-empty-string` (so `id.empty?` reduces to
363
- `Constant[false]` and `id.size` reduces to `positive-int`).
364
-
365
- ## What rigor sees today
366
-
367
- - **Local / instance / class / global variables** —
368
- intra-method bindings, cross-method ivar / cvar
369
- accumulators, program-wide globals, and compound writes
370
- (`||=`, `&&=`, `+=`).
371
- - **`self` typing and constant lookup** — class and method
372
- body boundaries inject `Singleton[T]` / `Nominal[T]`;
373
- lexical constant resolution walks RBS-core, common stdlib,
374
- in-source class discovery, and in-source constant-value
375
- tracking (`BUCKETS = [:a, :b]; BUCKETS.first` →
376
- `Constant[:a]`).
377
- - **Predicate narrowing** — truthiness, `nil?`, `is_a?` /
378
- `kind_of?` / `instance_of?`, finite-literal equality,
379
- case-equality (`===`) for Class / Module / Range / Regexp,
380
- `case` / `when` integration. Paired-complement narrowing for
381
- Refined predicates (`~lowercase-string`
382
- `non-lowercase-string`).
383
- - **Tuple / HashShape carriers** shape-aware element access,
384
- range / start-length slices, closed / open / required /
385
- optional policies, per-element block fold over
386
- `map`, `select`, `filter_map`, `flat_map`, `find` /
387
- `find_index`, `count`, `any?` / `all?` / `none?`, `zip`.
388
- `&:symbol` block-pass on these methods is treated as
389
- `{ |x| x.symbol }` and dispatches against the element type
390
- so `Hash#transform_values(&:freeze)` returns `Hash[K, V]`
391
- instead of `Enumerator[...]`.
392
- - **Constant folding** — aggressive arithmetic / string /
393
- Symbol / Tuple-shaped `divmod` folding, cartesian fold over
394
- `Union[Constant…]`, integer-range arithmetic
395
- (`positive-int + 1` → `int<2, max>`), branch elision on
396
- provably-truthy / falsey predicates,
397
- `Constant<String>#%` format-string fold against
398
- `Tuple` / `HashShape` arguments.
399
- - **Built-in catalogues** — Numeric / Integer / Float, String /
400
- Symbol, Array, Hash, IO, File, Range, Set, Time, Date /
401
- DateTime, Comparable, Enumerable, Rational, Complex,
402
- Pathname, Random, Struct (+ `Data`), Encoding, Regexp /
403
- MatchData, Proc / Method / UnboundMethod, Exception. Each
404
- catalog drives the fold dispatcher with per-class blocklists
405
- for indirect mutators.
406
- - **Refinement carriers** — `Type::Difference`,
407
- `Type::Refined`, `Type::Intersection` provide the
408
- imported-built-in catalogue end-to-end through
409
- `Builtins::ImportedRefinements`. The parser accepts Symbol
410
- / String literals and `|`-unions at type-arg position
411
- (`pick_of[Shape, :a | :b]`, `Pick[T, "name" | "email"]`).
412
- - **`Method` carrier (`Type::BoundMethod`)** —
413
- `Object#method(:sym)` lifts into a binding carrier so
414
- `.call` / `.()` / `[]` recover the precise dispatch
415
- (`"1".method(:to_i).call` resolves to `Constant<1>`).
416
- Reflective Method members (`#owner` / `#name` / `#arity`)
417
- still resolve via the Method RBS sig.
418
- - **`RBS::Extended` directive routes** — `return:`, `param:`
419
- (call-site + body-side), `assert:` /
420
- `predicate-if-(true|false)` accept refinement payloads, and
421
- roll up into a single `Rigor::FlowContribution` bundle per
422
- method (the v0.1.0 plugin contribution merger reads bundles
423
- directly).
424
- - **Opt-in gem-source inference (ADR-10)** — gems listed under
425
- `dependencies.source_inference:` have their `lib/` walked.
426
- Per-gem budget, per-gem-version cache slice,
427
- `dynamic.dependency-source.*` diagnostic family covering
428
- gem-not-found / budget-exceeded / config-conflict /
429
- boundary-cross (the last surfaces RBS+gem-source overlap
430
- on `mode: :full` gems for audit).
431
-
432
- The full per-release surface lives in
433
- [`CHANGELOG.md`](CHANGELOG.md). The internal contracts the
434
- analyzer guarantees live under
435
- [`docs/internal-spec/`](docs/internal-spec/).
436
-
437
- ## Plugins
438
-
439
- `v0.1.0` introduced the extension API; `v0.1.x` rounds it out
440
- with the [ADR-9](docs/adr/9-cross-plugin-api.md) cross-plugin
441
- fact channel (one plugin publishes a fact like `:model_index`,
442
- another consumes it), [ADR-11](docs/adr/11-sorbet-input-adapter.md)
443
- Sorbet ingestion, [ADR-13](docs/adr/13-typenode-resolver-plugin.md)
444
- plugin-supplied type-vocabulary resolvers, and
445
- [ADR-16](docs/adr/16-macro-expansion.md) macro / DSL expansion
446
- substrate (declarative Tier A block-as-method / Tier B
447
- trait-inlining-registry / Tier C heredoc-template / Tier D
448
- external-file inclusion). Production plugins ship under
449
- [`plugins/`](plugins/) — each is a fully-shaped plugin gem
450
- with a runnable demo and an end-to-end integration spec.
451
- Plugin-contract walkthroughs (deliberately simplified
452
- virtual use cases that spotlight one architectural surface
453
- per example) live under [`examples/`](examples/).
454
-
455
- **Plugin-contract walkthroughs** (`examples/`, focus on a
456
- single extension-point):
457
-
458
- - [`rigor-deprecations`](examples/rigor-deprecations/) —
459
- smallest possible plugin (~80 lines); config-driven rules.
460
- - [`rigor-lisp-eval`](examples/rigor-lisp-eval/) — typing literal
461
- AST arguments at a method call.
462
- - [`rigor-pattern`](examples/rigor-pattern/) — plugin →
463
- analyzer collaboration via `Scope#type_of` and the
464
- literal-string carrier.
465
- - [`rigor-units`](examples/rigor-units/) — local-variable flow
466
- tracking through arithmetic.
467
- - [`rigor-routes`](examples/rigor-routes/) — `Plugin::IoBoundary`
468
- reads under `TrustPolicy` plus cache producers.
469
-
470
- **Other production plugins for type-language extension** (`plugins/`):
471
-
472
- - [`rigor-statesman`](plugins/rigor-statesman/) — two-pass DSL
473
- analysis (collect declarations, then validate references)
474
- for the Statesman state-machine gem.
475
- - [`rigor-typescript-utility-types`](plugins/rigor-typescript-utility-types/)
476
- — `Plugin::TypeNodeResolver` chain wiring TS-canonical names
477
- (`Pick` / `Omit` / `Partial` / `Required` / `Readonly`) onto
478
- Rigor's shape-projection type functions.
479
-
480
- **Macro expansion substrate consumers** (ADR-16 — declarative
481
- manifest entries, no walker code):
482
-
483
- - [`rigor-sinatra`](plugins/rigor-sinatra/) — **Tier A**
484
- block-as-method. Recognises Sinatra's nine class-level HTTP
485
- verb methods and narrows the route block's `self_type` so
486
- bare `params` / `redirect` / `halt` resolve through
487
- `Sinatra::Base`'s RBS.
488
- - [`rigor-dry-struct`](plugins/rigor-dry-struct/) — **Tier C**
489
- heredoc-template. Synthesises a reader on every `Dry::Struct`
490
- subclass for each `attribute :name, T` / `attribute? :name, T`
491
- call.
492
- - [`rigor-devise`](plugins/rigor-devise/) — **Tier B**
493
- trait-inlining registry mirroring `lib/devise/modules.rb`.
494
- Each `devise :strategy_a, :strategy_b` call explodes the
495
- included module's RBS instance methods onto the calling model
496
- class (Devise's `user.valid_password?` returns the module's
497
- authored `bool`).
498
-
499
- **Rails ecosystem plugins** (Tier 1 + Tier 2 + Tier 3 + Sorbet):
500
-
501
- - Tier 1: [`rigor-rails-routes`](plugins/rigor-rails-routes/),
502
- [`rigor-rails-i18n`](plugins/rigor-rails-i18n/),
503
- [`rigor-actionmailer`](plugins/rigor-actionmailer/),
504
- [`rigor-activejob`](plugins/rigor-activejob/).
505
- - Tier 2: [`rigor-actionpack`](plugins/rigor-actionpack/)
506
- (4 phases — routes / filters / renders / strong-params),
507
- [`rigor-factorybot`](plugins/rigor-factorybot/),
508
- [`rigor-activerecord`](plugins/rigor-activerecord/) —
509
- publishes `:model_index` via ADR-9 for the other two
510
- to consume.
511
- - Tier 3: [`rigor-pundit`](plugins/rigor-pundit/),
512
- [`rigor-sidekiq`](plugins/rigor-sidekiq/),
513
- [`rigor-rspec`](plugins/rigor-rspec/),
514
- [`rigor-actioncable`](plugins/rigor-actioncable/).
515
- - Parallel: [`rigor-sorbet`](plugins/rigor-sorbet/) — ingests
516
- Sorbet `sig` / `T.let` / `T.cast` / `T.must` / `T.bind` /
517
- `T.assert_type!` / `T.reveal_type` / `T.absurd` and RBI
518
- files as type sources.
519
-
520
- [`plugins/README.md`](plugins/README.md) is the production
521
- plugin catalogue (Rails / RSpec / dry-rb / Sorbet / etc.) and
522
- [`examples/README.md`](examples/README.md) is the walkthrough
523
- catalogue — comparison table, recommended reading order, and
524
- the architectural map of which surface each walkthrough
525
- exercises. The binding contract for the plugin API lives in
526
- [`docs/adr/2-extension-api.md`](docs/adr/2-extension-api.md);
527
- the slice-by-slice normative specs are under
528
- [`docs/internal-spec/plugin*.md`](docs/internal-spec/); the
529
- sibling ADRs that extend it ride the same surface
530
- ([ADR-9](docs/adr/9-cross-plugin-api.md) cross-plugin facts,
531
- [ADR-11](docs/adr/11-sorbet-input-adapter.md) Sorbet adapter,
532
- [ADR-13](docs/adr/13-typenode-resolver-plugin.md) TypeNode
533
- resolver).
199
+ **Rails ecosystem** — [`rigor-rails-routes`](plugins/rigor-rails-routes/),
200
+ [`rigor-rails-i18n`](plugins/rigor-rails-i18n/),
201
+ [`rigor-activerecord`](plugins/rigor-activerecord/),
202
+ [`rigor-actionpack`](plugins/rigor-actionpack/),
203
+ [`rigor-actionmailer`](plugins/rigor-actionmailer/),
204
+ [`rigor-activejob`](plugins/rigor-activejob/),
205
+ [`rigor-factorybot`](plugins/rigor-factorybot/),
206
+ [`rigor-pundit`](plugins/rigor-pundit/),
207
+ [`rigor-sidekiq`](plugins/rigor-sidekiq/).
208
+
209
+ **Other frameworks** — [`rigor-sinatra`](plugins/rigor-sinatra/),
210
+ [`rigor-devise`](plugins/rigor-devise/),
211
+ [`rigor-dry-struct`](plugins/rigor-dry-struct/),
212
+ [`rigor-rspec`](plugins/rigor-rspec/),
213
+ [`rigor-actioncable`](plugins/rigor-actioncable/).
214
+
215
+ **Type-system extensions** [`rigor-sorbet`](plugins/rigor-sorbet/)
216
+ (ingests Sorbet `sig` / `T.let` / RBI files),
217
+ [`rigor-statesman`](plugins/rigor-statesman/),
218
+ [`rigor-typescript-utility-types`](plugins/rigor-typescript-utility-types/).
219
+
220
+ See [`plugins/README.md`](plugins/README.md) for the full catalogue. To
221
+ write a plugin for your own DSL or framework, use the
222
+ [`rigor-plugin-author`](skills/rigor-plugin-author/) Skill described above.
223
+ Plugin-contract walkthroughs (simplified virtual use cases spotlighting
224
+ one extension surface each) are under [`examples/`](examples/).
534
225
 
535
226
  ## Configuration
536
227
 
537
- `rigor init` writes a starter `.rigor.yml`:
228
+ `rigor init` writes a starter `.rigor.yml`. Most projects only need a
229
+ handful of lines:
538
230
 
539
- ```sh
540
- bundle exec rigor init # fails if .rigor.yml exists
541
- bundle exec rigor init --force # overwrite
231
+ ```yaml
232
+ paths: [lib, app]
233
+ target_ruby: "3.3"
234
+ plugins:
235
+ - rigor-activerecord
236
+ - rigor-actionpack
237
+ baseline: .rigor-baseline.yml # when using baseline adoption mode
542
238
  ```
543
239
 
544
- Common knobs the file exposes:
545
-
546
- - `paths` directories `rigor check` and `rigor type-scan`
547
- scan when no path is given (defaults to `lib`).
548
- - `target_ruby` minimum Ruby version your project targets.
549
- - `libraries` — extra stdlib libraries to load on top of the
550
- bundled defaults (e.g. `["csv", "set"]`).
551
- - `signature_paths` explicit list of `sig/`-style directories.
552
- Leave unset (or `null`) to auto-detect `<root>/sig`. Use `[]`
553
- to disable project-RBS loading entirely.
554
- - `disable` — rule identifiers to silence project-wide. Shipped
555
- rules: `undefined-method`, `wrong-arity`,
556
- `argument-type-mismatch`, `possible-nil-receiver`,
557
- `dump-type`, `assert-type`, `always-raises`. In-source
558
- `# rigor:disable <rule>` end-of-line comments silence
559
- per-line; `# rigor:disable all` suppresses every rule.
240
+ Common knobs: `disable` (rule IDs to silence project-wide),
241
+ `signature_paths` (additional `sig/`-style directories),
242
+ `dependencies.source_inference` (opt-in gem-source walk for gems
243
+ without RBS). The `rigor-project-init` Skill writes and populates all
244
+ of this for you. Full reference on the
245
+ [website](https://rigor.typedduck.fail/).
246
+
247
+ In-source suppression: `# rigor:disable <rule>` silences a single line;
248
+ `# rigor:disable all` suppresses every rule on that line.
560
249
 
561
250
  ## Status
562
251
 
563
- Current released version: **`v0.1.5`**. The analyzer is usable
564
- on real Ruby code today; the rule catalogue is deliberately
565
- narrow — Rigor's stance is to surface zero false positives
566
- while the inference surface stabilises. Forward-looking commitments
567
- (in-flight cycle + queued work) live in
568
- [`docs/ROADMAP.md`](docs/ROADMAP.md); the release-by-release
569
- "what shipped" record is [`CHANGELOG.md`](CHANGELOG.md).
570
-
571
- `v0.1.5` (released 2026-05-16) delivered (full slice list in `CHANGELOG.md` § `[0.1.5]`):
572
-
573
- - **ADR-15 Ractor migration end-to-end** (Phases 1–4c + 4b.x) — opt-in `rigor check --workers=N` parallelism; pool ≡ sequential proven on 14 real-world projects (31,840 files); spec-suite wall-clock 162s → 27s on 12 cores via `parallel_tests`.
574
- - **[ADR-16](docs/adr/16-macro-expansion.md) macro / DSL expansion substrate** — four-tier declarative manifest contract (block-as-method, trait-inlining registry, heredoc-template, external-file) with Tier B/C precision promotion and three worked consumer plugins (`rigor-sinatra`, `rigor-devise`, `rigor-dry-struct`). Closes ROADMAP O2 at the WD13 floor.
575
- - **Real-world Rails / Ruby survey** — fourteen projects swept; opt-in `rigor-activesupport-core-ext` RBS bundle delivers `−75 %` total diagnostics; built-in vendored gem RBS for six native-extension gems (`pg` / `mysql2` / `nokogiri` / `bcrypt` / `redis` / `idn-ruby`); Bundler-aware sig discovery; `RbsLoader#env` failure-memo (~550× speedup on a conflicting sig).
576
- - **O4 Layer 3 target-project RBS source discovery (slices 1+2+3)** — `Gemfile.lock` parse + bundle-sig filter, `rbs_collection.lock.yaml` awareness, missing-gem `:info` diagnostic.
577
- - **DEFAULT_LIBRARIES stdlib coverage expansion** — out-of-the-box RBS classes available 1,273 → 1,427 (+154); 31 additional stdlib libraries auto-load.
578
- - **`is_a?(C)` lexical-nesting constant resolution** — predicate-narrowing now mirrors Ruby's `Module.nesting`-driven lookup.
579
-
580
- Production plugins ship under [`plugins/`](plugins/) (Rails /
581
- RSpec / dry-rb / Sorbet / etc.) — see
582
- [`plugins/README.md`](plugins/README.md) for the catalogue.
583
- Plugin-contract walkthroughs ship under
584
- [`examples/`](examples/) — see
585
- [`examples/README.md`](examples/README.md).
252
+ Current released version: **`v0.1.8`** (2026-05-21). The analyzer is
253
+ usable on real Ruby code today; the rule catalogue is deliberately
254
+ conservative — Rigor's stance is to surface zero false positives while
255
+ the inference surface stabilises.
256
+
257
+ Release history: [`CHANGELOG.md`](CHANGELOG.md). Forward-looking
258
+ commitments: [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
586
259
 
587
260
  ## Contributing
588
261
 
589
- See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the minimal
590
- `git clone` → green-tests path and a map of the spec / ADR /
591
- skill documentation contributors should know about.
262
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the `git clone` →
263
+ green-tests path and a map of the spec / ADR / skill documentation
264
+ contributors should read.
592
265
 
593
266
  ## License
594
267