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.
- checksums.yaml +4 -4
- data/README.md +12 -2
- data/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +29 -7
- data/lib/rigor/environment/rbs_loader.rb +17 -0
- data/lib/rigor/environment.rb +13 -3
- data/lib/rigor/scope.rb +14 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +1 -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 +139 -0
- data/skills/rigor-project-init/references/02-configure.md +202 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +169 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +14 -1
|
@@ -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)).
|