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