rigortype 0.1.13 → 0.1.15

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.
@@ -49,8 +49,10 @@ Use the three sections like this:
49
49
  | Hint `id` | Cause | Where this skill handles it |
50
50
  | --- | --- | --- |
51
51
  | `activesupport-core-ext` | ActiveSupport core-class monkey-patches not loaded. | Go back to Phase 3/4: add `rigor-activesupport-core-ext` to `plugins:` (it is an RBS-bundle plugin), re-run triage. This is a config gap, not a bug. |
52
- | `gem-without-rbs` | A dependency ships no RBS. | Phase 7 escalation — `rbs collection install`, or `dependencies.source_inference:`, or open a Rigor issue. |
53
- | `project-monkey-patch` | An in-project monkey-patch / refinement Rigor did not see. | Phase 7 escalation — register the defining file via `pre_eval:`, or (if it is a DSL) write a project plugin. |
52
+ | `gem-without-rbs` | A dependency ships no RBS. | If `rbs_collection.lock.yaml` was present and Phase 1 installed the collection, re-run `rigor triage` — the hint may shrink or disappear. Otherwise: Phase 8 escalation — `bundle exec rbs collection install`, or `dependencies.source_inference:`, or open a Rigor issue. |
53
+ | `project-monkey-patch-known` | **High confidence.** The engine proved the called method *is* defined by a project file (a reopened core/stdlib/gem class) but is not applied cross-file. The hint **names the defining file(s)**. | Phase 7 escalation — copy the named file(s) straight into `pre_eval:`. No detective work needed; the diagnostic already found the source. |
54
+ | `project-monkey-patch` | An in-project monkey-patch / refinement Rigor did not see, inferred from the *spread* of the same method across ≥3 files (no proven def site). | Phase 7 escalation — find the defining file (grep for `def <method>` / `class <Receiver>`), register it via `pre_eval:`, or (if it is a DSL) write a project plugin. |
55
+ | `unresolved-toplevel` | Toplevel calls (outside any `def`/`class`/`module`) that resolve to nothing visible — usually a script relying on a monkey-patch or a `require`d helper Rigor did not walk (ADR-34). | Phase 7 escalation — if a project file defines these (toplevel `def`, or a patch on `Object`/`Kernel`), list it in `pre_eval:`. If nothing defines them, treat as genuine typos / missing requires (Phase 8). |
54
56
  | `activerecord-relation-misinference` | An ActiveRecord relation inferred as `Array`. | Ensure `rigor-activerecord` is enabled (Phase 3). If it persists, it is an engine gap — open a Rigor issue. |
55
57
  | `systemic-file-cluster` | One file × one rule, large count. | Acknowledge mode: a clean baseline bucket. Strict mode: a single fix may clear many — review that file first. |
56
58
  | `genuine-bugs` | Low-count rules scattered across files. | **Phase 7** — these are the localised bugs Rigor caught. Review first, in both modes. Note: the hint groups all low-count rules regardless of severity — filter for `error` severity when prioritising actionable items. |
@@ -60,6 +62,80 @@ If triage flags `activesupport-core-ext` (or any config gap),
60
62
  baseline and the real-bug review should both run against the
61
63
  post-config diagnostic set, not the inflated one.
62
64
 
65
+ ## Phase 6a — Pre-baseline cleanup
66
+
67
+ Before generating the baseline, **apply every quick fix that triage
68
+ has already diagnosed**. A smaller baseline is a better baseline: each
69
+ bucket it omits is a regression that will surface the moment it
70
+ appears, rather than hiding behind a ceiling that the monkey-patch
71
+ noise inflated.
72
+
73
+ Quick fixes that belong here (apply now, not as post-baseline
74
+ escalation):
75
+
76
+ ### `project-monkey-patch` / `project-monkey-patch-known` → `pre_eval:`
77
+
78
+ When triage reports either hint, act before Phase 7:
79
+
80
+ 1. **`project-monkey-patch-known`** — the hint's action line **names
81
+ the defining file(s)**. Copy them straight into `pre_eval:` in
82
+ `.rigor.dist.yml`:
83
+
84
+ ```yaml
85
+ pre_eval:
86
+ - lib/core_ext/user_extensions.rb # exact path(s) named by the hint
87
+ ```
88
+
89
+ 2. **`project-monkey-patch`** (spread-based, no proven def site) —
90
+ find the defining file manually:
91
+
92
+ ```sh
93
+ # Replace `current` / `User` with the method and receiver named in the hint
94
+ grep -rn "def self\.current\|cattr_accessor.*current" app/ lib/
95
+ grep -rn "def deliver_\|def find_by_" app/ lib/
96
+ ```
97
+
98
+ Then add the file(s) to `pre_eval:`.
99
+
100
+ After adding `pre_eval:` entries, **re-run `rigor triage`** and note
101
+ the new total. If the count dropped by more than ~50 diagnostics,
102
+ the reduction is significant enough to justify this round; proceed
103
+ with the new, smaller count.
104
+
105
+ Repeat the loop (find → `pre_eval:` → re-triage) until the hint
106
+ disappears or the remaining count is stable. Then move to Phase 7.
107
+
108
+ > **If a cluster does NOT drop after you add its file to `pre_eval:`**,
109
+ > the methods are almost certainly **generated dynamically**
110
+ > (`define_method` with a computed name, `method_missing`, or a
111
+ > `class_eval` heredoc) — the pre-eval walker found no literal `def` to
112
+ > register. That is not a `pre_eval:` failure to retry; it is the signal
113
+ > that the durable fix is a **project-owned plugin**. Leave the file in
114
+ > `pre_eval:` only if it also defines literal methods; otherwise remove
115
+ > it and carry the cluster to Phase 8 § "Escalation path A", which
116
+ > hands off to the `rigor-plugin-author` skill.
117
+
118
+ > **Why `pre_eval:` reduces the count.** `pre_eval:` files are walked
119
+ > before per-file inference. Methods defined in them — including those
120
+ > added to existing classes via `class Foo; def bar; end; end` — become
121
+ > visible to every subsequent file analysis. The monkey-patched methods
122
+ > are no longer unknown, so `call.undefined-method` diagnostics that
123
+ > depended on them disappear.
124
+
125
+ ### `gem-without-rbs` (if rbs collection was not yet installed)
126
+
127
+ If Phase 1 did not install the collection (project had no
128
+ `rbs_collection.lock.yaml`) and triage now reports `gem-without-rbs`,
129
+ this is the right moment to act before the baseline:
130
+
131
+ ```sh
132
+ bundle exec rbs collection install # if rbs is in Gemfile
133
+ # or: rbs collection install
134
+ ```
135
+
136
+ Re-run `rigor triage`. If the `gem-without-rbs` count drops,
137
+ re-generate the baseline against the new number.
138
+
63
139
  ## Phase 7 — Generate the baseline (acknowledge mode only)
64
140
 
65
141
  **Strict mode skips this phase entirely.** A strict project has no
@@ -120,30 +196,160 @@ fine; the baseline is a starting envelope, not a verdict that the
120
196
  bug is acceptable. Recommend the user run the `rigor-baseline-reduce`
121
197
  skill next to work them down.
122
198
 
199
+ ### Distinguishing sig quality false positives from real bugs
200
+
201
+ Some diagnostics in the `call.wrong-arity` and
202
+ `def.return-type-mismatch` families are caused by **incomplete or
203
+ incorrect `sig/` declarations**, not by real bugs in the project code.
204
+ Identify them before presenting findings to the user — a sig FP looks
205
+ alarming but needs a sig fix, not a code fix.
206
+
207
+ #### `call.wrong-arity` on `Struct.new(...)` subclasses
208
+
209
+ When a project defines a class as:
210
+
211
+ ```ruby
212
+ MyStruct = Struct.new(:foo, :bar, :baz)
213
+ # or
214
+ class MyRecord < Struct.new(:id, :name)
215
+ ```
216
+
217
+ and its `sig/` entry is an empty shell:
218
+
219
+ ```rbs
220
+ class MyRecord
221
+ end
222
+ ```
223
+
224
+ Rigor reads the empty sig and infers `initialize` takes 0 arguments
225
+ (the default). Any `MyRecord.new(x, y)` call then fires
226
+ `call.wrong-arity`. This is a **sig quality issue** — the sig is
227
+ missing the generated `initialize`.
228
+
229
+ To confirm: check the sig file for the class. If the sig has no
230
+ `initialize` def AND the Ruby source inherits from `Struct.new(...)`,
231
+ the diagnostic is a FP. Note it as a sig improvement task (add
232
+ `initialize` matching the Struct fields) rather than a code bug.
233
+
234
+ #### `def.return-type-mismatch` when the declared type is `bot`
235
+
236
+ A sig that declares `-> bot` (or `-> void` for a `raises`-only
237
+ method) says: "this method never returns normally". If Rigor infers
238
+ an actual return value, it fires `def.return-type-mismatch`.
239
+
240
+ Two interpretations:
241
+
242
+ | Sig says `-> bot` and … | Interpretation |
243
+ | --- | --- |
244
+ | the implementation has a missing `raise` on one branch | **Likely real bug** — the method was *intended* to always raise, but a code path escapes. High priority: review the branch. |
245
+ | the sig was written conservatively (e.g. auto-generated) and the method does sometimes return | **Sig quality issue** — the sig is too strict. Fix by loosening the return type in the sig. |
246
+
247
+ To distinguish: read the method body. If every branch ends in `raise`
248
+ or `exit` except one, the missing raise is likely a bug. If the
249
+ method has intentional return values but the sig says `bot`, it is a
250
+ sig issue.
251
+
252
+ #### `call.argument-type-mismatch` on regex capture variables (`$1`, `$~`)
253
+
254
+ Rigor infers `$1`, `$~`, and similar capture variables as
255
+ `String | nil` everywhere, even inside `gsub`/`match` blocks where
256
+ they are guaranteed non-nil by the match condition. Diagnostics of
257
+ the form:
258
+
259
+ ```
260
+ expected String, got String | nil (on $1 / $~)
261
+ ```
262
+
263
+ are **engine FPs** (ADR-24 WD3 / known limitation). Note them as
264
+ noise rather than surfacing them as bugs. They belong in the
265
+ baseline.
266
+
123
267
  ### Escalation path A — application-specific metaprogramming
124
268
 
125
- If triage reports `project-monkey-patch`, or a `call.undefined-method`
126
- cluster lands on the project's own DSL / `define_method` factory /
127
- in-house macro, Rigor cannot follow it by default. Two answers,
128
- cheapest first:
129
-
130
- 1. **A plain monkey-patch in a known file** (e.g.
131
- `lib/core_ext/string_extensions.rb`) register it via `pre_eval:`
132
- in `.rigor.dist.yml`. Rigor walks those files before per-file
133
- inference, so the added methods become visible.
134
- 2. **A genuine project DSL** the durable fix is a **project-private
135
- Rigor plugin** that teaches Rigor the DSL's shape. Offer to hand
136
- off to the `rigor-plugin-author` skill. The plugin can live under
137
- the project's own `lib/` (loaded without a gemspec) or as a
138
- separate `rigor-<name>` gem.
269
+ If triage reports `project-monkey-patch-known`, `project-monkey-patch`,
270
+ `unresolved-toplevel`, or a `call.undefined-method` cluster lands on
271
+ the project's own DSL / `define_method` factory / in-house macro,
272
+ Rigor cannot follow it by default.
273
+
274
+ **The decision that picks the fix is static-vs-dynamic.** Find the
275
+ defining file (named by the hint, or `grep -rn 'def <method>\|<Receiver>'
276
+ app/ lib/`) and look at *how* the method is defined:
277
+
278
+ | How the method is defined | Fix | Why |
279
+ | --- | --- | --- |
280
+ | **Literal `def foo` / `def self.foo`** in a project file (a plain reopen / monkey-patch, e.g. `lib/core_ext/string_extensions.rb`, `User.current`) | **`pre_eval:`** — copy the file into `pre_eval:` in `.rigor.dist.yml`. | Rigor walks `pre_eval:` files before per-file inference, so the literal method becomes visible everywhere. This is Phase 6a; do it now. |
281
+ | **Dynamically generated** — computed-name `define_method`, `method_missing`, or a `class_eval <<~RUBY … def #{name} … RUBY` heredoc template | **A project-owned Rigor plugin** (escalation, below). `pre_eval:` will *not* help. | The pre-eval walker finds no literal `def` to register, so the cluster survives `pre_eval:`. If you added the file to `pre_eval:` and re-triage shows the cluster unchanged, this is the case you are in. |
282
+
283
+ So the routine is: try `pre_eval:` for the static case in Phase 6a; if a
284
+ cluster **survives** because the methods are generated, that surviving
285
+ residue is the plugin signal.
286
+
287
+ #### The plugin handoff (the recommended next step)
288
+
289
+ A genuine generated DSL is **the project's own plugin to own** — Rigor
290
+ does not bundle per-application plugins for it. The textbook example is
291
+ Redmine's settings accessors:
292
+
293
+ ```ruby
294
+ # app/models/setting.rb — generated, NOT a literal def
295
+ def self.define_setting(name, options = {})
296
+ # class_eval a heredoc that defines self.#{name} / #{name}? / #{name}=
297
+ end
298
+ # names come from config/settings.yml, iterated — not source literals
299
+ ```
300
+
301
+ `Setting.app_title`, `Setting.default_language`, … are never written as
302
+ literal `def`s, so `pre_eval:` cannot see them; the durable fix is a
303
+ project plugin (it matches ADR-16's Tier C heredoc-template shape).
304
+ Rigor's stance: application-specific homegrown DSLs are out of scope for
305
+ the bundled substrate (ADR-16 § Audience) — the app authors the plugin
306
+ in their own repo.
307
+
308
+ > **Sizing the cluster — `rigor triage` may not isolate it.** Triage's
309
+ > `project-monkey-patch` hint groups by method spread and often lumps a
310
+ > generated-DSL cluster in with unrelated patches (or buries it), so the
311
+ > hint count is not the per-receiver size. To measure the actual cluster,
312
+ > drop to the raw stream and count by receiver:
313
+ >
314
+ > ```sh
315
+ > rigor check --format json | \
316
+ > ruby -rjson -e 'd=JSON.parse(File.read(0, encoding: "UTF-8").scrub); \
317
+ > puts d["diagnostics"].count { |x| x["message"].to_s.include?("for singleton(Setting)") }'
318
+ > # or quick-and-dirty: rigor check 2>&1 | grep -c "for singleton(Setting)"
319
+ > ```
320
+ >
321
+ > (Force UTF-8 + `.scrub` — diagnostic messages can carry non-ASCII from
322
+ > the project's own strings, e.g. i18n content.) Swap `Setting` for the
323
+ > receiver the def-site grep identified.
324
+
325
+ When you reach this point:
326
+
327
+ 1. **Name the cluster and its generator** for the user — the count, the
328
+ receiver, and the defining method (e.g. *"~60 `call.undefined-method`
329
+ on `Setting.<name>`, generated by `Setting.define_setting`"*). Use the
330
+ raw-JSON count above, not the triage hint number.
331
+ 2. Say plainly that the durable fix is a **project-owned plugin**, not a
332
+ baseline entry — the baseline only parenthesises the noise; the
333
+ plugin removes it and types the synthetic methods.
334
+ 3. **Offer to launch the `rigor-plugin-author` skill**, and on the
335
+ user's confirmation, invoke it (Skill tool, `rigor-plugin-author`).
336
+ That skill scaffolds a standalone `rigor-<id>` gem or a
337
+ project-private plugin (loadable from the project's own `lib/`
338
+ without a gemspec) in the *user's* repo.
339
+
340
+ The offer is not mandatory — acknowledge mode may baseline the cluster
341
+ for now and author the plugin later. But make the next step explicit:
342
+ never leave a generated-DSL cluster in the baseline without telling the
343
+ user it has a real fix and what skill builds it.
139
344
 
140
345
  ### Escalation path B — an unsupported external gem
141
346
 
142
347
  If triage reports `gem-without-rbs`, a dependency ships no type
143
348
  information and Rigor has no built-in coverage. In order:
144
349
 
145
- 1. `rbs collection install` pulls community RBS for the gem if it
146
- exists. Re-run triage afterwards.
350
+ 1. `bundle exec rbs collection install` (bare `rbs collection install`
351
+ if `rbs` is not in the project's Gemfile) — pulls community RBS for
352
+ the gem if it exists. Re-run triage afterwards.
147
353
  2. `dependencies.source_inference:` in `.rigor.dist.yml` — opt the
148
354
  gem into Rigor inferring `Dynamic`-typed returns from its source.
149
355
  3. If the gem is widely used and genuinely warrants first-class
@@ -162,7 +368,16 @@ the cause; the user decides whether to act now or defer.
162
368
  `baseline:` line. Strict mode: neither.
163
369
  - The user has seen the likely real bugs and knows the two escalation
164
370
  paths.
165
-
166
- Next sessions: `rigor-baseline-reduce` to work the baseline down
167
- (acknowledge mode), or `rigor-plugin-author` if escalation path A
168
- applies.
371
+ - If a **dynamically-generated project DSL** was found, the user has
372
+ been told it needs a project-owned plugin and offered the
373
+ `rigor-plugin-author` handoff (path A) — not left silently in the
374
+ baseline.
375
+
376
+ Next sessions, by what the onboarding surfaced:
377
+
378
+ - **`rigor-plugin-author`** — when a generated project DSL survived
379
+ `pre_eval:` (escalation path A). This is the durable fix for the
380
+ cluster and the most impactful follow-up; offer it as the next step
381
+ before the user reaches for the baseline-reduce loop.
382
+ - **`rigor-baseline-reduce`** — acknowledge mode, to work the recorded
383
+ baseline down.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rigortype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rigor contributors
@@ -291,6 +291,7 @@ files:
291
291
  - lib/rigor/cli/explain_command.rb
292
292
  - lib/rigor/cli/lsp_command.rb
293
293
  - lib/rigor/cli/mcp_command.rb
294
+ - lib/rigor/cli/plugin_command.rb
294
295
  - lib/rigor/cli/plugins_command.rb
295
296
  - lib/rigor/cli/plugins_renderer.rb
296
297
  - lib/rigor/cli/prism_colorizer.rb