rigortype 0.1.11 → 0.1.13
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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/analysis/erb_template_detector.rb +38 -0
- data/lib/rigor/analysis/runner.rb +6 -1
- data/lib/rigor/analysis/worker_session.rb +6 -1
- data/lib/rigor/cli/plugins_command.rb +308 -0
- data/lib/rigor/cli/plugins_renderer.rb +173 -0
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +37 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/inference/block_parameter_binder.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +69 -30
- data/lib/rigor/inference/indexed_narrowing.rb +187 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
- data/lib/rigor/inference/method_dispatcher.rb +23 -0
- data/lib/rigor/inference/mutation_widening.rb +285 -0
- data/lib/rigor/inference/narrowing.rb +72 -4
- data/lib/rigor/inference/scope_indexer.rb +409 -12
- data/lib/rigor/inference/statement_evaluator.rb +256 -4
- data/lib/rigor/scope.rb +195 -4
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
- data/sig/rigor/scope.rbs +23 -0
- data/skills/rigor-baseline-reduce/SKILL.md +100 -0
- data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
- data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
- data/skills/rigor-plugin-author/SKILL.md +95 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
- data/skills/rigor-project-init/SKILL.md +129 -0
- data/skills/rigor-project-init/references/01-detect.md +101 -0
- data/skills/rigor-project-init/references/02-configure.md +185 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +22 -1
|
@@ -0,0 +1,101 @@
|
|
|
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. 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.
|
|
42
|
+
- **Both absent** → note it; Phase 6's triage may recommend
|
|
43
|
+
`rbs collection install` if `gem-without-rbs` hints appear.
|
|
44
|
+
|
|
45
|
+
### Path scope
|
|
46
|
+
|
|
47
|
+
Note the conventional source roots so Phase 4 can set `paths:`:
|
|
48
|
+
|
|
49
|
+
- Rails → `app`, `lib`.
|
|
50
|
+
- gem / library → `lib`.
|
|
51
|
+
- plain app → `lib`, or the directory holding the code.
|
|
52
|
+
|
|
53
|
+
`spec/` and `test/` are normally **excluded** from `paths:` — they
|
|
54
|
+
are checked differently and inflate the diagnostic count. `vendor/`
|
|
55
|
+
and `tmp/` are always excluded.
|
|
56
|
+
|
|
57
|
+
## Phase 3 — Plugin selection
|
|
58
|
+
|
|
59
|
+
Propose a plugin set from the detected families. Present it to the
|
|
60
|
+
user as a list they can trim — do not silently enable everything.
|
|
61
|
+
|
|
62
|
+
| Family | Recommended plugins |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Rails | `rigor-actionpack`, `rigor-activerecord`, `rigor-actionmailer`, `rigor-rails-routes`, `rigor-rails-i18n`, plus `rigor-activesupport-core-ext` (almost always needed — see below) |
|
|
65
|
+
| dry-rb | `rigor-dry-types`, `rigor-dry-struct`, and `rigor-dry-schema` / `rigor-dry-validation` when those gems are present |
|
|
66
|
+
| Sinatra | `rigor-sinatra` |
|
|
67
|
+
| RSpec | `rigor-rspec` |
|
|
68
|
+
| Devise / Pundit / Sidekiq present | `rigor-devise` / `rigor-pundit` / `rigor-sidekiq` |
|
|
69
|
+
| Sorbet present | `rigor-sorbet` (ingests existing `sig` blocks / RBI as type sources) |
|
|
70
|
+
| plain Ruby | none required — the core analyzer covers it |
|
|
71
|
+
|
|
72
|
+
The current production-plugin catalogue is the authority for which
|
|
73
|
+
plugins exist and how each is named / installed:
|
|
74
|
+
<https://github.com/rigortype/rigor/blob/master/plugins/README.md>.
|
|
75
|
+
The set drifts as new plugins land — consult that page rather than
|
|
76
|
+
treating the table above as exhaustive.
|
|
77
|
+
|
|
78
|
+
### `rigor-activesupport-core-ext` — the common Rails gap
|
|
79
|
+
|
|
80
|
+
ActiveSupport monkey-patches the core classes (`3.days`,
|
|
81
|
+
`5.minutes`, `"x".squish`, `Time.current`, …). Without the
|
|
82
|
+
`rigor-activesupport-core-ext` bundle, every such call reports
|
|
83
|
+
`call.undefined-method` — on a real Rails app this is the single
|
|
84
|
+
largest diagnostic cluster (a measured Mastodon run: ~365 of 489
|
|
85
|
+
diagnostics were exactly this). Phase 5's `rigor triage` flags it as
|
|
86
|
+
hint `activesupport-core-ext`.
|
|
87
|
+
|
|
88
|
+
`rigor-activesupport-core-ext` is a **plugin** (an RBS-bundle plugin
|
|
89
|
+
— it contributes signatures, not diagnostics). Activate it like any
|
|
90
|
+
other: list it under `plugins:`. No `signature_paths:` wiring is
|
|
91
|
+
needed — the plugin ships its own `sig/`. Include it for any
|
|
92
|
+
Rails-family project.
|
|
93
|
+
|
|
94
|
+
## Output of this module
|
|
95
|
+
|
|
96
|
+
- A framework-family list.
|
|
97
|
+
- A proposed, user-trimmed plugin set.
|
|
98
|
+
- The `paths:` / `exclude:` scope for Phase 4.
|
|
99
|
+
- Whether an RBS collection already exists.
|
|
100
|
+
|
|
101
|
+
Carry these into Phase 4 ([`02-configure.md`](02-configure.md)).
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# 02 — Write the configuration
|
|
2
|
+
|
|
3
|
+
Covers **Phase 4**. Inputs: the plugin set + path scope from
|
|
4
|
+
[`01-detect.md`](01-detect.md), and the adoption mode chosen in
|
|
5
|
+
Phase 2.
|
|
6
|
+
|
|
7
|
+
## `.rigor.dist.yml` vs `.rigor.yml`
|
|
8
|
+
|
|
9
|
+
Rigor reads both. The convention:
|
|
10
|
+
|
|
11
|
+
- **`.rigor.dist.yml`** — the committed, shared project config. This
|
|
12
|
+
skill writes this file.
|
|
13
|
+
- **`.rigor.yml`** — an optional, gitignored per-developer override.
|
|
14
|
+
Leave it for individuals to create; do not write one here.
|
|
15
|
+
|
|
16
|
+
When both exist, `.rigor.yml` takes precedence. Writing the shared
|
|
17
|
+
config as `.rigor.dist.yml` lets a contributor opt out locally (for
|
|
18
|
+
example, run without the baseline) without touching the committed
|
|
19
|
+
file.
|
|
20
|
+
|
|
21
|
+
## Severity profile — follows the mode
|
|
22
|
+
|
|
23
|
+
The `severity_profile:` key re-stamps every rule's severity. Set it
|
|
24
|
+
from the Phase 2 mode:
|
|
25
|
+
|
|
26
|
+
| Mode | `severity_profile` | Why |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| Acknowledge, >100 errors on first run | `lenient` | Most rules become warnings; the baseline carries the residue. The point is the regression guard, not a wall of errors. |
|
|
29
|
+
| Acknowledge, small project | `balanced` | The default. Errors stay errors; the baseline is small enough to live with. |
|
|
30
|
+
| Strict | `strict` | Promotes borderline rules to errors. Paired with no baseline, every diagnostic is a live gate. |
|
|
31
|
+
|
|
32
|
+
`balanced` is the built-in default — omit the key to get it.
|
|
33
|
+
|
|
34
|
+
## No separate installation needed
|
|
35
|
+
|
|
36
|
+
All plugins ship **bundled inside the `rigortype` gem**. The
|
|
37
|
+
`plugins:` list in the config is all that is needed to activate
|
|
38
|
+
them — the plugin loader runs `require "rigor-<id>"` from within
|
|
39
|
+
the gem's own load path. No Gemfile entry, no `bundle install`,
|
|
40
|
+
no separate gem channel.
|
|
41
|
+
|
|
42
|
+
**The `rigortype` gem itself stays out of the project's
|
|
43
|
+
`Gemfile`** — install it standalone per the manual's
|
|
44
|
+
[Installing Rigor](../../docs/manual/01-installation.md) chapter
|
|
45
|
+
(`mise use gem:rigortype` is the recommended channel). The
|
|
46
|
+
project's Gemfile is untouched by this workflow.
|
|
47
|
+
|
|
48
|
+
## The template
|
|
49
|
+
|
|
50
|
+
Write `.rigor.dist.yml` at the project root. A Rails app in
|
|
51
|
+
acknowledge mode looks like:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
# .rigor.dist.yml — Rigor configuration (committed; shared).
|
|
55
|
+
# Generated by the rigor-project-init workflow.
|
|
56
|
+
|
|
57
|
+
# Match the Ruby version in .ruby-version or the Gemfile `ruby "x.y"` line.
|
|
58
|
+
# Rigor defaults to 4.0; set this explicitly if the project targets Ruby < 4.0.
|
|
59
|
+
target_ruby: "3.4"
|
|
60
|
+
|
|
61
|
+
paths:
|
|
62
|
+
- app
|
|
63
|
+
- lib
|
|
64
|
+
|
|
65
|
+
exclude:
|
|
66
|
+
- vendor
|
|
67
|
+
- tmp
|
|
68
|
+
|
|
69
|
+
plugins:
|
|
70
|
+
- rigor-actionpack
|
|
71
|
+
- rigor-activerecord
|
|
72
|
+
- rigor-actionmailer
|
|
73
|
+
- rigor-rails-routes
|
|
74
|
+
- rigor-rails-i18n
|
|
75
|
+
# An RBS-bundle plugin — ships ActiveSupport core_ext signatures,
|
|
76
|
+
# no signature_paths: wiring needed (see 01-detect.md).
|
|
77
|
+
- rigor-activesupport-core-ext
|
|
78
|
+
|
|
79
|
+
severity_profile: lenient
|
|
80
|
+
|
|
81
|
+
# Phase 6 (acknowledge mode) appends this line after generating the
|
|
82
|
+
# baseline. Strict mode leaves it out entirely.
|
|
83
|
+
# baseline: .rigor-baseline.yml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Existing `sig/` directory
|
|
87
|
+
|
|
88
|
+
If the project already has a `sig/` directory (from Steep, `rbs_rails`,
|
|
89
|
+
or handwritten annotations), wire it into `signature_paths:` so Rigor
|
|
90
|
+
consumes it. Phase 5 (sig uplift) can be skipped, but the paths must
|
|
91
|
+
still be declared — Rigor does not auto-detect `sig/`:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
signature_paths:
|
|
95
|
+
- sig/handwritten # adjust to the layout in your project
|
|
96
|
+
- sig/generated
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
List only directories that contain `.rbs` files or subdirectories of
|
|
100
|
+
them. Rigor walks each path recursively. If the sig layout is a single
|
|
101
|
+
flat `sig/` directory, use `- sig` instead.
|
|
102
|
+
|
|
103
|
+
A strict-mode plain-Ruby gem is shorter:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
paths:
|
|
107
|
+
- lib
|
|
108
|
+
severity_profile: strict
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Key reference
|
|
112
|
+
|
|
113
|
+
Only the keys this skill needs. `rigor --help` and the project
|
|
114
|
+
handbook document the full surface.
|
|
115
|
+
|
|
116
|
+
| Key | Meaning |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `paths:` | Directories Rigor analyses. Source roots only — not `spec/` / `test/`. |
|
|
119
|
+
| `exclude:` | Paths removed from the `paths:` walk. |
|
|
120
|
+
| `plugins:` | Plugin ids to activate (the Phase 3 set). |
|
|
121
|
+
| `signature_paths:` | Extra RBS source **directories** (paths, not gem names; resolved relative to the config file). Use it for the project's own local `sig/` if it has one. RBS-bundle *plugins* like `rigor-activesupport-core-ext` ship their own `sig/` and need no entry here — list them under `plugins:`. |
|
|
122
|
+
| `severity_profile:` | `lenient` / `balanced` / `strict`. See the table above. |
|
|
123
|
+
| `severity_overrides:` | Per-rule severity tweaks. Leave empty at init; the baseline-reduce workflow tunes it later. |
|
|
124
|
+
| `baseline:` | Path to the baseline file. **Only acknowledge mode sets it**, and only in Phase 6 *after* the file exists. Per Rigor's no-magic rule, a `.rigor-baseline.yml` on disk does nothing until this key names it. |
|
|
125
|
+
| `pre_eval:` | Project files Rigor walks before per-file inference — used to register in-project monkey-patches. Leave empty at init; Phase 7 may suggest it. |
|
|
126
|
+
| `dependencies.source_inference:` | Opt-in inference for gems shipping no RBS. Leave empty at init; Phase 7 may suggest it. |
|
|
127
|
+
|
|
128
|
+
## Do not write the baseline yet
|
|
129
|
+
|
|
130
|
+
Phase 4 writes the config with the `baseline:` line **commented out
|
|
131
|
+
or absent**. The baseline file does not exist until Phase 6, and a
|
|
132
|
+
`baseline:` pointing at a missing file is an error. Phase 6 writes
|
|
133
|
+
the file and uncomments / appends the line in one step.
|
|
134
|
+
|
|
135
|
+
Strict mode never adds `baseline:` at all.
|
|
136
|
+
|
|
137
|
+
## Verify plugin activation (`rigor plugins`)
|
|
138
|
+
|
|
139
|
+
Before proceeding to sig uplift, **run `rigor plugins`** from the
|
|
140
|
+
project root to confirm every entry under `plugins:` actually
|
|
141
|
+
loaded. The activation surface is the part of the configuration
|
|
142
|
+
most prone to silent failure — a typoed gem name, a missing
|
|
143
|
+
third-party plugin gem, or running rigor from a different cwd
|
|
144
|
+
(so a different `.rigor.yml` is discovered) all leave plugins
|
|
145
|
+
inert, which the per-file diagnostic stream then attributes to
|
|
146
|
+
"missing types" rather than to a config gap.
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
rigor plugins
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Expected: every entry shows `[OK ]`, the `loaded: N` count
|
|
153
|
+
matches the entries in `plugins:`, and `load-error: 0`. The
|
|
154
|
+
report also lists each plugin's `signature_paths:` (with a
|
|
155
|
+
per-directory `.rbs` file count) and any `open_receivers:` /
|
|
156
|
+
`owns_receivers:` / `produces:` / `consumes:` / macro substrate
|
|
157
|
+
contributions — useful for confirming the plugin is contributing
|
|
158
|
+
what you expect.
|
|
159
|
+
|
|
160
|
+
If any entry shows `[ERR]`, read the `load error:` line:
|
|
161
|
+
|
|
162
|
+
| Error fragment | Cause | Fix |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| `could not load plugin gem "rigor-foo"` | The gem is not on the load path. Either misspelled in `plugins:`, or a third-party plugin (per ADR-31 WD4) that is not installed in the rigor runtime environment. | Check spelling against the catalogue in `01-detect.md`; for third-party plugins, install via the same channel that installed `rigortype` (mise / gem install). |
|
|
165
|
+
| `did not register any plugin via Rigor::Plugin.register` | The require succeeded but the gem did not call `Rigor::Plugin.register`. Usually a version mismatch (older / forked rigor-foo) or a partial install. | Reinstall the plugin gem; verify the version matches `rigortype`'s. |
|
|
166
|
+
| `signature path "..." is not a directory` | The bundled `sig/` directory is missing — a packaging bug in the plugin gem. | File an issue against the plugin gem's repo. |
|
|
167
|
+
| `plugin "foo" config invalid: ...` | The `config:` block under the plugin entry has a typo or wrong value type. | Compare against the plugin's documented `config_schema:`. |
|
|
168
|
+
|
|
169
|
+
Re-run `rigor plugins` until `load-error: 0`. Add `--strict` to
|
|
170
|
+
the invocation when you want it to exit 1 (the canonical CI gate
|
|
171
|
+
shape):
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
rigor plugins --strict
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Output of this module
|
|
178
|
+
|
|
179
|
+
A committed `.rigor.dist.yml` with `paths:`, `exclude:`,
|
|
180
|
+
`plugins:`, and `severity_profile:` set — and no active
|
|
181
|
+
`baseline:` line. `rigor plugins` reports every entry loaded,
|
|
182
|
+
zero load errors. No Gemfile changes; plugins are bundled inside
|
|
183
|
+
`rigortype`.
|
|
184
|
+
|
|
185
|
+
Proceed to Phase 5 ([`04-sig-uplift.md`](04-sig-uplift.md)).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# 03 — Triage, baseline, and surfacing real bugs
|
|
2
|
+
|
|
3
|
+
Covers **Phase 6** (triage), **Phase 7** (baseline — acknowledge mode
|
|
4
|
+
only), and **Phase 8** (real bugs + escalation).
|
|
5
|
+
|
|
6
|
+
## Phase 6 — Triage the diagnostic stream
|
|
7
|
+
|
|
8
|
+
Do **not** read the raw `rigor check` output to decide what to do. A
|
|
9
|
+
mature codebase's raw stream is hundreds of lines and reads as
|
|
10
|
+
hundreds of unrelated problems — it is the wrong first artefact. Run
|
|
11
|
+
the triage command instead:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
rigor triage --format json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`rigor triage` runs the same analysis as `rigor check`, then returns
|
|
18
|
+
a structured summary instead of the per-line dump. It is read-only
|
|
19
|
+
and advisory — it never edits config and never writes a baseline.
|
|
20
|
+
The JSON shape:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"summary": { "total": 489, "error": 480, "warning": 9, "info": 0 },
|
|
25
|
+
"distribution": [ { "rule": "call.undefined-method", "count": 437 } ],
|
|
26
|
+
"hotspots": [ { "file": "app/models/status.rb", "count": 42,
|
|
27
|
+
"by_rule": { "call.undefined-method": 40 } } ],
|
|
28
|
+
"hints": [
|
|
29
|
+
{ "id": "activesupport-core-ext", "confidence": "likely",
|
|
30
|
+
"diagnostic_count": 365, "summary": "...", "action": "..." }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use the three sections like this:
|
|
36
|
+
|
|
37
|
+
- **`summary` / `distribution`** — the scale, and which rules
|
|
38
|
+
dominate. Decides nothing on its own; feeds the mode sanity-check
|
|
39
|
+
(>100 errors → acknowledge mode is the right default).
|
|
40
|
+
- **`hotspots`** — files carrying the most diagnostics. A single hot
|
|
41
|
+
file is often one structural cause, not many bugs.
|
|
42
|
+
- **`hints`** — the heuristic catalogue. Each hint names a *likely
|
|
43
|
+
cause* and a *suggested action*. They are signal, not verdicts —
|
|
44
|
+
the `confidence` field is `likely` or `possible`; verify before
|
|
45
|
+
acting.
|
|
46
|
+
|
|
47
|
+
### Hint catalogue → what to do
|
|
48
|
+
|
|
49
|
+
| Hint `id` | Cause | Where this skill handles it |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `activesupport-core-ext` | ActiveSupport core-class monkey-patches not loaded. | Go back to Phase 3/4: add `rigor-activesupport-core-ext` to `plugins:` (it is an RBS-bundle plugin), re-run triage. This is a config gap, not a bug. |
|
|
52
|
+
| `gem-without-rbs` | A dependency ships no RBS. | Phase 7 escalation — `rbs collection install`, or `dependencies.source_inference:`, or open a Rigor issue. |
|
|
53
|
+
| `project-monkey-patch` | An in-project monkey-patch / refinement Rigor did not see. | Phase 7 escalation — register the defining file via `pre_eval:`, or (if it is a DSL) write a project plugin. |
|
|
54
|
+
| `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
|
+
| `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
|
+
| `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. |
|
|
57
|
+
|
|
58
|
+
If triage flags `activesupport-core-ext` (or any config gap),
|
|
59
|
+
**fix the config and re-run `rigor triage` before continuing**. The
|
|
60
|
+
baseline and the real-bug review should both run against the
|
|
61
|
+
post-config diagnostic set, not the inflated one.
|
|
62
|
+
|
|
63
|
+
## Phase 7 — Generate the baseline (acknowledge mode only)
|
|
64
|
+
|
|
65
|
+
**Strict mode skips this phase entirely.** A strict project has no
|
|
66
|
+
baseline; every diagnostic stays live.
|
|
67
|
+
|
|
68
|
+
In acknowledge mode, snapshot today's diagnostics:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
rigor baseline generate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This writes `.rigor-baseline.yml` at the project root — one
|
|
75
|
+
`(file, rule, count)` bucket per cluster. The command refuses to
|
|
76
|
+
overwrite an existing baseline without `--force`.
|
|
77
|
+
|
|
78
|
+
Then **wire it into the config**. Per Rigor's no-magic rule, the
|
|
79
|
+
baseline file does nothing until `.rigor.dist.yml` names it. Add (or
|
|
80
|
+
uncomment) the line written in Phase 4:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
baseline: .rigor-baseline.yml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`rigor baseline generate` prints a note reminding you of this if the
|
|
87
|
+
config does not yet declare `baseline:`. Do both edits — generate and
|
|
88
|
+
wire — in one step so the user never has a generated baseline that
|
|
89
|
+
silently does nothing.
|
|
90
|
+
|
|
91
|
+
How the baseline behaves afterwards (acknowledge mode's whole point):
|
|
92
|
+
|
|
93
|
+
- A `(file, rule)` bucket is silenced while its live count stays at
|
|
94
|
+
or below the recorded number.
|
|
95
|
+
- If a commit pushes a bucket *over* its recorded count, **every**
|
|
96
|
+
diagnostic in that bucket surfaces — the bucket is now a regression
|
|
97
|
+
to review.
|
|
98
|
+
- New `(file, rule)` pairs that were not in the baseline surface
|
|
99
|
+
immediately.
|
|
100
|
+
|
|
101
|
+
So ordinary coding cannot quietly grow the diagnostic count: the
|
|
102
|
+
baseline is a ceiling, not a blanket. Reducing it later is the
|
|
103
|
+
`rigor-baseline-reduce` skill's job.
|
|
104
|
+
|
|
105
|
+
Commit `.rigor-baseline.yml` — it documents project state.
|
|
106
|
+
|
|
107
|
+
Print the suppression summary for the user: "N diagnostics recorded
|
|
108
|
+
as baseline; M will surface on subsequent runs."
|
|
109
|
+
|
|
110
|
+
## Phase 8 — Surface real bugs & offer escalation
|
|
111
|
+
|
|
112
|
+
The triage `genuine-bugs` hint (and any low-count, scattered rule in
|
|
113
|
+
`distribution`) points at the diagnostics most likely to be **actual
|
|
114
|
+
bugs** — a `nil`-receiver crash on a rarely-exercised line, a typo'd
|
|
115
|
+
method. In **both modes**, surface 2–3 of these to the user and offer
|
|
116
|
+
to walk them: a small, scattered rule is rarely systemic.
|
|
117
|
+
|
|
118
|
+
In acknowledge mode these still went into the baseline — that is
|
|
119
|
+
fine; the baseline is a starting envelope, not a verdict that the
|
|
120
|
+
bug is acceptable. Recommend the user run the `rigor-baseline-reduce`
|
|
121
|
+
skill next to work them down.
|
|
122
|
+
|
|
123
|
+
### Escalation path A — application-specific metaprogramming
|
|
124
|
+
|
|
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.
|
|
139
|
+
|
|
140
|
+
### Escalation path B — an unsupported external gem
|
|
141
|
+
|
|
142
|
+
If triage reports `gem-without-rbs`, a dependency ships no type
|
|
143
|
+
information and Rigor has no built-in coverage. In order:
|
|
144
|
+
|
|
145
|
+
1. `rbs collection install` — pulls community RBS for the gem if it
|
|
146
|
+
exists. Re-run triage afterwards.
|
|
147
|
+
2. `dependencies.source_inference:` in `.rigor.dist.yml` — opt the
|
|
148
|
+
gem into Rigor inferring `Dynamic`-typed returns from its source.
|
|
149
|
+
3. If the gem is widely used and genuinely warrants first-class
|
|
150
|
+
support, **open an issue on the Rigor project** so the maintainers
|
|
151
|
+
can ship a plugin or RBS bundle:
|
|
152
|
+
<https://github.com/rigortype/rigor/issues>. Include the gem name,
|
|
153
|
+
version, and a sample of the diagnostics.
|
|
154
|
+
|
|
155
|
+
Neither escalation is mandatory — offer them when triage points at
|
|
156
|
+
the cause; the user decides whether to act now or defer.
|
|
157
|
+
|
|
158
|
+
## Output of this module — onboarding complete
|
|
159
|
+
|
|
160
|
+
- A committed `.rigor.dist.yml`.
|
|
161
|
+
- Acknowledge mode: a committed `.rigor-baseline.yml` + an active
|
|
162
|
+
`baseline:` line. Strict mode: neither.
|
|
163
|
+
- The user has seen the likely real bugs and knows the two escalation
|
|
164
|
+
paths.
|
|
165
|
+
|
|
166
|
+
Next sessions: `rigor-baseline-reduce` to work the baseline down
|
|
167
|
+
(acknowledge mode), or `rigor-plugin-author` if escalation path A
|
|
168
|
+
applies.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# 04 — Generate initial RBS sigs and uplift precision
|
|
2
|
+
|
|
3
|
+
Covers **Phase 5**. Inputs: the configured `.rigor.dist.yml` from
|
|
4
|
+
Phase 4 and the detected source paths.
|
|
5
|
+
|
|
6
|
+
## Why generate sigs before triaging
|
|
7
|
+
|
|
8
|
+
Rigor's inference engine uses RBS signatures from `sig/` to sharpen
|
|
9
|
+
its type flow. Without them, whole chains of methods fall back to
|
|
10
|
+
`untyped`. Running `rigor triage` against an uninitialized `sig/`
|
|
11
|
+
inflates the diagnostic count with cascade noise that sigs would
|
|
12
|
+
eliminate. Generating sigs first means Phase 6's `rigor triage`
|
|
13
|
+
report is as signal-rich as possible — the `genuine-bugs` hint
|
|
14
|
+
counts bugs, not sig gaps.
|
|
15
|
+
|
|
16
|
+
This phase is **optional if the project already has a `sig/`
|
|
17
|
+
directory** with handwritten RBS annotations. In that case skip
|
|
18
|
+
straight to Phase 6.
|
|
19
|
+
|
|
20
|
+
## Step 5-a — Dry-run sig-gen
|
|
21
|
+
|
|
22
|
+
Inspect what sig-gen would produce without writing anything:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
rigor sig-gen lib # adjust path to match the paths: key
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Typical output at this point: most `new_method` candidates have
|
|
29
|
+
literal or concrete return types (`"hello"`, `42`, `:done`, `nil`).
|
|
30
|
+
Methods whose return type cannot be inferred show up with
|
|
31
|
+
`skip_reason: :untyped_return` — these are the sig precision targets.
|
|
32
|
+
|
|
33
|
+
To get a breakdown in JSON:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
rigor sig-gen --format json lib | ruby -e '
|
|
37
|
+
require "json"
|
|
38
|
+
data = JSON.parse($stdin.read)["candidates"]
|
|
39
|
+
puts "=== classification breakdown ==="
|
|
40
|
+
data.group_by { |c| c["classification"] }
|
|
41
|
+
.sort_by { |k, _| k }
|
|
42
|
+
.each { |k, v| puts " #{k}: #{v.size}" }
|
|
43
|
+
skipped = data.select { |c| c["classification"] == "skipped" }
|
|
44
|
+
puts "\n=== skip reasons ==="
|
|
45
|
+
skipped.group_by { |c| c["skip_reason"] }
|
|
46
|
+
.sort_by { |_, v| -v.size }
|
|
47
|
+
.each { |r, v| puts " #{r}: #{v.size}" }
|
|
48
|
+
'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Step 5-b — Write the baseline sigs
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
rigor sig-gen --write lib
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This creates `sig/**/*.rbs` with one method signature per inferred
|
|
58
|
+
method. Check in a few files:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
head -40 sig/lib/your_class.rbs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
At this point, `attr_reader` and `attr_accessor` methods that rely on
|
|
65
|
+
ivar types set from `initialize` parameters will likely still be
|
|
66
|
+
absent (classified as `:untyped_return`). Step 5-c fixes that.
|
|
67
|
+
|
|
68
|
+
## Step 5-c — Precision uplift with --params=observed
|
|
69
|
+
|
|
70
|
+
`--params=observed` tells sig-gen to collect observed argument types
|
|
71
|
+
from every call site it processes during the analysis pass. The most
|
|
72
|
+
important use case: **`attr_reader` / `attr_writer` / `attr_accessor`
|
|
73
|
+
methods whose `@ivar` is assigned from an `initialize` parameter**.
|
|
74
|
+
|
|
75
|
+
### Example
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
class Person
|
|
79
|
+
attr_reader :name, :age
|
|
80
|
+
|
|
81
|
+
def initialize(name, age)
|
|
82
|
+
@name = name
|
|
83
|
+
@age = age
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Person.new("Alice", 30)
|
|
88
|
+
Person.new("Bob", 25)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Without observations: `attr_reader :name` → skipped as `:untyped_return`
|
|
92
|
+
(the ivar's type is unknown because the blank inference scope never
|
|
93
|
+
sees the parameter values).
|
|
94
|
+
|
|
95
|
+
With `--params=observed`: sig-gen accumulates `name → "Alice" | "Bob"`,
|
|
96
|
+
`age → 25 | 30` from the `Person.new(...)` call sites, then propagates:
|
|
97
|
+
`@name: ("Alice" | "Bob")` → `def name: () -> ("Alice" | "Bob")`.
|
|
98
|
+
|
|
99
|
+
Run:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
rigor sig-gen --params=observed --write lib
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The cascade effect is significant: a single resolved `attr_reader`
|
|
106
|
+
can unlock dozens of downstream methods whose precision depends on it.
|
|
107
|
+
|
|
108
|
+
### Measuring the uplift
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
rigor sig-gen --format json lib | ruby -e '
|
|
112
|
+
require "json"
|
|
113
|
+
data = JSON.parse($stdin.read)["candidates"]
|
|
114
|
+
new_m = data.select { |c| c["classification"] == "new_method" }
|
|
115
|
+
untyped = new_m.count { |c| (c["inferred_return"] || "").include?("untyped") }
|
|
116
|
+
concrete = new_m.count { |c| !(c["rbs"] || "").include?("untyped") }
|
|
117
|
+
puts "new_method: #{new_m.size} | still-untyped return: #{untyped} | concrete: #{concrete}"
|
|
118
|
+
'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run this before and after `--params=observed` to see how many methods
|
|
122
|
+
resolved.
|
|
123
|
+
|
|
124
|
+
## Step 5-d — Handle remaining untyped methods
|
|
125
|
+
|
|
126
|
+
For methods still showing `untyped` return after `--params=observed`,
|
|
127
|
+
there are a few options depending on the cause:
|
|
128
|
+
|
|
129
|
+
| Pattern | Cause | Fix |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `attr_reader :x` with `@x` never set in `initialize` | ivar set from a DB query, config read, or side effect | Add a hand-written sig: create (or edit) `sig/your_class.rbs` with `attr_reader x: String` |
|
|
132
|
+
| Deep method chains on untyped receivers | Cascade from a gem with no RBS | `rbs collection install`; Phase 7 escalation path B |
|
|
133
|
+
| Recursive or mutually recursive methods | Return type not inferrable without a base case | Add a `# @rbs return: YourType` inline annotation, or a hand-written sig |
|
|
134
|
+
| Dynamic methods (`define_method`, DSL) | Metaprogramming Rigor cannot follow | Phase 7 escalation path A (project plugin) |
|
|
135
|
+
|
|
136
|
+
Do not spend long on residual `untyped` methods at this stage — a
|
|
137
|
+
handful of `untyped` returns in `sig/` does not block adoption. The
|
|
138
|
+
objective is to reduce the false-positive count before triage, not to
|
|
139
|
+
reach perfect sig coverage.
|
|
140
|
+
|
|
141
|
+
## Step 5-e — Commit the sig/ directory
|
|
142
|
+
|
|
143
|
+
Once you are satisfied with the initial sig quality:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
git add sig/
|
|
147
|
+
git commit -m "Add initial RBS sigs from rigor sig-gen (--params=observed)"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A committed `sig/` is a first-class project artefact: it improves
|
|
151
|
+
inference quality on every subsequent run and is maintained alongside
|
|
152
|
+
the source (add new sig files when adding classes; update sigs with
|
|
153
|
+
`rigor sig-gen --write lib` when method signatures change).
|
|
154
|
+
|
|
155
|
+
## Quick reference — sig-gen flags used in this phase
|
|
156
|
+
|
|
157
|
+
| Flag | Effect |
|
|
158
|
+
|---|---|
|
|
159
|
+
| *(no flags)* | Print candidates to stdout; nothing written |
|
|
160
|
+
| `--write` | Write `sig/**/*.rbs` files |
|
|
161
|
+
| `--params=observed` | Collect observed argument types from call sites; used to resolve attr_reader / attr_accessor / attr_writer ivar types from initialize observations |
|
|
162
|
+
| `--format json` | Output structured JSON (for scripted analysis) |
|
|
163
|
+
| `--diff` | Show what would change vs. existing sigs (useful for incremental updates) |
|
|
164
|
+
|
|
165
|
+
## Output of this module
|
|
166
|
+
|
|
167
|
+
A committed `sig/` directory with RBS skeletons for all statically
|
|
168
|
+
inferrable methods. Remaining `:untyped_return` methods are noted for
|
|
169
|
+
potential manual annotation; they do not block Phase 6.
|
|
170
|
+
|
|
171
|
+
Proceed to Phase 6 ([`03-baseline-and-bugs.md`](03-baseline-and-bugs.md)).
|