rigortype 0.3.5 → 0.3.6

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/gem_overlay/activesupport/core_ext.rbs +33 -0
  4. data/docs/handbook/02-everyday-types.md +1 -1
  5. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  6. data/docs/handbook/08-understanding-errors.md +1 -1
  7. data/docs/handbook/09-plugins.md +2 -2
  8. data/docs/handbook/10-sorbet.md +1 -1
  9. data/docs/handbook/README.md +2 -2
  10. data/docs/handbook/appendix-go.md +1 -1
  11. data/docs/handbook/appendix-java-csharp.md +2 -2
  12. data/docs/handbook/appendix-mypy.md +1 -1
  13. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  14. data/docs/handbook/appendix-rust.md +1 -1
  15. data/docs/handbook/appendix-type-theory.md +2 -2
  16. data/docs/handbook/appendix-typescript.md +4 -4
  17. data/docs/manual/02-cli-reference.md +13 -9
  18. data/docs/manual/03-configuration.md +1 -1
  19. data/docs/manual/04-diagnostics.md +12 -0
  20. data/docs/manual/08-skills.md +1 -1
  21. data/docs/manual/11-ci.md +1 -1
  22. data/docs/manual/12-caching.md +1 -1
  23. data/docs/manual/15-type-protection-coverage.md +1 -1
  24. data/docs/manual/18-removing-dead-code.md +13 -4
  25. data/docs/manual/19-effect-labels.md +19 -12
  26. data/docs/manual/README.md +2 -2
  27. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  28. data/docs/manual/plugins/rigor-activejob.md +3 -3
  29. data/docs/manual/plugins/rigor-activerecord.md +4 -4
  30. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  31. data/docs/manual/plugins/rigor-rspec.md +6 -2
  32. data/lib/rigor/analysis/diagnostic.rb +17 -6
  33. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  34. data/lib/rigor/analysis/reachability/graph.rb +30 -11
  35. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  36. data/lib/rigor/analysis/reachability/scan.rb +20 -4
  37. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  38. data/lib/rigor/analysis/run_cache_key.rb +12 -0
  39. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
  40. data/lib/rigor/analysis/runner.rb +151 -28
  41. data/lib/rigor/cache/file_digest.rb +20 -2
  42. data/lib/rigor/cli/check_command.rb +67 -48
  43. data/lib/rigor/cli/coverage_command.rb +5 -6
  44. data/lib/rigor/cli/doc_links.rb +100 -0
  45. data/lib/rigor/cli/docs_command.rb +32 -2
  46. data/lib/rigor/cli/effects_command.rb +27 -2
  47. data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
  48. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  49. data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
  50. data/lib/rigor/cli/unused_command.rb +65 -16
  51. data/lib/rigor/cli.rb +21 -8
  52. data/lib/rigor/effects/definition_lines.rb +100 -0
  53. data/lib/rigor/effects/envelope_check.rb +18 -1
  54. data/lib/rigor/effects/liskov_check.rb +17 -8
  55. data/lib/rigor/effects/signature_sources.rb +13 -2
  56. data/lib/rigor/effects/snapshot.rb +53 -21
  57. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  59. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  60. data/lib/rigor/version.rb +1 -1
  61. data/skills/rigor-ci-setup/SKILL.md +2 -2
  62. data/skills/rigor-editor-setup/SKILL.md +2 -2
  63. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  64. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  65. data/skills/rigor-plugin-review/SKILL.md +3 -3
  66. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc7f03a32a104c1c3ed00374974fc9af408c84e1c4d19dbb9ad8dc863f0ea0c4
4
- data.tar.gz: 6c7f1fd0b81526c436731ac6c4710c0937bd00043570b6db5e9d59d6973f7872
3
+ metadata.gz: b2709c0d68fc616109c3300013f08fda3702484c6ca01ff67b4ccf9d0f354ad0
4
+ data.tar.gz: 77486c048ef9cd94a725b5ebb09ae4e161003b37f67919c769845ed36f6c60bf
5
5
  SHA512:
6
- metadata.gz: 492d3d04d9bce6672ad46885d2eda1234d4f4a30f41b3168244f794754f37fa1a3d0edfc88e425fc2b0582dd09b5f45afee312189c2099c8101140db798eec0a
7
- data.tar.gz: 8fd06bd7e9af340ba29019c84bd913934c77b094987e5c52be131351bc60c52e1a8a28d777d4a417ac96480a42cf110236346a1a4f62aa5d54f4a5f50d0c8e5b
6
+ metadata.gz: 15a406b49bcd62e9efc2c1c143903a85157dbaf165ccca5defdf4abd69949d0c0d0107afd55469e5a0b27fa667e3c59a0ec58de854af503cad0a9e3952568a6e
7
+ data.tar.gz: fa429f47f4ed13b53fa07c7078510524bd6d3b304f7ffa48916b3cfab0e954b42167bda011b413946448dbdaeb63e78dfeaf444ec86a68cf928cefcf3fd70813
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Rigor
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rigortype.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/rigortype)
4
- [![GitHub License](https://img.shields.io/github/license/rigortype/rigor)](https://github.com/rigortype/rigor/blob/master/LICENSE)
4
+ [![GitHub License](https://img.shields.io/github/license/rigortype/rigor)](LICENSE)
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rigortype/rigor)
6
6
 
7
7
  **Type-aware bug finding for Ruby — no annotations required, and a
@@ -47,7 +47,7 @@ s.lenght
47
47
 
48
48
  ```shell-session
49
49
  $ rigor check demo.rb
50
- demo.rb:7:3: error: undefined method `lenght' for "hello-world"
50
+ demo.rb:7:3: error: undefined method `lenght' for "hello-world" [call.undefined-method]
51
51
  ```
52
52
 
53
53
  Note what the error says: not ``undefined method `lenght' for String``
@@ -185,9 +185,9 @@ plugins:
185
185
 
186
186
  The full catalogue is in [`plugins/README.md`](plugins/README.md); the
187
187
  `rigor-project-init` Skill picks the right set for you. To teach Rigor
188
- your own DSL, the [`rigor-plugin-author`](skills/rigor-plugin-author/)
188
+ your own DSL, the [`rigor-plugin-author`](skills/rigor-plugin-author)
189
189
  Skill authors a plugin step-by-step, and
190
- [`rigor-baseline-reduce`](skills/rigor-baseline-reduce/) drives the
190
+ [`rigor-baseline-reduce`](skills/rigor-baseline-reduce) drives the
191
191
  baseline down once you are running.
192
192
 
193
193
  ## Going deeper
@@ -231,7 +231,7 @@ rigor docs --list # list every bundled page
231
231
 
232
232
  ## Status
233
233
 
234
- Current release: **`v0.3.5`** (2026-08-25) — on the
234
+ Current release: **`v0.3.6`** (2026-08-30) — on the
235
235
  `0.3.x` line, continuing the evaluation era opened by `v0.2.0`, the first
236
236
  publicly-announced (general / evaluation) release. The line publishes
237
237
  an enumerated [compatibility surface](docs/compatibility.md) as a
@@ -65,6 +65,9 @@ class Object
65
65
  # `acts_like?(:string)` / `acts_like?(:date)` / `acts_like?(:time)`
66
66
  # is ActiveSupport's "duck-typing helper" predicate.
67
67
  def acts_like?: (Symbol | String) -> bool
68
+
69
+ # `core_ext/object/inclusion` — `x.in?([a, b])` / `x.in?(1..10)`.
70
+ def in?: (untyped) -> bool
68
71
  end
69
72
 
70
73
  # `nil.blank?` / `nil.present?` / `nil.try` are the most frequent
@@ -108,6 +111,9 @@ class String
108
111
  def pluralize: (?Integer count, ?Symbol locale) -> String
109
112
  def singularize: (?Symbol locale) -> String
110
113
  def humanize: (?capitalize: bool, ?keep_id_suffix: bool) -> String
114
+ def titlecase: () -> String
115
+ def dasherize: () -> String
116
+ def upcase_first: () -> String
111
117
 
112
118
  # `core_ext/string/filters`
113
119
  def squish: () -> String
@@ -115,6 +121,8 @@ class String
115
121
  def truncate: (Integer truncate_at, ?omission: String, ?separator: String | Regexp | nil) -> String
116
122
  def truncate_bytes: (Integer truncate_at, ?omission: String) -> String
117
123
  def truncate_words: (Integer words_count, ?omission: String, ?separator: String | Regexp | nil) -> String
124
+ def remove: (*Regexp | String patterns) -> String
125
+ def remove!: (*Regexp | String patterns) -> String
118
126
 
119
127
  # `core_ext/string/output_safety` — `html_safe` returns an
120
128
  # `ActiveSupport::SafeBuffer` (a String subclass). The closest
@@ -282,6 +290,8 @@ class Time
282
290
  # Re-declaring it raised `RBS::DuplicatedMethodDefinitionError` and took the WHOLE `Time` definition
283
291
  # down with it (every `Time` call degraded to `Dynamic[top]`). This overlay only ADDS what
284
292
  # ActiveSupport adds.
293
+ def advance: (untyped options) -> Time
294
+ def all_day: () -> Range[Time]
285
295
  def acts_like_time?: () -> true
286
296
  end
287
297
 
@@ -323,6 +333,17 @@ class Date
323
333
  def at_beginning_of_day: () -> Time
324
334
  def end_of_day: () -> Time
325
335
  def at_end_of_day: () -> Time
336
+ def advance: (untyped options) -> Date
337
+ def all_day: () -> Range[Time]
338
+
339
+ # `Date#to_time` is an OVERLOAD CONTINUATION (`| ...`), not a plain declaration: stdlib `date`
340
+ # already types `to_time: () -> Time`, and re-declaring it raises
341
+ # `RBS::DuplicatedMethodDefinitionError`, which collapses `Date` to `Dynamic[top]` for every project
342
+ # this overlay reaches — the failure the `Time#utc?` note above records, and the one #437 hit on the
343
+ # plugin. The row cannot simply be omitted either: ActiveSupport genuinely widens the arity, so
344
+ # without it `date.to_time(:utc)` — correct Rails code — draws an arity diagnostic (#449). `| ...`
345
+ # appends this overload ahead of the stdlib one, so both arities resolve.
346
+ def to_time: (?Symbol form) -> Time | ...
326
347
  end
327
348
 
328
349
  # ---------------------------------------------------------------
@@ -475,3 +496,15 @@ class DateTime
475
496
  def end_of_minute: () -> DateTime
476
497
  def acts_like_time?: () -> true
477
498
  end
499
+
500
+ # ---------------------------------------------------------------
501
+ # ERB::Util — ActionView extends it with `html_escape_once`
502
+ # ---------------------------------------------------------------
503
+
504
+ # `ERB` is a CLASS in Ruby / RBS (not a module), so reopen it as a class — a `module ERB` wrapper
505
+ # collides with upstream `class ERB` once the `erb` stdlib is in scope. `ERB::Util` itself is a module.
506
+ class ERB
507
+ module Util
508
+ def self.html_escape_once: (untyped) -> String
509
+ end
510
+ end
@@ -113,7 +113,7 @@ String, Symbol, Array, and Hash. The list is not in this
113
113
  handbook (it would fill several pages); see
114
114
  [`docs/types.md`](../types.md) and the per-class catalogues
115
115
  under
116
- [`data/builtins/ruby_core/`](../../data/builtins/ruby_core/).
116
+ [`data/builtins/ruby_core/`](../../data/builtins/ruby_core).
117
117
 
118
118
  When folding is **not** safe (because a method has side
119
119
  effects, depends on the environment, or is not in a
@@ -275,7 +275,7 @@ the underlying RBS sig advertises.
275
275
 
276
276
  If you prefer the TS spellings (`Pick<T, K>` etc.) in
277
277
  directives, opt into the
278
- [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/)
278
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types)
279
279
  plugin. The plugin registers a `Plugin::TypeNodeResolver` that
280
280
  translates each TS name onto the canonical projection:
281
281
 
@@ -236,6 +236,6 @@ deprecations, …). Most projects will never write one; the
236
236
  chapter exists so you know the option is there.
237
237
  [Chapter 10 — Coexisting with Sorbet](10-sorbet.md) is for
238
238
  projects arriving from a Sorbet codebase: the
239
- [`rigor-sorbet`](../../plugins/rigor-sorbet/) adapter reads
239
+ [`rigor-sorbet`](../../plugins/rigor-sorbet) adapter reads
240
240
  `sig { ... }` blocks, RBI files, and `T.let` / `T.cast` /
241
241
  `T.must` / `T.unsafe` assertions as type sources.
@@ -122,7 +122,7 @@ Reach for a plugin only when:
122
122
 
123
123
  If those are true, [`examples/README.md`](../../examples/README.md)
124
124
  is your starting point. The
125
- [`rigor-deprecations`](../../examples/rigor-deprecations/)
125
+ [`rigor-deprecations`](../../examples/rigor-deprecations)
126
126
  example is the smallest fully-shaped plugin — manifest +
127
127
  single per-file walk + a couple of diagnostic emissions —
128
128
  and is the recommended template for "I want to author my
@@ -146,7 +146,7 @@ From here:
146
146
  to deeper material in
147
147
  [`docs/type-specification/`](../type-specification/README.md),
148
148
  [`docs/internal-spec/`](../internal-spec/README.md), and
149
- [`docs/adr/`](../adr/).
149
+ [`docs/adr/`](../adr).
150
150
  - The [`CHANGELOG.md`](../../CHANGELOG.md) is the per-release
151
151
  truth for what shipped when.
152
152
 
@@ -1,7 +1,7 @@
1
1
  # Coexisting with Sorbet
2
2
 
3
3
  If your project already uses [Sorbet](https://sorbet.org/),
4
- the [`rigor-sorbet`](../../plugins/rigor-sorbet/) plugin
4
+ the [`rigor-sorbet`](../../plugins/rigor-sorbet) plugin
5
5
  lets Rigor read your existing `sig` blocks, RBI files, and
6
6
  `T.let` / `T.cast` / `T.must` / `T.unsafe` assertions as type
7
7
  sources. You do not have to rewrite anything in RBS to start
@@ -66,7 +66,7 @@ up the flag, key, or command that *acts* on it.
66
66
  landing page.
67
67
  10. [**Coexisting with Sorbet**](10-sorbet.md) — for users
68
68
  arriving from a Sorbet-using project: the
69
- [`rigor-sorbet`](../../plugins/rigor-sorbet/) adapter
69
+ [`rigor-sorbet`](../../plugins/rigor-sorbet) adapter
70
70
  reads `sig { ... }` blocks, RBI files, and
71
71
  `T.let` / `T.cast` / `T.must` / `T.unsafe` assertions
72
72
  as type sources without rewriting in RBS.
@@ -255,7 +255,7 @@ ADRs:
255
255
  - [`docs/internal-spec/`](../internal-spec/README.md) —
256
256
  analyzer-internal contracts (engine surface, type-object
257
257
  public API).
258
- - [`docs/adr/`](../adr/) — architecture decision records.
258
+ - [`docs/adr/`](../adr) — architecture decision records.
259
259
 
260
260
  ## Non-goals
261
261
 
@@ -105,7 +105,7 @@ type switches. Rigor has direct analogues.
105
105
  | `if x != nil` | `if x` (strips `nil`), or `unless x.nil?` |
106
106
  | `v, ok := x.(string)` (comma-ok assertion) | `x.is_a?(String)` narrowing in an `if` |
107
107
  | `switch v := x.(type) { case string: … }` | `case x; in String => v` |
108
- | `x.(T)` (assertion, panics on fail) | (no panicking assertion) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet/) |
108
+ | `x.(T)` (assertion, panics on fail) | (no panicking assertion) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet) |
109
109
  | user func returning `bool` | `%a{rigor:v1:predicate-if-true x is Foo}` directive |
110
110
 
111
111
  Go's type switch is *not* exhaustive — you can omit cases and
@@ -58,7 +58,7 @@ advisor that only speaks when it is sure.
58
58
  | `Set<T>` | `HashSet<T>` / `ISet<T>` | `Set[T]` | |
59
59
  | `record Point(int x, int y)` | `record Point(int X, int Y)` | `Point = Data.define(:x, :y)` | See [Records ↔ Data.define](#records--datadefine). |
60
60
  | `Optional<T>` | `T?` (nullable reference type) | `T?` (i.e. `T \| nil`) | Java models it as a *container*; C# as a *type modifier*. See [Nullability](#nullability). |
61
- | `enum Color { RED, GREEN }` | `enum Color { Red, Green }` | `Constant<:red> \| Constant<:green>` (Symbol union) | Ruby has no native enum; the [`rigor-mangrove`](../../plugins/) plugin types richer enum DSLs. |
61
+ | `enum Color { RED, GREEN }` | `enum Color { Red, Green }` | `Constant<:red> \| Constant<:green>` (Symbol union) | Ruby has no native enum; the [`rigor-mangrove`](../../plugins) plugin types richer enum DSLs. |
62
62
  | `sealed interface Shape permits …` | `abstract` base + sealed hierarchy | union of the subtypes | See [Sealed types & exhaustiveness](#sealed-types-and-exhaustiveness). |
63
63
  | `<T>` (generic) | `<T>` (generic) | RBS `[T]` type parameter | |
64
64
  | `? extends T` (use-site) | `out T` (declaration-site) | covariant type parameter | See [Generics & variance](#generics-and-variance). |
@@ -124,7 +124,7 @@ behaviour matches.
124
124
  | `x instanceof String` | `x is string` | `x.is_a?(String)` |
125
125
  | `x instanceof String s` (binding) | `x is string s` (binding) | `case x; in String => s` |
126
126
  | `switch (x) { case Foo f -> … }` | `switch (x) { case Foo f => … }` | `case x; in Foo => f` |
127
- | `(Foo) x` (cast) | `(Foo)x` (cast) | (no in-source cast) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet/) |
127
+ | `(Foo) x` (cast) | `(Foo)x` (cast) | (no in-source cast) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet) |
128
128
  | `Objects.requireNonNull(x)` | `x!` (null-forgiving) | (no in-source assertion) — `unless x.nil?`, or `T.must` via `rigor-sorbet` |
129
129
  | user method returning `boolean` | user method returning `bool` | `%a{rigor:v1:predicate-if-true x is Foo}` directive on the predicate |
130
130
 
@@ -296,7 +296,7 @@ draws the distinction in full.
296
296
  Pyright's "type alias narrowing" and mypy's overload stacks
297
297
  cover some cases; Rigor's plugin contract gives you full
298
298
  Ruby code at the dispatch point. The
299
- [`rigor-lisp-eval`](../../examples/rigor-lisp-eval/) example
299
+ [`rigor-lisp-eval`](../../examples/rigor-lisp-eval) example
300
300
  is the canonical demo — `Lisp.eval([:+, 1, 2])` returns
301
301
  `Integer`, `Lisp.eval([:<, 1, 2])` returns `bool`.
302
302
 
@@ -228,8 +228,8 @@ author):
228
228
  - The `missing-protocol-method` / `protocol-return-mismatch`
229
229
  diagnostics are **plugin diagnostics**, emitted under the
230
230
  plugin's `plugin.<id>.` provenance — not core Rigor rules. The
231
- worked references are [`examples/rigor-web/`](../../examples/rigor-web/)
232
- (the minimal tutorial) and [`plugins/rigor-hanami/`](../../plugins/rigor-hanami/)
231
+ worked references are [`examples/rigor-web/`](../../examples/rigor-web)
232
+ (the minimal tutorial) and [`plugins/rigor-hanami/`](../../plugins/rigor-hanami)
233
233
  (production Hanami 2 actions).
234
234
 
235
235
  ## Interface vs protocol contract
@@ -91,7 +91,7 @@ The reflex to drop is `unwrap()`. In Rust you reach for
91
91
  `.unwrap()` / `.expect()` when you *know* it is `Some`. Rigor
92
92
  has no in-source assertion that lies to the checker; the
93
93
  equivalents are a `nil?` guard (checked, not asserted) or
94
- `T.must` via the [`rigor-sorbet`](../../plugins/rigor-sorbet/)
94
+ `T.must` via the [`rigor-sorbet`](../../plugins/rigor-sorbet)
95
95
  plugin (see [Chapter 10](10-sorbet.md)).
96
96
 
97
97
  **`Result<T, E>` ↔ exceptions.** Here the models diverge. Ruby
@@ -709,7 +709,7 @@ Rigor maps onto this as:
709
709
  | Dynamic type `?` | **`Dynamic[T]`** — a carrier that *wraps* a "best-guess" type `T` while marking the value as not-statically-verified. `Dynamic[top]` is the maximally-dynamic form. |
710
710
  | Consistency `~` | The `maybe` arm of the trinary certainty — `Dynamic[T] ~ U` holds whenever `T ~ U` does. |
711
711
  | Static/dynamic boundary | Per-method, per-file, per-plugin contribution — Rigor records *why* a value became `Dynamic[T]` in its dynamic-origin algebra. |
712
- | Casts | No in-source cast operator. The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet/) plugin reads `T.let` / `T.cast` / `T.must` as cast forms; `RBS::Extended` `assert_type` directives serve the same role from `.rbs`. |
712
+ | Casts | No in-source cast operator. The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet) plugin reads `T.let` / `T.cast` / `T.must` as cast forms; `RBS::Extended` `assert_type` directives serve the same role from `.rbs`. |
713
713
 
714
714
  Two Rigor-specific extensions matter:
715
715
 
@@ -794,7 +794,7 @@ against:
794
794
  ### What Rigor explicitly does NOT do
795
795
 
796
796
  - **Runtime contract insertion at the static / dynamic boundary.**
797
- The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet/) plugin
797
+ The opt-in [`rigor-sorbet`](../../plugins/rigor-sorbet) plugin
798
798
  reads Sorbet's `T.let` / `T.cast` / `T.must` as cast forms, but
799
799
  the contract *enforcement* is `sorbet-runtime`'s job, not
800
800
  Rigor's. Rigor's static analysis uses the cast as a hint, not
@@ -51,7 +51,7 @@ inference cannot see further).
51
51
  | `{ name: string; age: number }` | `HashShape{name: String, age: Integer}` | Same per-key model; Ruby uses Symbol keys idiomatically. |
52
52
  | `Array<T>` / `T[]` | `Array[T]` | Same. |
53
53
  | `Record<K, V>` | `Hash[K, V]` | Same. |
54
- | `Readonly<T>` | `readonly_of[T]` (via opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/) plugin) | View-level read-only marker on every entry of a `HashShape`. Does NOT prove the underlying object is frozen — ADR-13 § "Readonly". |
54
+ | `Readonly<T>` | `readonly_of[T]` (via opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types) plugin) | View-level read-only marker on every entry of a `HashShape`. Does NOT prove the underlying object is frozen — ADR-13 § "Readonly". |
55
55
  | `Partial<T>` / `Required<T>` | `partial_of[T]` / `required_of[T]` (same plugin) | Flips every entry's required-ness on a `HashShape`. `Partial` does NOT widen value types to `nil` — Rigor's `HashShape` distinguishes "key absent" from "key present with nil value" (ADR-13 WD on required-ness flips). |
56
56
  | `Pick<T, K>` / `Omit<T, K>` | `pick_of[T, K]` / `omit_of[T, K]` (same plugin) | Restrict / remove `HashShape` entries by literal-key union; Tuple receivers project by integer index. Non-shape carriers degrade conservatively and surface `dynamic.shape.lossy-projection`. |
57
57
  | Conditional types `T extends U ? A : B` | (none in core; plugin contributions) | A plugin can vary return type by argument shape. |
@@ -159,7 +159,7 @@ inferred call-site instantiation as routinely as TypeScript.
159
159
  | `Array<T>` | `Array[T]` |
160
160
  | `Map<K, V>` | `Hash[K, V]` |
161
161
  | `Promise<T>` | (no analogue — Ruby has no built-in Promise) |
162
- | `Pick<T, K>` / `Omit<T, K>` / `Partial<T>` / `Required<T>` / `Readonly<T>` | Opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/) plugin maps each onto `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of` over `HashShape` (and `pick_of` / `omit_of` over `Tuple`). |
162
+ | `Pick<T, K>` / `Omit<T, K>` / `Partial<T>` / `Required<T>` / `Readonly<T>` | Opt-in [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types) plugin maps each onto `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of` over `HashShape` (and `pick_of` / `omit_of` over `Tuple`). |
163
163
  | Conditional types | (no analogue — would need a plugin) |
164
164
 
165
165
  Rigor reads RBS generics through its dispatcher and instantiates
@@ -214,7 +214,7 @@ Be honest about what you give up:
214
214
  variation, not type-level expressions.
215
215
  - **Mapped types.** `Pick`, `Omit`, `Partial`, `Required`, and
216
216
  `Readonly` ship as opt-in plugin-supplied vocabulary via
217
- [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/),
217
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types),
218
218
  which maps them onto the Rigor-canonical `pick_of` / `omit_of`
219
219
  / `partial_of` / `required_of` / `readonly_of` shape-projection
220
220
  type functions on `HashShape` (and `pick_of` / `omit_of` on
@@ -294,7 +294,7 @@ def pick: [K, V] (Hash[K, V] obj, Array[K] keys) -> Hash[K, V]
294
294
 
295
295
  The RBS sig stays generic. If you want `Pick<T, K>`'s exact-
296
296
  key-set tracking back, opt into the
297
- [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/)
297
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types)
298
298
  plugin and annotate the return type with the `Pick` spelling:
299
299
 
300
300
  ```rbs
@@ -38,7 +38,7 @@ the `paths:` list from the configuration file.
38
38
  | `--incremental` | Re-analyse only the files changed since the last run plus the files that depend on them, serving the rest from a cross-process disk snapshot (ADR-46). Diagnostics are identical to a full run; a config / gem / version change (or a file added or removed) transparently forces a full re-analysis. See [Caching](12-caching.md). |
39
39
  | `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics are byte-identical, then exit (0 on match, 1 with the differing diagnostics on mismatch). Used in CI to guarantee `--incremental` never serves a stale result. |
40
40
  | `--clear-cache` | Delete the cache directory before running. |
41
- | `--cache-stats` | Print the on-disk cache inventory when finished. |
41
+ | `--cache-stats` | Print the on-disk cache inventory when finished — on stdout under `--format text`, on stderr under every other format, so machine-readable output stays a parseable document. `--clear-cache`'s and `--verify-incremental`'s notes follow the same rule. |
42
42
  | `--[no-]stats` | Print a run summary (files, classes, memory, wall time) to stderr. Default on. |
43
43
  | `--coverage` | Add a type-precision coverage block to the output (`coverage` object under `--format json`; a one-line summary in text mode). Off by default — it is a second precision pass over the analyzed files, the same scan [`rigor coverage`](#rigor-coverage) runs, so it is opt-in. |
44
44
  | `--workers=N` | Dispatch analysis across `N` parallel worker processes (fork-based pool today; ADR-15). Default `0` (sequential). Applies to `--incremental` re-checks as well as full runs. |
@@ -304,8 +304,8 @@ supposed to fan out — the fan-out is the blast radius.
304
304
 
305
305
  ```yaml
306
306
  # .rigor-effects.yml — generated by `rigor effects update`. Commit it; review its diff.
307
- schema: 1
308
- rigor: "0.3.3"
307
+ schema: 2
308
+ rigor: "0.3.5"
309
309
  vocabulary: 1
310
310
  config_digest: "9ec82bfc…"
311
311
  methods:
@@ -314,7 +314,7 @@ methods:
314
314
  "Reports::Nightly#perform":
315
315
  effects: ["io.db.read"]
316
316
  exhaustive: false
317
- unresolved: ["dynamic-send"]
317
+ unresolved: 1
318
318
  reach:
319
319
  "OrdersController#create":
320
320
  effects: ["io.db.read", "io.net.http", "job.enqueue"]
@@ -376,14 +376,17 @@ spelled out:
376
376
  Effect drift against .rigor-effects.yml:
377
377
 
378
378
  methods:
379
- PaymentGateway#charge + io.net.http
379
+ PaymentGateway#charge + io.net.http (app/services/payment_gateway.rb:18)
380
380
 
381
381
  reach:
382
- OrdersController#create + io.net.http
382
+ OrdersController#create + io.net.http (app/controllers/orders_controller.rb:7)
383
383
 
384
- Run `rigor effects update` and commit the result if this change is intended.
384
+ Run `rigor effects explain` to see what caused this, and `rigor effects update` to accept it.
385
385
  ```
386
386
 
387
+ Each row names where the method is defined, so the reviewer reads the report
388
+ rather than searching for the method.
389
+
387
390
  The author runs `rigor effects explain` to see the route —
388
391
 
389
392
  ```
@@ -556,7 +559,7 @@ hand-adjudicated corpus target only **7% of the rows were genuinely
556
559
  unused**; the rest were reachable by means static analysis cannot
557
560
  see. That is why this is a separate command and never a `rigor check`
558
561
  diagnostic — see
559
- [ADR-102](https://github.com/rigortype/rigor/blob/master/docs/adr/102-unused-code-reachability-report.md).
562
+ [ADR-102](../adr/102-unused-code-reachability-report.md).
560
563
 
561
564
  Reachability is computed from **roots**, not by counting references,
562
565
  so a cluster of classes that only reference each other is still
@@ -1092,9 +1095,10 @@ operational knobs read the environment instead.
1092
1095
  | `RIGOR_CI_DETECT=0` | Turn off CI auto-detection — the same as `--no-ci-detect`. See [Running Rigor in CI § auto-detection](11-ci.md). |
1093
1096
  | `RIGOR_RACTOR_WORKERS=N` | Worker count for parallel analysis. Sits between the CLI flag and the config key in precedence: `--workers=N` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > `0` (sequential). |
1094
1097
  | `RIGOR_POOL_BACKEND=ractor` | Opt back into the (off-by-default) Ractor worker pool instead of the active fork-based pool ([ADR-15](../adr/15-ractor-concurrency.md)). Only relevant with a non-zero worker count; the fork pool is the supported backend. |
1098
+ | `RIGOR_LSP_POOL_MIN_BATCH=N` | Fewest buffers an [`rigor lsp`](#rigor-lsp) batch must carry before analysis is dispatched across the worker pool rather than run in-process (default `16`). Lower it if your project's per-file analysis is expensive enough that pooling pays off sooner. |
1095
1099
  | `RIGOR_PLUGIN_ISOLATION=none\|process\|ruby_box` | How a plugin's direct calls into its target library are isolated. Default `process`. See [Using plugins § Isolation strategy](07-plugins.md). `RIGOR_BOX` is a legacy alias for `ruby_box`. |
1096
1100
  | `RIGOR_STRICT_VALIDATION=1` | Force full-content cache validation for one run (the same as `cache.validation: digest`, and winning over it) — re-hash every file's content instead of trusting its stat metadata. Use it if a filesystem's timestamps or inode numbers cannot be trusted. See [Caching § How a file is checked for changes](12-caching.md#how-a-file-is-checked-for-changes). |
1097
- | `RIGOR_DISABLE_YJIT=1` | Opt out of Rigor's deferred YJIT enablement. Rigor turns YJIT on partway through a long `check` / `coverage` run so short runs never pay the JIT warm-up; this variable leaves it off entirely. Diagnostics and allocations are identical either way — the effect is wall-time only. |
1101
+ | `RIGOR_DISABLE_YJIT=1` | Opt out of Rigor's deferred YJIT enablement. Rigor turns YJIT on partway through any long run so short runs never pay the JIT warm-up; this variable leaves it off entirely. Diagnostics and allocations are identical either way — the effect is wall-time only. |
1098
1102
  | `RIGOR_YJIT_DEADLINE=<seconds>` | Advanced: tune how long a run must last before deferred YJIT enables (default `5.0`). Lower it if your runs are long and you want the JIT sooner; raise it to protect short runs. Ignored when `RIGOR_DISABLE_YJIT=1` is set or YJIT is unavailable. |
1099
1103
 
1100
1104
  Three further variables (`RIGOR_BUDGET_TRACE`,
@@ -251,7 +251,7 @@ stanza it broke:
251
251
  ```
252
252
  app/presenters/user_presenter.rb:14:1: warning: Method Presenters::User#render performs io.fs.read
253
253
  (File.read), but is declared effect: [] at .rigor.yml effects.envelopes[0], so io.fs.read exceeds
254
- the envelope.
254
+ the envelope. [effect.envelope-exceeded]
255
255
  ```
256
256
 
257
257
  When one method is a deliberate exception, write the narrower envelope on it in RBS — nearest wins,
@@ -19,6 +19,18 @@ Every rule has a two-segment `family.rule` identifier:
19
19
  | `assert` | `assert_type` checks. |
20
20
  | `dump` | `dump_type` notices. |
21
21
 
22
+ Every diagnostic carries its identifier in brackets at the end of the
23
+ line, so the ID you need for `# rigor:disable`, for `disable:` and for
24
+ `severity_profile:` is the one you are already looking at:
25
+
26
+ ```text
27
+ app/user.rb:11:3: error: undefined method `lenght' for "hello" [call.undefined-method]
28
+ ```
29
+
30
+ A diagnostic no rule produced — a parse error, a path error, an
31
+ internal analyzer error — has nothing to suppress and carries no
32
+ bracket.
33
+
22
34
  `rigor explain <rule>` prints the full catalogue entry for any
23
35
  built-in rule ID; `rigor explain` with no argument lists them all.
24
36
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Rigor bundles a set of **Agent Skills** — structured workflows an AI
4
4
  coding agent (Claude Code and compatible tools) can run on your behalf.
5
- They live in [`skills/`](../../skills/) and are auto-discovered when an
5
+ They live in [`skills/`](../../skills) and are auto-discovered when an
6
6
  agent works inside a project that has Rigor available.
7
7
 
8
8
  Skills are optional. Everything they do, you can do by hand with the
data/docs/manual/11-ci.md CHANGED
@@ -73,7 +73,7 @@ platform reads to annotate the diff directly, instead of leaving them in
73
73
  the job log ([ADR-51](../adr/51-ci-diagnostic-output-formats.md)). These
74
74
  layer on the generic `--format json` stream; they add no new diagnostics,
75
75
  only a platform-native rendering. Ready-to-copy template files live under
76
- [`ci-templates/`](ci-templates/).
76
+ [`ci-templates/`](ci-templates).
77
77
 
78
78
  ### GitHub — inline annotations (the default)
79
79
 
@@ -137,7 +137,7 @@ CI detection that `auto` relies on.
137
137
  | --- | --- |
138
138
  | `rigor check --no-cache` | Run without reading or writing the persistent cache. |
139
139
  | `rigor check --clear-cache` | Delete the cache directory, then run. |
140
- | `rigor check --cache-stats` | Print the on-disk cache inventory when the run finishes. |
140
+ | `rigor check --cache-stats` | Print the on-disk cache inventory when the run finishes (on stderr under every `--format` but `text`, so the document on stdout stays parseable). |
141
141
  | `rigor check --incremental` | Re-analyse only what changed; serve the rest from the incremental snapshot (see below). |
142
142
 
143
143
  There is no config key to disable caching permanently — the
@@ -333,7 +333,7 @@ ones a type actually catches.
333
333
  > [Configuration](03-configuration.md)). A project whose gems live in
334
334
  > the active Ruby's default gem home — the common `rbenv` / `mise`
335
335
  > case with no `--path` set — is invisible to the isolated analyzer by
336
- > design ([ADR-27](https://github.com/rigortype/rigor/blob/master/docs/adr/27-tool-distribution-model.md)):
336
+ > design ([ADR-27](../adr/27-tool-distribution-model.md)):
337
337
  > point Rigor at it with `bundler.bundle_path:`. Until you do, these
338
338
  > holes keep the generic `engine_gap` cause instead of `add_rbs` —
339
339
  > the label is missing, never wrong.
@@ -140,7 +140,7 @@ Four sections, needing four different decisions:
140
140
 
141
141
  | Section | What it means | What to do |
142
142
  | --- | --- | --- |
143
- | **Reachable only from test code** | Live test, no production caller | Work these first |
143
+ | **Reachable only from test code** | Live test, no production caller Rigor can see or suspect | Work these first |
144
144
  | **Candidates** | Nothing reachable names it | Adjudicate — most are still live |
145
145
  | **Cannot decide** | Something can name it at runtime | Read the reason; do not delete from here |
146
146
  | **Namespace-only** | A module wrapping live code | Excluded from candidates; count only |
@@ -209,14 +209,23 @@ skip most of a list quickly:
209
209
 
210
210
  ### Cannot decide: read the reason, do not delete
211
211
 
212
- These were demoted out of `candidates` because something can name the
213
- class at runtime. Each row says what:
212
+ These were demoted because something can name the class at runtime —
213
+ out of `candidates`, or out of **reachable only from test code**. Each
214
+ row says what:
214
215
 
215
216
  ```
216
217
  1 Handlers::Alpha lib/handlers.rb:2
217
218
  constantize on an interpolated string (lib/dispatch.rb:14)
218
219
  ```
219
220
 
221
+ A row demoted out of the test-only section is the second kind, and it
222
+ is the one worth knowing about: a class your specs reference and a
223
+ data file also names — a job in `config/recurring.yml`, a class named
224
+ from a YAML setting — is not a dead production path, because the
225
+ configuration may well be what drives it. The test-only section makes
226
+ a claim about production, so a row Rigor holds evidence against
227
+ belongs here instead, with the file named.
228
+
220
229
  `"Foo".constantize` names `Foo` exactly, so it counts as an ordinary
221
230
  reference and never reaches this section. `"Foo::#{key}".constantize`
222
231
  can only bound the namespace, so everything under `Foo` is demoted. A
@@ -322,5 +331,5 @@ Running it as a **reporting** job is fine, and is the intended way to
322
331
  notice the drift described above: run it on a schedule, publish the
323
332
  JSON, and let a person read the trend.
324
333
 
325
- [adr-102]: https://github.com/rigortype/rigor/blob/master/docs/adr/102-unused-code-reachability-report.md
334
+ [adr-102]: ../adr/102-unused-code-reachability-report.md
326
335
  [issue-363]: https://github.com/rigortype/rigor/issues/363
@@ -313,8 +313,8 @@ digest of your `effects:` block, so an upgrade or a policy edit shows up as a
313
313
 
314
314
  ```yaml
315
315
  # .rigor-effects.yml — generated by `rigor effects update`. Commit it; review its diff.
316
- schema: 1
317
- rigor: "0.3.4"
316
+ schema: 2
317
+ rigor: "0.3.5"
318
318
  vocabulary: 1
319
319
  config_digest: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
320
320
  methods:
@@ -324,10 +324,12 @@ methods:
324
324
 
325
325
  Two notes on reading the file:
326
326
 
327
- - **Do not read the `unresolved:` arrays.** They are inference-quality metadata,
328
- they are what makes half the file's bytes, and they churn on Rigor upgrades
329
- and on unrelated edits. They exist so `check` can print
330
- `exhaustive not`. The lines a reviewer reads are `effects:` and `declared:`.
327
+ - **`unresolved:` is a count, not a list.** It says how many calls the analyzer
328
+ could not follow, which is why `exhaustive:` is false. The causes themselves
329
+ are not recorded they are inference-quality metadata that churns on Rigor
330
+ upgrades and on unrelated edits so ask for them when you need them:
331
+ `rigor effects explain` names them, including for an `exhaustive → not` row.
332
+ The lines a reviewer reads are `effects:` and `declared:`.
331
333
  - Trivial and synthesised entries are left out, as in the report. `--full`
332
334
  records everything, and produces a much larger and much noisier file.
333
335
 
@@ -398,11 +400,11 @@ CI runs `rigor effects check` and fails, exit 1:
398
400
  Effect drift against .rigor-effects.yml:
399
401
 
400
402
  methods:
401
- Change#init_path + io.fs.write
402
- Change#init_path + nondet.time
403
- Change#init_path exhaustive → not
403
+ Change#init_path + io.fs.write (app/models/change.rb:41)
404
+ Change#init_path + nondet.time (app/models/change.rb:41)
405
+ Change#init_path exhaustive → not (app/models/change.rb:41)
404
406
 
405
- Run `rigor effects update` and commit the result if this change is intended.
407
+ Run `rigor effects explain` to see what caused this, and `rigor effects update` to accept it.
406
408
  ```
407
409
 
408
410
  `+ label` and `- label` are the proven lane; `≤+` / `≤-` the declared one;
@@ -410,6 +412,11 @@ Run `rigor effects update` and commit the result if this change is intended.
410
412
  someone introduced a call Rigor cannot follow; `+symbol` / `-symbol` are methods
411
413
  that appeared or vanished, and a rename is one of each.
412
414
 
415
+ The parenthetical is where the method is defined — the file, and the `def`'s own
416
+ line when the file has one. A method defined by a reopening spans several files
417
+ and the row names them all; a `-symbol` row carries no position, because a method
418
+ this run no longer sees is one it cannot locate.
419
+
413
420
  Before regenerating, ask why:
414
421
 
415
422
  ```sh
@@ -518,7 +525,7 @@ A method that exceeds its bound gets one diagnostic per (method, label) pair, at
518
525
  its `def`, naming the route:
519
526
 
520
527
  ```
521
- app/helpers/application_helper.rb:59:1: warning: Method ApplicationHelper#link_to_principal performs io.fs.read (Dir.glob via IconsHelper#principal_icon → IconsHelper#sprite_icon → IconsHelper#sprite_source → Redmine::Themes::Helper#current_theme → Redmine::Themes.theme → Redmine::Themes.themes → Redmine::Themes.scan_themes), but is declared effect: [] at .rigor.yml effects.envelopes[0], so io.fs.read exceeds the envelope.
528
+ app/helpers/application_helper.rb:59:1: warning: Method ApplicationHelper#link_to_principal performs io.fs.read (Dir.glob via IconsHelper#principal_icon → IconsHelper#sprite_icon → IconsHelper#sprite_source → Redmine::Themes::Helper#current_theme → Redmine::Themes.theme → Redmine::Themes.themes → Redmine::Themes.scan_themes), but is declared effect: [] at .rigor.yml effects.envelopes[0], so io.fs.read exceeds the envelope. [effect.envelope-exceeded]
522
529
  ```
523
530
 
524
531
  **Budget for a big first number.** That one stanza, on Redmine, is **343
@@ -608,7 +615,7 @@ app/serializers/rest/v1/instance_serializer.rb:89:1: warning: Method
608
615
  REST::V1::InstanceSerializer#invites_enabled performs mutate.self
609
616
  (receiver-mutation via UserRole.everyone → UserRole.create! → UserRole#set_position),
610
617
  but is declared effect: [] at .rigor.yml effects.envelopes[0], so mutate.self
611
- exceeds the envelope.
618
+ exceeds the envelope. [effect.envelope-exceeded]
612
619
  ```
613
620
 
614
621
  Rigor walked *through* the database write and reported the ivar assignment beyond
@@ -80,7 +80,7 @@ flag, key, or command that *acts* on it.
80
80
  Cline, …) via `rigor mcp`.
81
81
  11. [Running Rigor in CI](11-ci.md) — a clean CI job, inline
82
82
  PR/MR diagnostics (SARIF / GitHub Actions / GitLab Code
83
- Quality), copy-paste [templates](ci-templates/), and
83
+ Quality), copy-paste [templates](ci-templates), and
84
84
  version pinning.
85
85
  12. [Caching](12-caching.md) — where the cache lives, what
86
86
  invalidates it, and how to clear it.
@@ -94,4 +94,4 @@ flag, key, or command that *acts* on it.
94
94
  - [`docs/types.md`](../types.md) — one-page type-system guide.
95
95
  - [`docs/type-specification/`](../type-specification/README.md)
96
96
  — the normative spec corpus.
97
- - [`docs/adr/`](../adr/) — architecture decision records.
97
+ - [`docs/adr/`](../adr) — architecture decision records.
@@ -17,10 +17,10 @@ plugins:
17
17
  ## What it checks
18
18
 
19
19
  ```text
20
- demo.rb:7:1: info: `UserMailer.welcome` matches mailer action (arity 1..2)
21
- errors_demo.rb:7:1: error: `UserMailer.welcome` expects 1..2 argument(s), got 0
22
- errors_demo.rb:15:1: error: `UserMailer.does_not_exist` is not a defined mailer action (known actions: digest, reset_password, welcome)
23
- app/mailers/user_mailer.rb:14:7: warning: `UserMailer#digest` has no view template under `app/views/user_mailer/`
20
+ demo.rb:7:1: info: `UserMailer.welcome` matches mailer action (arity 1..2) [plugin.actionmailer.mailer-call]
21
+ errors_demo.rb:10:1: error: `UserMailer.welcome` expects 1..2 argument(s), got 0 [plugin.actionmailer.wrong-arity]
22
+ errors_demo.rb:18:1: error: `UserMailer.does_not_exist` is not a defined mailer action (known actions: digest, reset_password, welcome) [plugin.actionmailer.unknown-action]
23
+ app/mailers/user_mailer.rb:26:7: warning: `UserMailer#digest` has no view template under `app/views/user_mailer/` [plugin.actionmailer.missing-view]
24
24
  ```
25
25
 
26
26
  1. **Action existence** — `Mailer.unknown_action(...)` →
@@ -18,9 +18,9 @@ Given a job whose `#perform` takes one required and one optional
18
18
  argument (arity `1..2`):
19
19
 
20
20
  ```text
21
- demo.rb:6:1: info: `WelcomeEmailJob.perform_later` matches `#perform` (arity 1..2)
22
- demo.rb:9:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 0
23
- demo.rb:12:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 3
21
+ demo.rb:8:1: info: `WelcomeEmailJob.perform_later` matches `#perform` (arity 1..2) [plugin.activejob.job-call]
22
+ errors_demo.rb:10:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 0 [plugin.activejob.wrong-arity]
23
+ errors_demo.rb:14:1: error: `WelcomeEmailJob.perform_later` expects 1..2 argument(s), got 3 [plugin.activejob.wrong-arity]
24
24
  ```
25
25
 
26
26
  A `*rest` parameter yields an unbounded upper bound (`arity 0+`).