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.
@@ -22,6 +22,7 @@ module Rigor
22
22
  module_function
23
23
 
24
24
  UNDEFINED_METHOD_RULE = "call.undefined-method"
25
+ UNRESOLVED_TOPLEVEL_RULE = "call.unresolved-toplevel"
25
26
 
26
27
  # `undefined method `foo' for <receiver>`
27
28
  UNDEF_METHOD = /\Aundefined method [`'"]([^`'"]+)['"`] for (.+)\z/
@@ -104,9 +105,19 @@ module Rigor
104
105
  # monkey-patch): a known AR method on `Array[...]` deserves
105
106
  # the precise relation-misinference hint, not the generic
106
107
  # "project core-ext" guess H2 would otherwise claim it for.
108
+ #
109
+ # H2K (known project patch) runs before the generic H2: the
110
+ # engine has already proved the defining site via the
111
+ # `project_definition_site` field (ADR-17), so those
112
+ # diagnostics get the high-confidence file-naming hint rather
113
+ # than the spread-based guess. H7 (unresolved toplevel) runs
114
+ # before the systemic / genuine-bug catch-alls so toplevel
115
+ # resolution misses route to `pre_eval:` (ADR-34) instead of
116
+ # reading as scattered bugs.
107
117
  def recognisers
108
118
  %i[h1_activesupport h4_ar_relation h3_gem_without_rbs
109
- h2_monkey_patch h5_systemic_cluster h6_genuine_bugs]
119
+ h2k_known_project_patch h2_monkey_patch h7_unresolved_toplevel
120
+ h5_systemic_cluster h6_genuine_bugs]
110
121
  end
111
122
 
112
123
  # --- H1 — likely ActiveSupport core_ext --------------------
@@ -127,6 +138,31 @@ module Rigor
127
138
  ), matched]
128
139
  end
129
140
 
141
+ # --- H2K — known project monkey-patch (engine-proven) ------
142
+ # ADR-17 / WD3 slice 4: the `call.undefined-method` rule sets
143
+ # `project_definition_site` when the project itself defines the
144
+ # called method on the receiver class somewhere in the file set
145
+ # (a reopened core/stdlib/gem class the dispatcher does not
146
+ # apply cross-file). That is direct evidence — not a spread
147
+ # heuristic — so this recogniser is `:likely` and names the
148
+ # defining files outright. It runs before the generic H2.
149
+ def h2k_known_project_patch(pool)
150
+ matched = pool.select(&:project_definition_site)
151
+ return nil if matched.empty?
152
+
153
+ files = matched.map { |d| d.project_definition_site.sub(/:\d+\z/, "") }
154
+ .uniq.sort
155
+ [Hint.new(
156
+ id: "project-monkey-patch-known", confidence: :likely,
157
+ diagnostic_count: matched.size,
158
+ summary: "#{matched.size} undefined-method site(s) resolve to project " \
159
+ "definitions in #{files.first(3).join(', ')} — reopened core/" \
160
+ "stdlib/gem classes Rigor does not apply cross-file",
161
+ action: "List #{files.size == 1 ? 'this file' : 'these files'} in " \
162
+ "`.rigor.yml`'s `pre_eval:` (ADR-17): #{files.join(', ')}"
163
+ ), matched]
164
+ end
165
+
130
166
  # --- H2 — likely a project monkey-patch / refinement -------
131
167
  def h2_monkey_patch(pool)
132
168
  groups = undefined_method_groups(pool).select do |(_method, _recv), diags|
@@ -179,6 +215,30 @@ module Rigor
179
215
  ), matched]
180
216
  end
181
217
 
218
+ # --- H7 — unresolved toplevel implicit-self calls ----------
219
+ # ADR-34: `call.unresolved-toplevel` fires on a toplevel
220
+ # implicit-self call (no receiver, outside any def / class /
221
+ # module) that resolves against no visible contributor. The
222
+ # canonical opt-out is `pre_eval:` — the file is usually a
223
+ # script relying on methods defined by a monkey-patch or a
224
+ # required helper Rigor did not walk. Grouped, not per-site,
225
+ # so the report names the cluster once.
226
+ def h7_unresolved_toplevel(pool)
227
+ matched = pool.select { |d| rule_of(d) == UNRESOLVED_TOPLEVEL_RULE }
228
+ return nil if matched.empty?
229
+
230
+ files = matched.map(&:path).uniq.sort
231
+ [Hint.new(
232
+ id: "unresolved-toplevel", confidence: :possible,
233
+ diagnostic_count: matched.size,
234
+ summary: "#{matched.size} toplevel call(s) resolve to nothing visible " \
235
+ "across #{files.size} file(s) (#{top_methods(matched, parser: :toplevel)})",
236
+ action: "If a monkey-patch or required helper defines these, list its " \
237
+ "file in `.rigor.yml`'s `pre_eval:` (ADR-17); otherwise they may " \
238
+ "be genuine typos or missing requires."
239
+ ), matched]
240
+ end
241
+
182
242
  # --- H5 — systemic single-file cluster ---------------------
183
243
  def h5_systemic_cluster(pool)
184
244
  bucket = pool.group_by { |d| [d.path, rule_of(d)] }
@@ -282,10 +342,16 @@ module Rigor
282
342
  groups.keys.first(3).map { |method, recv| "`#{method}` on #{recv}" }.join(", ")
283
343
  end
284
344
 
285
- def top_methods(diagnostics, limit: 5)
286
- diagnostics.filter_map { |d| parse_undefined_method(d)&.fetch(:method) }
287
- .tally.sort_by { |method, count| [-count, method] }
288
- .first(limit).map { |method, count| "#{method}×#{count}" }.join(" ")
345
+ # `parser: :undefined_method` (default) reads the method from
346
+ # the parsed `undefined-method` shape; `parser: :toplevel`
347
+ # reads the structured `method_name` field directly (the
348
+ # `unresolved-toplevel` rule carries no receiver to parse).
349
+ def top_methods(diagnostics, limit: 5, parser: :undefined_method)
350
+ names = diagnostics.filter_map do |d|
351
+ parser == :toplevel ? d.method_name : parse_undefined_method(d)&.fetch(:method)
352
+ end
353
+ names.tally.sort_by { |method, count| [-count, method] }
354
+ .first(limit).map { |method, count| "#{method}×#{count}" }.join(" ")
289
355
  end
290
356
 
291
357
  def rule_of(diag)
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.1.13"
4
+ VERSION = "0.1.15"
5
5
  end
@@ -51,6 +51,7 @@ module Rigor
51
51
  def self.reset_default!: () -> void
52
52
  def self.build_env_for: (libraries: Array[String], signature_paths: Array[String | _ToPath]) -> untyped
53
53
  def self.vendored_gem_sig_paths: () -> Array[Pathname]
54
+ def self.vendored_gem_names: () -> Array[String]
54
55
 
55
56
  def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath], ?cache_store: untyped?) -> void
56
57
  def class_known?: (String | Symbol name) -> bool
data/sig/rigor/scope.rbs CHANGED
@@ -15,6 +15,7 @@ module Rigor
15
15
  attr_reader in_source_constants: Hash[String, Type::t]
16
16
  attr_reader discovered_methods: Hash[String, Hash[Symbol, Symbol]]
17
17
  attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
18
+ attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
18
19
  attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
19
20
  attr_reader discovered_superclasses: Hash[String, String]
20
21
  attr_reader discovered_includes: Hash[String, Array[String]]
@@ -56,7 +57,9 @@ module Rigor
56
57
  def with_discovered_methods: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
57
58
  def discovered_method?: (String | Symbol class_name, String | Symbol method_name, Symbol kind) -> bool
58
59
  def with_discovered_def_nodes: (Hash[String, Hash[Symbol, untyped]] table) -> Scope
60
+ def with_discovered_def_sources: (Hash[String, Hash[Symbol, String]] table) -> Scope
59
61
  def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
62
+ def user_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?
60
63
  def top_level_def_for: (String | Symbol method_name) -> untyped?
61
64
  def toplevel?: () -> bool
62
65
  def with_discovered_method_visibilities: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
@@ -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.
@@ -35,13 +35,51 @@ Also note per-gem markers that have their own plugin: `devise`,
35
35
  `rbs_collection.auto_detect: true` (the default).
36
36
  - **Lockfile present, `.gem_rbs_collection/` absent** → the lockfile
37
37
  was generated but the gems were never downloaded. Rigor loads the
38
- lockfile but finds no RBS files. Note this for Phase 6: if triage
39
- reports `gem-without-rbs` hints for gems that appear in
40
- `rbs_collection.yaml`, run `rbs collection install` first and
41
- re-run triage.
38
+ lockfile but finds no RBS files. **Act now see below.**
42
39
  - **Both absent** → note it; Phase 6's triage may recommend
43
40
  `rbs collection install` if `gem-without-rbs` hints appear.
44
41
 
42
+ ### RBS collection — install if absent
43
+
44
+ When `rbs_collection.lock.yaml` is present **and** `.gem_rbs_collection/`
45
+ is absent, the collection was configured (e.g. by Steep or `rbs_rails`)
46
+ but never installed. Installing it now — before writing the config or
47
+ running triage — gives Rigor community RBS for dozens of gems and avoids
48
+ a triage → config-fix → re-triage cycle.
49
+
50
+ Offer to run:
51
+
52
+ ```sh
53
+ bundle exec rbs collection install
54
+ ```
55
+
56
+ Use `bundle exec` when `rbs` appears in `Gemfile` or `Gemfile.lock`
57
+ (the common case — Steep / rbs_rails workflows put it there). If `rbs`
58
+ is not in the Gemfile, use the bare `rbs collection install` instead.
59
+
60
+ Ask the user for permission before running, since this installs files
61
+ into `.gem_rbs_collection/` (a generated directory that belongs in
62
+ `.gitignore`). Only proceed with their confirmation.
63
+
64
+ After installation, verify with:
65
+
66
+ ```sh
67
+ ls .gem_rbs_collection/
68
+ ```
69
+
70
+ The directory should contain RBS gem subdirectories. Continue to
71
+ Phase 2 — the collection will be auto-detected by Rigor at analysis
72
+ time.
73
+
74
+ **Note: RBS collision after install.** Some gems (e.g. `cgi`, `logger`,
75
+ `base64`) were extracted from Ruby's stdlib into standalone gems from
76
+ Ruby 3.3 onwards. When these appear in both `.gem_rbs_collection/` and
77
+ Rigor's bundled stdlib, `rigor triage` may print a
78
+ `RBS::DuplicatedDeclarationError`. If this happens, note the error and
79
+ continue — plugin-based diagnostics are unaffected. File a Rigor issue
80
+ at <https://github.com/rigortype/rigor/issues> so the engine can
81
+ deduplicate stdlib gems from the collection automatically.
82
+
45
83
  ### Path scope
46
84
 
47
85
  Note the conventional source roots so Phase 4 can set `paths:`:
@@ -18,6 +18,23 @@ config as `.rigor.dist.yml` lets a contributor opt out locally (for
18
18
  example, run without the baseline) without touching the committed
19
19
  file.
20
20
 
21
+ ## Selecting `target_ruby`
22
+
23
+ Read the project's declared Ruby version from `.ruby-version`, the
24
+ `Gemfile` `ruby "…"` line, or `.tool-versions`. Use the **minimum**
25
+ version of the declared range (e.g. `>= 3.2.0, < 3.5.0` → `3.2`).
26
+
27
+ Then apply this decision table — **do not ask the user**; act
28
+ automatically and emit the message described:
29
+
30
+ | Declared minimum | `target_ruby` to write | Action |
31
+ | --- | --- | --- |
32
+ | 4.x | `"4.0"` (or exact patch) | Write as-is. |
33
+ | 3.3 – 3.x | `"3.3"` (or exact minor) | Write as-is. |
34
+ | 3.0 – 3.2 | `"3.3"` | Write `"3.3"` automatically. Emit: *"Project targets Ruby X.Y, but Prism (the parser bundled with rigortype) supports Ruby 3.3 as its minimum. Setting `target_ruby: \"3.3\"` — syntax parsing is compatible across 3.0–3.3, but RBS type definitions in `.gem_rbs_collection/` or community libraries may be authored for 3.3+ and could produce type-level false positives on APIs that changed between 3.0 and 3.3."* |
35
+ | 2.x | — | **Stop.** Emit: *"Project targets Ruby 2.x. Rigor's bundled Prism parser does not support Ruby 2.x syntax (removed keyword argument syntax, pattern matching differences). Rigor cannot reliably analyse this project. Consider upgrading the project to Ruby 3.3+ before onboarding Rigor."* Do not write a config; do not continue the skill. |
36
+ | Unknown / not declared | `"4.0"` (Rigor's default) | Write as-is; note that the default was used. |
37
+
21
38
  ## Severity profile — follows the mode
22
39
 
23
40
  The `severity_profile:` key re-stamps every rule's severity. Set it