rigortype 0.1.14 → 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.
@@ -43,6 +43,26 @@ release as potentially contract-changing:
43
43
  Tell the user this up front. A plugin written today is a preview
44
44
  artefact, valuable but not yet on a stable foundation.
45
45
 
46
+ ## Read a real plugin — `rigor plugin`
47
+
48
+ You do not have to learn the `Rigor::Plugin::Base` surface from this
49
+ prose alone. Because Rigor is installed on disk (`mise` / `gem
50
+ install`), every plugin bundled in the toolchain is readable source.
51
+ Use it as a worked-example library throughout this skill:
52
+
53
+ ```sh
54
+ rigor plugin list # all bundled + example plugins, with paths
55
+ rigor plugin print rigor-activesupport-core-ext # a plugin's main source, inline
56
+ rigor plugin path rigor-units # the dir, to browse with your file tool
57
+ rigor plugin root # gem root + public API (lib/rigor/plugin.rb)
58
+ ```
59
+
60
+ `rigor plugin` (singular) browses the toolchain's plugins; `rigor
61
+ plugins` (plural) reports your own `.rigor.yml` activation — different
62
+ commands. When a step below is thinner than you need, read a shipped
63
+ plugin that does the same thing. (Paths are local to where `rigor`
64
+ runs — see the command's own note about containers.)
65
+
46
66
  ## Phase 0 — Standalone gem or project-private?
47
67
 
48
68
  Decide where the plugin lives before scaffolding anything.
@@ -63,9 +63,57 @@ on it directly.
63
63
  ## Project-private layout
64
64
 
65
65
  A project-private plugin lives inside the application repo and is
66
- never published. Two ways to make `require "rigor-<id>"` succeed:
66
+ never published. Rigor activates it by `require "rigor-<id>"`, so the
67
+ plugin's `lib/` must be on the **load path of the Ruby process that
68
+ runs `rigor`**. *Which* mechanism puts it there depends entirely on
69
+ **how `rigor` is installed** — and getting this wrong is the most
70
+ common project-private activation failure (`could not load plugin gem
71
+ "rigor-<id>"`).
72
+
73
+ > **First answer this: how does `rigor` run?** Per the
74
+ > `rigor-project-init` workflow and the manual's installation chapter,
75
+ > the recommended install is **standalone** (`mise` / `gem install`) —
76
+ > and crucially **`rigortype` is NOT in your app's `Gemfile`**. A
77
+ > standalone `rigor` does **not** load your project's bundle, so a
78
+ > `path:`-gem in the `Gemfile` + `bundle install` puts the plugin on
79
+ > the *bundle's* load path, which the standalone `rigor` never reads.
80
+ > The path-gem route below works **only** if you deliberately run
81
+ > `rigor` from a bundle that includes `rigortype` (an advanced / CI
82
+ > setup). For the default standalone install, use `RUBYLIB`.
83
+
84
+ ### Recommended for a standalone (mise / gem install) `rigor` — `RUBYLIB`
85
+
86
+ Drop the plugin under the app and put its `lib/` (or its root, if the
87
+ entry file sits at the top) on `RUBYLIB` as an **absolute path**:
67
88
 
68
- ### Recommended — a path gem
89
+ ```text
90
+ your-app/rigor-ext/rigor-myapp.rb # requires the plugin class
91
+ ```
92
+
93
+ ```sh
94
+ # Absolute path — a relative RUBYLIB is resolved against the process
95
+ # CWD and frequently does not match; use $(pwd).
96
+ RUBYLIB="$(pwd)/rigor-ext" rigor check
97
+ ```
98
+
99
+ Ruby adds `RUBYLIB` entries to `$LOAD_PATH` at startup, so the
100
+ standalone `rigor` binary finds `require "rigor-myapp"`. Set it on
101
+ every invocation (CI included); a `Rakefile` task or a shell alias
102
+ keeps it ergonomic.
103
+
104
+ > **Pitfall — do not wrap this in `bundle exec`.** `bundle exec`
105
+ > rebuilds `$LOAD_PATH` from the bundle and drops `RUBYLIB` entries, so
106
+ > `RUBYLIB=… bundle exec rigor` fails to find the plugin. If for some
107
+ > reason you must run `rigor` through `bundle exec` (or any wrapper
108
+ > that resets the load path), pass the directory as a Ruby flag
109
+ > instead — `RUBYOPT="-I$(pwd)/rigor-ext" rigor check` — which Bundler
110
+ > preserves.
111
+
112
+ Verify activation with `rigor plugins` (plural): the entry should show
113
+ `[OK ]` with `load-error: 0`. If it shows `[ERR] could not load plugin
114
+ gem`, the load path is the problem, not the plugin code.
115
+
116
+ ### Advanced (only when `rigor` runs from a bundle) — a path gem
69
117
 
70
118
  Keep the plugin in a subdirectory with its own gemspec, and reference
71
119
  it from the app's `Gemfile` by path:
@@ -87,25 +135,15 @@ gem "rigortype", "~> 0.1.0"
87
135
  gem "rigor-myapp", path: "rigor-plugin"
88
136
  ```
89
137
 
90
- `bundle install` then puts `rigor-myapp` on the load path, so Rigor's
91
- `require "rigor-myapp"` resolves. This keeps the plugin versioned,
92
- testable, and trivially promotable to a real gem later.
93
-
94
- ### Simplest a bare file on the load path
95
-
96
- If you do not want a gemspec at all, drop `rigor-myapp.rb` somewhere
97
- and run `rigor` with that directory on `RUBYLIB`:
98
-
99
- ```text
100
- your-app/rigor-ext/rigor-myapp.rb # requires the plugin class
101
- ```
102
-
103
- ```sh
104
- RUBYLIB=rigor-ext rigor check
105
- ```
106
-
107
- Workable, but the `RUBYLIB` has to be set on every invocation (CI
108
- included). Prefer the path gem unless the plugin is a throwaway.
138
+ `bundle install` puts `rigor-myapp` on the bundle's load path but
139
+ this only helps if you then **run `rigor` through that bundle**
140
+ (`bundle exec rigor`, or a bundle whose `bin/` is on `PATH`). It also
141
+ means `rigortype` *is* in this `Gemfile`, which the `rigor-project-init`
142
+ workflow recommends against for the analyzer-as-tool install. So treat
143
+ this route as the **CI / isolated-bundle** option, not the default. It
144
+ keeps the plugin versioned, testable, and trivially promotable to a
145
+ real gem later — choose it when your `rigor` already runs from a
146
+ bundle; otherwise use `RUBYLIB` above.
109
147
 
110
148
  ## The plugin class skeleton
111
149
 
@@ -89,6 +89,21 @@ type.
89
89
 
90
90
  ## Optional — contribute a return type with `flow_contribution_for`
91
91
 
92
+ > **Critical — this hook does NOT make a method "defined", so it does
93
+ > NOT suppress `call.undefined-method`.** Method *existence* and call
94
+ > *type* are two independent checks. `flow_contribution_for` sharpens
95
+ > the type of a call the analyzer has **already resolved to a real
96
+ > method** (turning a `Dynamic` return into something precise). It is
97
+ > never consulted for a receiver/method the analyzer cannot find — that
98
+ > fires `call.undefined-method` first, and a flow contribution does
99
+ > nothing to silence it. **If your goal is to kill a
100
+ > `call.undefined-method` cluster on a DSL-generated method (the common
101
+ > reason `rigor-project-init` hands off to this skill), the fix is to
102
+ > make the method *exist* in Rigor's view — ship RBS declaring it (see
103
+ > "Shipping RBS for the DSL" below), not a flow contribution.** Reach
104
+ > for `flow_contribution_for` only when the call already resolves and
105
+ > you want a *better return type*.
106
+
92
107
  A plugin can do more than emit diagnostics: it can *supply* the
93
108
  inferred return type for a call site the core analyzer would
94
109
  otherwise type as `Dynamic`. Implement `flow_contribution_for`:
@@ -128,23 +143,57 @@ most likely to shift before v0.2.0. Implement it only if the plugin
128
143
  genuinely needs to sharpen call-site types; a diagnostics-only
129
144
  plugin can skip it entirely.
130
145
 
131
- ## Shipping RBS for the DSL
146
+ ## Shipping RBS for the DSL — the way to suppress `call.undefined-method`
132
147
 
133
148
  If the DSL introduces methods or classes that Rigor cannot see (a
134
- `Money` class defined by metaprogramming, methods mixed into
135
- `Numeric`), give Rigor RBS for them so *core* inference — not just
136
- your plugin understands them. Ship `sig/*.rbs` with the plugin (or
137
- in the consuming project) and point `.rigor.yml` at it:
138
-
139
- ```yaml
140
- signature_paths:
141
- - sig
142
- ```
143
-
144
- RBS covers what the *shape* of the DSL is; the plugin walker covers
145
- the *dynamic* parts RBS cannot express (a value computed from a
146
- literal argument, a dimensional rule). They compose many plugins
147
- ship both.
149
+ `Money` class defined by metaprogramming, `Setting.<name>` accessors a
150
+ `class_eval` heredoc generates, methods mixed into `Numeric`), give
151
+ Rigor RBS declaring them so *core* inference not just your plugin
152
+ treats them as **defined**. This is what removes the
153
+ `call.undefined-method` diagnostics on those methods; nothing else
154
+ (not `diagnostics_for_file`, not `flow_contribution_for`) makes a
155
+ method exist in Rigor's view.
156
+
157
+ Two ways to wire the RBS, depending on how the plugin is packaged:
158
+
159
+ 1. **A packaged gem plugin** declare `signature_paths:` in the
160
+ **plugin manifest** (resolved relative to the plugin's gem root, per
161
+ ADR-25), so activating the plugin contributes its RBS with no
162
+ user-side wiring. This is how the bundled RBS-bundle plugins work —
163
+ read one as a worked example:
164
+
165
+ ```sh
166
+ rigor plugin print rigor-activesupport-core-ext # see manifest + sig/ layout
167
+ rigor plugin path rigor-activesupport-core-ext # then browse its sig/ dir
168
+ ```
169
+
170
+ 2. **A project-private plugin** — the manifest field still works, but
171
+ the simplest reliable route is to ship the `.rbs` under the plugin's
172
+ own `sig/` and add **that path** to the *consuming project's*
173
+ `.rigor.yml`:
174
+
175
+ ```yaml
176
+ signature_paths:
177
+ - rigor-ext/sig # the project-private plugin's RBS directory
178
+ ```
179
+
180
+ If the DSL's method names are generated from a data file (Redmine's
181
+ `Setting` names come from `config/settings.yml`), generate the `.rbs`
182
+ from that same source — a small script that reads the data file and
183
+ emits one declaration per name keeps the RBS in sync with the DSL.
184
+
185
+ RBS covers what the *shape* of the DSL is (which methods exist, their
186
+ signatures); the plugin walker covers the *dynamic* parts RBS cannot
187
+ express (a value computed from a literal argument, a dimensional rule).
188
+ They compose — many plugins ship both.
189
+
190
+ > **Browse a real plugin instead of guessing the API.** Because Rigor
191
+ > is installed on disk (`mise` / `gem install`), every bundled plugin's
192
+ > source is readable as a worked example. `rigor plugin list` shows
193
+ > them all with absolute paths; `rigor plugin print <name>` inlines a
194
+ > plugin's main source; `rigor plugin root` points at the engine source
195
+ > and the public `Rigor::Plugin::Base` API. When the prose here is
196
+ > thinner than you need, read a shipped plugin that does the same thing.
148
197
 
149
198
  ## Output of this module
150
199
 
@@ -92,8 +92,10 @@ Below that, either mode is reasonable — ask.
92
92
  | 4 | Write `.rigor.dist.yml` — severity profile follows the mode. Verify activation with `rigor plugins`. | [`references/02-configure.md`](references/02-configure.md) |
93
93
  | 5 | Generate initial RBS sigs; uplift attr_reader precision with `--params=observed`. | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) |
94
94
  | 6 | Run `rigor triage --format json` to diagnose the diagnostic stream. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
95
+ | 6a | **Pre-baseline cleanup** — apply quick fixes that triage diagnosed (`pre_eval:` for monkey-patch hints, `rbs collection install` if still needed). Re-run triage; repeat until the count is stable. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) § "Phase 6a" |
95
96
  | 7 | Acknowledge mode only — generate the baseline and wire `baseline:`. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
96
- | 8 | Surface likely real bugs; offer the two escalation paths. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
97
+ | 8 | Surface likely real bugs; distinguish sig quality FPs from real bugs; offer escalation paths. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
98
+ | 9 | Confirm the generated files with the user — what each is, and whether to commit it. | (this file — § "Final step") |
97
99
 
98
100
  Load each reference when you reach its phase. Phases run in order;
99
101
  the only branch is Phase 7 (acknowledge mode runs it, strict mode
@@ -107,7 +109,7 @@ committed `sig/` directory.
107
109
  | 1 | [`references/01-detect.md`](references/01-detect.md) | **Phases 1 + 3.** Gemfile / Gemfile.lock walk → framework family. The plugin-recommendation table (Rails / dry-rb / Sinatra / RSpec / plain Ruby). RBS-collection presence check. |
108
110
  | 2 | [`references/02-configure.md`](references/02-configure.md) | **Phase 4.** Severity-profile choice tied to the mode. The `.rigor.dist.yml` template and every key it uses. The `.rigor.dist.yml` vs `.rigor.yml` convention. |
109
111
  | 3 | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) | **Phase 5.** `rigor sig-gen --write` baseline. `rigor sig-gen --params=observed --write` attr_reader precision uplift. Handling residual `untyped` methods. Committing `sig/`. |
110
- | 4 | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) | **Phases 6–8.** `rigor triage` as the diagnosis layer. `rigor baseline generate` + wiring `baseline:`. Surfacing likely real bugs. The two escalation paths — write a project plugin, or open a Rigor issue. |
112
+ | 4 | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) | **Phases 6–8.** `rigor triage` as the diagnosis layer. Phase 6a pre-baseline cleanup loop (`pre_eval:` for monkey-patch hints, `rbs collection install`). `rigor baseline generate` + wiring `baseline:`. Surfacing likely real bugs; sig quality FP recognition (Struct `call.wrong-arity`, `-> bot` return-type-mismatch, regex-capture `$1` FPs). The two escalation paths — write a project plugin, or open a Rigor issue. |
111
113
 
112
114
  ## Escalation paths (Phase 7 preview)
113
115
 
@@ -115,10 +117,23 @@ Some diagnostic clusters are neither a quick fix nor honest baseline
115
117
  material. Two of them have a dedicated answer this skill hands off to:
116
118
 
117
119
  - **Application-specific metaprogramming** — a project DSL,
118
- `define_method` factory, or in-house macro that Rigor cannot follow
119
- produces a cluster of `call.undefined-method`. The durable fix is a
120
- **project-private Rigor plugin** that teaches Rigor the DSL. Hand
121
- off to the `rigor-plugin-author` skill.
120
+ `define_method` factory, `method_missing` accessor, or `class_eval`
121
+ heredoc generator that Rigor cannot follow produces a cluster of
122
+ `call.undefined-method` (or `unresolved-toplevel`). **First decide
123
+ whether `pre_eval:` can fix it** — it resolves only methods written
124
+ as *literal* `def` / `def self.` in a project file. When the methods
125
+ are **generated dynamically** (computed `define_method` names,
126
+ `method_missing`, a `class_eval <<~RUBY … def #{name} … RUBY`
127
+ template), `pre_eval:` walks the file and finds no literal method to
128
+ register, so the cluster *survives*. That residue is the signal that
129
+ the durable fix is a **project-private Rigor plugin** which teaches
130
+ Rigor the DSL's shape. **This is the recommended next step**, and
131
+ Rigor does **not** ship per-application plugins for it — a Redmine
132
+ app's `Setting.define_setting` accessors, an in-house
133
+ `acts_as_*`, etc. are the *project's* plugin to own (ADR-16 §
134
+ Audience: application-specific homegrown DSLs are out of scope for
135
+ the bundled substrate). Surface it and offer to launch the
136
+ **`rigor-plugin-author`** skill (see § "Next step" below).
122
137
  - **An external gem Rigor does not understand** — a dependency ships
123
138
  no RBS and Rigor has no built-in coverage for it. Try
124
139
  `rbs collection install` first; if that gem genuinely needs Rigor
@@ -126,4 +141,54 @@ material. Two of them have a dedicated answer this skill hands off to:
126
141
  <https://github.com/rigortype/rigor/issues>.
127
142
 
128
143
  Neither is a Phase 7 obligation — they are options to *offer* the
129
- user when the triage report points at one of these causes.
144
+ user when the triage report points at one of these causes. The
145
+ project-DSL handoff is detailed in
146
+ [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md)
147
+ § "Escalation path A".
148
+
149
+ ## Next step — hand off to plugin authoring when a project DSL remains
150
+
151
+ Onboarding's job ends at a committed config + (acknowledge mode) a
152
+ baseline. But if Phase 6a/8 found a **dynamically-generated project
153
+ DSL** that `pre_eval:` could not resolve (a `define_method` factory,
154
+ `method_missing`, or a `class_eval` heredoc generator), the onboarding
155
+ is not *complete* until the user knows the durable fix — a
156
+ **project-owned Rigor plugin**, which Rigor does not bundle per app.
157
+
158
+ Before the final file-confirmation step, when such a cluster exists:
159
+ name it and its generator, say plainly that the fix is a project plugin
160
+ (not a baseline entry), and **offer to launch the `rigor-plugin-author`
161
+ skill** — on the user's confirmation, invoke it (Skill tool). The full
162
+ detection-and-handoff recipe is
163
+ [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md)
164
+ § "Escalation path A". The offer is not automatic — the user may
165
+ baseline the cluster now and author the plugin later — but never leave
166
+ a generated-DSL cluster in the baseline without naming its real fix.
167
+
168
+ ## Final step — confirm the generated files with the user
169
+
170
+ Onboarding scatters new files across the project root. Before
171
+ finishing, present the user with a short message that names **each
172
+ file the workflow produced**, says what it is, and recommends
173
+ whether to commit it — so the team shares one consistent setup
174
+ rather than each developer reinventing it. Do not silently leave the
175
+ files for the user to discover.
176
+
177
+ Run `git status --short` first; only describe files that actually
178
+ exist (e.g. `sig/` exists only if Phase 5 ran; `.rigor-baseline.yml`
179
+ only in acknowledge mode). For each, give the commit recommendation:
180
+
181
+ | File | What it is | Commit? |
182
+ | --- | --- | --- |
183
+ | `.rigor.dist.yml` | The shared project config (Phase 4) — `target_ruby`, `paths:`, `plugins:`, `severity_profile:`, and the `baseline:` pointer. The single source of truth every contributor's `rigor check` reads. | **Yes** — it is the shared config; sharing it is the whole point. |
184
+ | `.rigor-baseline.yml` | Acknowledge mode only (Phase 7) — the snapshot of today's known diagnostics. Doubles as a record of project state; without it each developer's baseline diverges and the regression guard means different things per machine. | **Yes** — commit it; it documents project state and pins the regression envelope. |
185
+ | `sig/` | RBS skeletons from `rigor sig-gen` (Phase 5), if that phase ran. A first-class project artefact — it sharpens inference for everyone. | **Yes** — commit it (see [`references/04-sig-uplift.md`](references/04-sig-uplift.md) § "Commit the sig/ directory"). |
186
+ | `.rigor/` (contains `cache/`) | The per-file analysis cache `rigor check` writes to speed up re-runs. Regenerable and machine-local. | **No** — add `.rigor/` to `.gitignore`. |
187
+ | `.rigor.yml` | Optional per-developer local override (not written by this skill). Takes precedence over `.rigor.dist.yml`; used to opt out locally (e.g. run without the baseline). | **No** — gitignore it if a developer creates one. |
188
+
189
+ Recommend the two concrete actions and **ask before doing them**
190
+ (both touch the user's repo): (1) add `.rigor/` — and `.rigor.yml`
191
+ if present — to `.gitignore`; (2) commit `.rigor.dist.yml`,
192
+ `.rigor-baseline.yml`, and `sig/` as the shared Rigor setup. Per the
193
+ git-safety default, do not commit on the user's behalf until they
194
+ confirm.
@@ -50,7 +50,9 @@ Use the three sections like this:
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
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` | 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. |
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,22 +196,151 @@ 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
 
@@ -163,7 +368,16 @@ the cause; the user decides whether to act now or defer.
163
368
  `baseline:` line. Strict mode: neither.
164
369
  - The user has seen the likely real bugs and knows the two escalation
165
370
  paths.
166
-
167
- Next sessions: `rigor-baseline-reduce` to work the baseline down
168
- (acknowledge mode), or `rigor-plugin-author` if escalation path A
169
- 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.14
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