rigortype 0.1.12 → 0.1.14

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.
@@ -0,0 +1,155 @@
1
+ # 02 — The walker and types
2
+
3
+ Covers **Phase 2** — making `diagnostics_for_file` analyse the AST,
4
+ emit diagnostics, and (optionally) contribute return types.
5
+
6
+ ## `diagnostics_for_file` — the core hook
7
+
8
+ ```ruby
9
+ def diagnostics_for_file(path:, scope:, root:)
10
+ # root — Prism::Node, the parsed file (a ProgramNode).
11
+ # scope — answers inferred-type queries: scope.type_of(node).
12
+ # path — the file path, for Diagnostic#path.
13
+ # → return Array<Rigor::Analysis::Diagnostic>
14
+ end
15
+ ```
16
+
17
+ Walk `root` with a Prism visitor or a recursive descent, recognise
18
+ the DSL's call shapes, and collect diagnostics. Keep the walk in a
19
+ separate `Analyzer` class once it grows past a few methods — pass it
20
+ `path` and let it return the diagnostic array.
21
+
22
+ ### Recognising call sites
23
+
24
+ Most DSL plugins key off `Prism::CallNode`:
25
+
26
+ ```ruby
27
+ def each_call(node, &block)
28
+ block.call(node) if node.is_a?(Prism::CallNode)
29
+ node&.compact_child_nodes&.each { |child| each_call(child, &block) }
30
+ end
31
+ ```
32
+
33
+ Then match on `node.name` (the method name, a Symbol),
34
+ `node.receiver`, and `node.arguments&.arguments`.
35
+
36
+ ## Building a `Diagnostic`
37
+
38
+ Every diagnostic the plugin emits is a `Rigor::Analysis::Diagnostic`.
39
+ A small constructor helper keeps the call sites clean:
40
+
41
+ ```ruby
42
+ def diagnostic(path, node, severity:, rule:, message:)
43
+ loc = node.location
44
+ Rigor::Analysis::Diagnostic.new(
45
+ path: path,
46
+ line: loc.start_line,
47
+ column: loc.start_column + 1, # 1-based column
48
+ message: message,
49
+ severity: severity, # :error | :warning | :info
50
+ rule: rule # short kebab-case id
51
+ )
52
+ end
53
+ ```
54
+
55
+ `rule` is a short identifier (`dimension-mismatch`,
56
+ `unknown-state`). Rigor namespaces it under your plugin —
57
+ diagnostics surface as `plugin.<manifest.id>.<rule>`, and that
58
+ qualified id is what `.rigor.yml` `disable:` and the baseline key
59
+ on. Pick `severity`:
60
+
61
+ - `:error` — a real defect (a type mismatch, a call that will
62
+ raise). Fails `rigor check`.
63
+ - `:warning` — suspicious but not certainly wrong.
64
+ - `:info` — informational; surfaces inferred facts without
65
+ judgement.
66
+
67
+ ## Asking the analyzer for types — `scope.type_of`
68
+
69
+ A plugin does not have to infer types itself. `scope.type_of(node)`
70
+ returns the type the core analyzer inferred for any AST node — the
71
+ plugin can build on it:
72
+
73
+ ```ruby
74
+ receiver_type = scope.type_of(call_node.receiver)
75
+ ```
76
+
77
+ The returned object is one of the `Rigor::Type::*` carriers. The
78
+ ones a plugin meets most:
79
+
80
+ - `Rigor::Type::Nominal` — a class type; `#class_name` is the
81
+ String.
82
+ - `Rigor::Type::Constant` — a literal value; `#value` is the Ruby
83
+ object.
84
+ - `Rigor::Type::IntegerRange` — a bounded integer.
85
+
86
+ Match with `case`/`when` on the carrier class. Treat any carrier you
87
+ do not recognise as "decline to act" — never crash on an unexpected
88
+ type.
89
+
90
+ ## Optional — contribute a return type with `flow_contribution_for`
91
+
92
+ A plugin can do more than emit diagnostics: it can *supply* the
93
+ inferred return type for a call site the core analyzer would
94
+ otherwise type as `Dynamic`. Implement `flow_contribution_for`:
95
+
96
+ ```ruby
97
+ def flow_contribution_for(call_node:, scope:)
98
+ return nil unless call_node.is_a?(Prism::CallNode)
99
+ # ... decide the call site's real return type ...
100
+ return nil if undecidable # nil = "I have nothing to add"
101
+
102
+ Rigor::FlowContribution.new(
103
+ return_type: a_rigor_type,
104
+ provenance: Rigor::FlowContribution::Provenance.new(
105
+ source_family: "plugin.#{manifest.id}",
106
+ plugin_id: manifest.id,
107
+ node: call_node,
108
+ descriptor: nil
109
+ )
110
+ )
111
+ end
112
+ ```
113
+
114
+ Build the `return_type` with `Rigor::Type::Combinator`:
115
+
116
+ ```ruby
117
+ Rigor::Type::Combinator.nominal_of("Money") # a class type
118
+ Rigor::Type::Combinator.constant_of(true) # a literal
119
+ Rigor::Type::Combinator.union(a, b) # a union
120
+ ```
121
+
122
+ Returning `nil` is always safe — it means "no contribution", and the
123
+ core analyzer keeps its own answer. Contribute a type only when the
124
+ plugin is confident; a wrong contribution propagates downstream.
125
+
126
+ This hook is the most contract-sensitive surface — it is the part
127
+ most likely to shift before v0.2.0. Implement it only if the plugin
128
+ genuinely needs to sharpen call-site types; a diagnostics-only
129
+ plugin can skip it entirely.
130
+
131
+ ## Shipping RBS for the DSL
132
+
133
+ 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.
148
+
149
+ ## Output of this module
150
+
151
+ A plugin whose `diagnostics_for_file` recognises the DSL and emits
152
+ diagnostics with correct severities and rule ids — optionally a
153
+ `flow_contribution_for` and a `sig/` bundle. Verify by eye with
154
+ `rigor check`; lock it down with tests in Phase 3
155
+ ([`03-test-and-ship.md`](03-test-and-ship.md)).
@@ -0,0 +1,163 @@
1
+ # 03 — Test and ship
2
+
3
+ Covers **Phase 3** — testing the plugin from outside the rigor
4
+ monorepo, pinning against the pre-1.0 contract, and shipping.
5
+
6
+ ## Testing — drive the public CLI
7
+
8
+ The rigor monorepo's plugin specs use an internal `plugin_helpers.rb`
9
+ (`run_plugin`, `plugin_diagnostics`). That helper is **not part of
10
+ the published `rigortype` surface** — an external plugin cannot use
11
+ it. Test against what you *do* have: the `rigor` CLI.
12
+
13
+ The robust pattern is a **fixture project + `rigor check --format
14
+ json`**. It exercises the real load path, the real walker, the real
15
+ diagnostic pipeline — exactly what your users get — and works
16
+ identically under RSpec or Minitest.
17
+
18
+ ### Fixture layout
19
+
20
+ ```text
21
+ spec/fixtures/basic/ (or test/fixtures/basic/)
22
+ ├── .rigor.yml # plugins: [rigor-<id>]
23
+ └── sample.rb # code that exercises the plugin
24
+ ```
25
+
26
+ The fixture's `.rigor.yml` activates the plugin and scopes `paths:`
27
+ to the sample file:
28
+
29
+ ```yaml
30
+ paths:
31
+ - sample.rb
32
+ plugins:
33
+ - rigor-<id>
34
+ ```
35
+
36
+ ### RSpec
37
+
38
+ ```ruby
39
+ # spec/rigor_<id>_spec.rb
40
+ require "json"
41
+ require "open3"
42
+
43
+ RSpec.describe "rigor-<id>" do
44
+ def diagnostics_for(fixture)
45
+ dir = File.expand_path("fixtures/#{fixture}", __dir__)
46
+ out, _err, _status = Open3.capture3(
47
+ "bundle", "exec", "rigor", "check", "--format", "json", chdir: dir
48
+ )
49
+ JSON.parse(out).fetch("diagnostics")
50
+ end
51
+
52
+ it "flags a dimensional mismatch" do
53
+ diags = diagnostics_for("basic")
54
+ rule = diags.map { |d| d["rule"] }
55
+ expect(rule).to include("plugin.<id>.dimension-mismatch")
56
+ end
57
+ end
58
+ ```
59
+
60
+ ### Minitest
61
+
62
+ ```ruby
63
+ # test/rigor_<id>_test.rb
64
+ require "minitest/autorun"
65
+ require "json"
66
+ require "open3"
67
+
68
+ class RigorPluginTest < Minitest::Test
69
+ def diagnostics_for(fixture)
70
+ dir = File.expand_path("fixtures/#{fixture}", __dir__)
71
+ out, = Open3.capture3("bundle", "exec", "rigor", "check",
72
+ "--format", "json", chdir: dir)
73
+ JSON.parse(out).fetch("diagnostics")
74
+ end
75
+
76
+ def test_flags_dimensional_mismatch
77
+ rules = diagnostics_for("basic").map { |d| d["rule"] }
78
+ assert_includes rules, "plugin.<id>.dimension-mismatch"
79
+ end
80
+ end
81
+ ```
82
+
83
+ `rigor check --format json` emits one object per run; the
84
+ `"diagnostics"` array carries `path` / `line` / `column` / `message`
85
+ / `severity` / `rule`. Assert on `rule` and `severity` — they are the
86
+ stable fields. Avoid asserting on exact `message` wording; it changes
87
+ between Rigor releases.
88
+
89
+ ### Unit-test the pure parts directly
90
+
91
+ Dispatch tables, parsers, and dimension maths inside the plugin are
92
+ plain Ruby — test them as ordinary objects, no `rigor` process
93
+ needed:
94
+
95
+ ```ruby
96
+ it "dispatches distance / time to speed" do
97
+ result = Rigor::Plugin::Units::MethodTable.dispatch(
98
+ receiver: :distance, method: :/, args: [:time]
99
+ )
100
+ expect(result.dimension).to eq(:speed)
101
+ end
102
+ ```
103
+
104
+ Split the suite: fast unit tests for the logic, a few fixture-driven
105
+ CLI tests for the end-to-end wiring.
106
+
107
+ ## Version pinning — the pre-1.0 contract
108
+
109
+ The plugin contract is **not frozen until `rigortype` v0.2.0** (see
110
+ SKILL.md). Concretely:
111
+
112
+ - Gemspec / Gemfile: `rigortype` `>= 0.1.0, < 0.2.0`. Never `>= 0.1`
113
+ alone — that floats across the contract-changing v0.2.0 boundary.
114
+ - Your plugin's own version is normal semver, independent of
115
+ `rigortype`'s.
116
+ - When you bump the `rigortype` pin to a new minor, **re-run the
117
+ full test suite** — the walker hook signature or the `Diagnostic` /
118
+ type-carrier shapes may have changed. The fixture CLI tests are
119
+ what catch a contract drift.
120
+ - State the supported `rigortype` range in the README so users do
121
+ not pair the plugin with an incompatible Rigor.
122
+
123
+ When v0.2.0 lands, re-pin to the stable range and ordinary
124
+ compatibility rules apply.
125
+
126
+ ## README
127
+
128
+ A plugin README should carry:
129
+
130
+ 1. **What it does** — the DSL / framework it teaches Rigor, and one
131
+ sample diagnostic.
132
+ 2. **Install** — `gem "rigor-<id>"` (or the path-gem snippet for a
133
+ project-private plugin).
134
+ 3. **Activate** — the `.rigor.yml` `plugins:` entry, plus any
135
+ `config:` keys and `signature_paths:`.
136
+ 4. **Compatibility** — the supported `rigortype` version range, and
137
+ the pre-1.0-contract caveat.
138
+ 5. **License.**
139
+
140
+ ## Ship
141
+
142
+ **Standalone gem** — `gem build rigor-<id>.gemspec` then
143
+ `gem push`. Tag the release. Announce the supported `rigortype`
144
+ range.
145
+
146
+ **Project-private plugin** — nothing to publish. Commit it with the
147
+ app (the `rigor-plugin/` path-gem directory, or the `RUBYLIB` file).
148
+ Make sure CI runs `rigor check` so the plugin stays
149
+ wired and the fixture tests run.
150
+
151
+ Either way, if the plugin uncovered a gap that *should* be core
152
+ Rigor behaviour — or if you hit a plugin-contract rough edge — report
153
+ it: <https://github.com/rigortype/rigor/issues>. External plugin
154
+ authors are the main source of pre-v0.2.0 contract feedback.
155
+
156
+ ## Output of this module — plugin shipped
157
+
158
+ - A test suite: fast unit tests + fixture-driven `rigor check`
159
+ tests, in RSpec or Minitest.
160
+ - A `rigortype` pin tight to `< 0.2.0`.
161
+ - A README stating the compatibility range.
162
+ - The plugin published as a gem, or committed project-private with
163
+ CI wired.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: rigor-project-init
3
+ description: |
4
+ Onboard a project to Rigor type-checking from scratch: detect the stack, choose an adoption mode (baseline vs. strict), select plugins, write `.rigor.dist.yml`, then snapshot a baseline or commit to a zero-diagnostic gate. Triggers: "set up Rigor in this project", "configure rigor for X", "add type checking", or running `rigor check` in a Gemfile directory with no `.rigor.yml`. NOT for reducing an existing baseline (use rigor-baseline-reduce) or authoring a plugin (use rigor-plugin-author).
5
+ license: MPL-2.0
6
+ metadata:
7
+ version: 0.1.0
8
+ homepage: https://github.com/rigortype/rigor
9
+ ---
10
+
11
+ # Rigor Project Init
12
+
13
+ End-to-end onboarding for a project that has never run Rigor. The
14
+ output is a committed `.rigor.dist.yml`, an explicit adoption mode,
15
+ and — if the project chooses it — a `.rigor-baseline.yml` snapshot.
16
+
17
+ This skill is for **users adopting Rigor on their own project**. It
18
+ uses the published `rigor` executable, installed standalone — Rigor
19
+ is a tool, not a library, so it does **not** go in the project's
20
+ `Gemfile`. See the manual's
21
+ [Installing Rigor](../../docs/manual/01-installation.md)
22
+ chapter for the install channels (`mise` recommended). This skill
23
+ references only public CLI flags and config keys — the same surface
24
+ `rigor --help` documents.
25
+
26
+ ## Phase 0 — When to use this skill
27
+
28
+ Trigger when the user says "set up Rigor here", "configure rigor for
29
+ this app", "add type checking", or runs `rigor check` in a project
30
+ that has no `.rigor.yml` / `.rigor.dist.yml`.
31
+
32
+ Do NOT trigger for:
33
+
34
+ - **An existing baseline the user wants to shrink** — that is the
35
+ `rigor-baseline-reduce` skill.
36
+ - **Writing a Rigor plugin** for the project's own DSL /
37
+ metaprogramming — that is the `rigor-plugin-author` skill. (This
38
+ skill *points at* plugin authoring as an escalation in Phase 7;
39
+ it does not do it.)
40
+ - **Tweaking an already-configured project** — ordinary edits to an
41
+ existing `.rigor.yml`; no onboarding pipeline needed.
42
+
43
+ ## Note — plugin installation
44
+
45
+ All `rigor-*` plugins ship **bundled inside the `rigortype` gem**.
46
+ No separate installation is needed. Listing a plugin under
47
+ `plugins:` in `.rigor.dist.yml` is sufficient to activate it.
48
+ The project's `Gemfile` is untouched by this workflow.
49
+ See [`references/02-configure.md`](references/02-configure.md)
50
+ § "No separate installation needed" for detail.
51
+
52
+ ## The central decision — adoption mode
53
+
54
+ A mature Ruby codebase routinely reports hundreds to thousands of
55
+ diagnostics the first time `rigor check` runs. Most are not fresh
56
+ bugs: some are real-but-empirically-safe (`T | nil` that production
57
+ always initialises), some are project style, a minority are genuine
58
+ latent bugs. Forcing every site to zero before adoption blocks
59
+ adoption entirely.
60
+
61
+ So **before writing any config, present the user with two modes**
62
+ and let them choose. The mode drives the severity profile, whether a
63
+ baseline is generated, and how Phase 7 frames the leftover
64
+ diagnostics.
65
+
66
+ | | **Acknowledge mode** (baseline adoption) | **Strict mode** (no compromise) |
67
+ | --- | --- | --- |
68
+ | Goal | Adopt Rigor now; make sure *ordinary coding does not increase* the diagnostic count. | Drive the project to zero outstanding diagnostics and keep it there. |
69
+ | Today's diagnostics | Snapshotted into `.rigor-baseline.yml`; suppressed as long as the count does not grow. | All surfaced; every one is fixed or consciously suppressed. |
70
+ | Hard-to-fix diagnostics | Left in the baseline. The project **trusts its test / spec suite** to cover runtime correctness for those sites — the static `T | nil` reading is worst-case-sound, the suite proves the worst case is not hit. | Fixed, or annotated `# rigor:disable <rule>` with an author-intent reason at the specific line. No blanket suppression. |
71
+ | `severity_profile` | `lenient` (or `balanced` for a small project). | `strict`. |
72
+ | Best for | Mature codebases; incremental adoption; teams that want the regression guard without a big upfront fix. | New / small projects; libraries; teams that want the maximum guarantee and have the budget to reach zero. |
73
+ | New diagnostics later | Surface immediately — anything beyond the baseline envelope is a regression. | Surface immediately — there is no envelope; every diagnostic is live. |
74
+
75
+ Both modes give the same core guarantee: **a change that introduces
76
+ a new diagnostic is caught.** They differ only in what happens to the
77
+ diagnostics that exist *today*. Acknowledge mode parenthesises them
78
+ behind a baseline and leans on the test suite; strict mode refuses to
79
+ parenthesise anything.
80
+
81
+ If the project's first `rigor check` reports more than ~100 errors,
82
+ recommend acknowledge mode as the default and let the user override.
83
+ Below that, either mode is reasonable — ask.
84
+
85
+ ## Phase outline
86
+
87
+ | Phase | What | Reference |
88
+ | --- | --- | --- |
89
+ | 1 | Detect the project shape (Gemfile / Gemfile.lock walk). | [`references/01-detect.md`](references/01-detect.md) |
90
+ | 2 | Present the two adoption modes; record the user's choice. | (this file — § "The central decision") |
91
+ | 3 | Select the plugin set matching the detected stack. | [`references/01-detect.md`](references/01-detect.md) |
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
+ | 5 | Generate initial RBS sigs; uplift attr_reader precision with `--params=observed`. | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) |
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
+ | 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
+
98
+ Load each reference when you reach its phase. Phases run in order;
99
+ the only branch is Phase 7 (acknowledge mode runs it, strict mode
100
+ skips it). Phase 5 is also optional if the project already has a
101
+ committed `sig/` directory.
102
+
103
+ ## Reading order — modules
104
+
105
+ | Module | Read | Covers |
106
+ | --- | --- | --- |
107
+ | 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
+ | 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
+ | 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. |
111
+
112
+ ## Escalation paths (Phase 7 preview)
113
+
114
+ Some diagnostic clusters are neither a quick fix nor honest baseline
115
+ material. Two of them have a dedicated answer this skill hands off to:
116
+
117
+ - **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.
122
+ - **An external gem Rigor does not understand** — a dependency ships
123
+ no RBS and Rigor has no built-in coverage for it. Try
124
+ `rbs collection install` first; if that gem genuinely needs Rigor
125
+ support, **open an issue on the Rigor project** asking for it:
126
+ <https://github.com/rigortype/rigor/issues>.
127
+
128
+ Neither is a Phase 7 obligation — they are options to *offer* the
129
+ user when the triage report points at one of these causes.
@@ -0,0 +1,139 @@
1
+ # 01 — Detect the project shape & select plugins
2
+
3
+ Covers **Phase 1** (detect) and **Phase 3** (plugin selection). Run
4
+ Phase 2 — the mode choice — from `SKILL.md` between them.
5
+
6
+ ## Phase 1 — Detect
7
+
8
+ Read two files at the project root. Do not run code; just parse them.
9
+
10
+ ### `Gemfile` — framework family
11
+
12
+ Scan the `gem "…"` lines for the markers below. A project can match
13
+ more than one row (a Rails app with RSpec and Sidekiq matches three).
14
+
15
+ | Marker gems | Family |
16
+ | --- | --- |
17
+ | `rails`, `railties`, `actionpack`, `activerecord` | Rails |
18
+ | `sinatra` | Sinatra |
19
+ | `dry-types`, `dry-struct`, `dry-schema`, `dry-validation` | dry-rb |
20
+ | `rspec`, `rspec-core` | RSpec test suite |
21
+ | `sorbet`, `sorbet-runtime` | Sorbet-typed |
22
+ | none of the above | plain Ruby |
23
+
24
+ Also note per-gem markers that have their own plugin: `devise`,
25
+ `pundit`, `sidekiq`.
26
+
27
+ ### `Gemfile.lock` — versions & RBS state
28
+
29
+ - Read the **locked versions** of the framework gems — a plugin
30
+ recommendation can depend on a major version.
31
+ - Check for `rbs_collection.lock.yaml` at the project root AND whether
32
+ `.gem_rbs_collection/` exists alongside it.
33
+ - **Lockfile present, `.gem_rbs_collection/` present** → the collection
34
+ is installed; Rigor will auto-detect and consume it via
35
+ `rbs_collection.auto_detect: true` (the default).
36
+ - **Lockfile present, `.gem_rbs_collection/` absent** → the lockfile
37
+ was generated but the gems were never downloaded. Rigor loads the
38
+ lockfile but finds no RBS files. **Act now — see below.**
39
+ - **Both absent** → note it; Phase 6's triage may recommend
40
+ `rbs collection install` if `gem-without-rbs` hints appear.
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
+
83
+ ### Path scope
84
+
85
+ Note the conventional source roots so Phase 4 can set `paths:`:
86
+
87
+ - Rails → `app`, `lib`.
88
+ - gem / library → `lib`.
89
+ - plain app → `lib`, or the directory holding the code.
90
+
91
+ `spec/` and `test/` are normally **excluded** from `paths:` — they
92
+ are checked differently and inflate the diagnostic count. `vendor/`
93
+ and `tmp/` are always excluded.
94
+
95
+ ## Phase 3 — Plugin selection
96
+
97
+ Propose a plugin set from the detected families. Present it to the
98
+ user as a list they can trim — do not silently enable everything.
99
+
100
+ | Family | Recommended plugins |
101
+ | --- | --- |
102
+ | Rails | `rigor-actionpack`, `rigor-activerecord`, `rigor-actionmailer`, `rigor-rails-routes`, `rigor-rails-i18n`, plus `rigor-activesupport-core-ext` (almost always needed — see below) |
103
+ | dry-rb | `rigor-dry-types`, `rigor-dry-struct`, and `rigor-dry-schema` / `rigor-dry-validation` when those gems are present |
104
+ | Sinatra | `rigor-sinatra` |
105
+ | RSpec | `rigor-rspec` |
106
+ | Devise / Pundit / Sidekiq present | `rigor-devise` / `rigor-pundit` / `rigor-sidekiq` |
107
+ | Sorbet present | `rigor-sorbet` (ingests existing `sig` blocks / RBI as type sources) |
108
+ | plain Ruby | none required — the core analyzer covers it |
109
+
110
+ The current production-plugin catalogue is the authority for which
111
+ plugins exist and how each is named / installed:
112
+ <https://github.com/rigortype/rigor/blob/master/plugins/README.md>.
113
+ The set drifts as new plugins land — consult that page rather than
114
+ treating the table above as exhaustive.
115
+
116
+ ### `rigor-activesupport-core-ext` — the common Rails gap
117
+
118
+ ActiveSupport monkey-patches the core classes (`3.days`,
119
+ `5.minutes`, `"x".squish`, `Time.current`, …). Without the
120
+ `rigor-activesupport-core-ext` bundle, every such call reports
121
+ `call.undefined-method` — on a real Rails app this is the single
122
+ largest diagnostic cluster (a measured Mastodon run: ~365 of 489
123
+ diagnostics were exactly this). Phase 5's `rigor triage` flags it as
124
+ hint `activesupport-core-ext`.
125
+
126
+ `rigor-activesupport-core-ext` is a **plugin** (an RBS-bundle plugin
127
+ — it contributes signatures, not diagnostics). Activate it like any
128
+ other: list it under `plugins:`. No `signature_paths:` wiring is
129
+ needed — the plugin ships its own `sig/`. Include it for any
130
+ Rails-family project.
131
+
132
+ ## Output of this module
133
+
134
+ - A framework-family list.
135
+ - A proposed, user-trimmed plugin set.
136
+ - The `paths:` / `exclude:` scope for Phase 4.
137
+ - Whether an RBS collection already exists.
138
+
139
+ Carry these into Phase 4 ([`02-configure.md`](02-configure.md)).