rigortype 0.2.5 → 0.2.7

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/handbook/09-plugins.md +5 -2
  4. data/docs/handbook/appendix-liskov.md +5 -3
  5. data/docs/handbook/appendix-phpstan.md +2 -2
  6. data/docs/install.md +1 -1
  7. data/docs/manual/02-cli-reference.md +60 -2
  8. data/docs/manual/06-baseline.md +12 -0
  9. data/docs/manual/08-skills.md +21 -0
  10. data/docs/manual/11-ci.md +6 -6
  11. data/docs/manual/15-type-protection-coverage.md +29 -0
  12. data/docs/manual/plugins/rigor-minitest.md +1 -1
  13. data/lib/rigor/cli/check_command.rb +4 -33
  14. data/lib/rigor/cli/check_runner_factory.rb +63 -0
  15. data/lib/rigor/cli/coverage_command.rb +42 -10
  16. data/lib/rigor/cli/doctor_command.rb +295 -0
  17. data/lib/rigor/cli/plugins_command.rb +2 -2
  18. data/lib/rigor/cli/plugins_renderer.rb +1 -1
  19. data/lib/rigor/cli/protection_renderer.rb +32 -2
  20. data/lib/rigor/cli/protection_report.rb +32 -6
  21. data/lib/rigor/cli/skill_command.rb +52 -1
  22. data/lib/rigor/cli/upgrade_command.rb +25 -0
  23. data/lib/rigor/cli.rb +17 -1
  24. data/lib/rigor/environment/rbs_loader.rb +28 -0
  25. data/lib/rigor/flow_contribution/fact.rb +1 -1
  26. data/lib/rigor/inference/dynamic_origin.rb +67 -0
  27. data/lib/rigor/inference/expression_typer.rb +22 -10
  28. data/lib/rigor/inference/fallback.rb +2 -2
  29. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -0
  30. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +41 -2
  31. data/lib/rigor/inference/method_dispatcher.rb +19 -4
  32. data/lib/rigor/inference/mutation_widening.rb +18 -0
  33. data/lib/rigor/inference/protection_scanner.rb +6 -3
  34. data/lib/rigor/inference/statement_evaluator.rb +5 -8
  35. data/lib/rigor/plugin/base.rb +34 -7
  36. data/lib/rigor/plugin/registry.rb +1 -1
  37. data/lib/rigor/scope.rb +16 -5
  38. data/lib/rigor/sig_gen/generator.rb +25 -0
  39. data/lib/rigor/sig_gen/method_candidate.rb +7 -2
  40. data/lib/rigor/sig_gen/writer.rb +60 -13
  41. data/lib/rigor/version.rb +1 -1
  42. data/lib/rigor.rb +1 -0
  43. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +63 -2
  44. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +2 -3
  45. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +14 -24
  46. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +10 -3
  47. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +1 -1
  48. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -1
  49. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -1
  50. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +36 -79
  51. data/sig/rigor/plugin/base.rbs +2 -0
  52. data/sig/rigor/scope.rbs +3 -1
  53. data/skills/rigor-ask/SKILL.md +21 -1
  54. data/skills/rigor-baseline-reduce/SKILL.md +16 -0
  55. data/skills/rigor-ci-setup/SKILL.md +96 -249
  56. data/skills/rigor-doctor/SKILL.md +39 -49
  57. data/skills/rigor-doctor/references/01-checks.md +52 -0
  58. data/skills/rigor-editor-setup/SKILL.md +14 -0
  59. data/skills/rigor-mcp-setup/SKILL.md +14 -0
  60. data/skills/rigor-monkeypatch-resolve/SKILL.md +15 -0
  61. data/skills/rigor-plugin-author/SKILL.md +24 -5
  62. data/skills/rigor-plugin-author/references/02-walker-and-types.md +8 -4
  63. data/skills/rigor-plugin-review/SKILL.md +174 -0
  64. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +214 -0
  65. data/skills/rigor-plugin-tune/SKILL.md +21 -2
  66. data/skills/rigor-project-init/SKILL.md +16 -0
  67. data/skills/rigor-protection-uplift/SKILL.md +15 -0
  68. data/skills/rigor-rbs-setup/SKILL.md +15 -0
  69. data/skills/rigor-upgrade/SKILL.md +16 -0
  70. metadata +11 -4
@@ -29,6 +29,20 @@ This skill is the *workflow* around it (identify the editor → apply the
29
29
  manual's snippet → verify), so it does not duplicate (and cannot
30
30
  stale-out) the config details.
31
31
 
32
+ ## First: load the version-current copy
33
+
34
+ The config details already come live from `rigor docs`; this section keeps
35
+ the *workflow itself* current too. Prefer the copy of this skill that ships
36
+ with the **installed** Rigor over any vendored or frozen copy of this file:
37
+
38
+ ```sh
39
+ rigor skill --full rigor-editor-setup
40
+ ```
41
+
42
+ If you already loaded this skill *via* `rigor skill` you have the current
43
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
44
+ **`rigor-next-steps`** to install Rigor first, then come back.
45
+
32
46
  ## When to use
33
47
 
34
48
  - A developer wants Rigor feedback live in their editor, not just from
@@ -31,6 +31,20 @@ This skill is the *workflow* around it (identify the client → apply the
31
31
  manual's snippet → verify the handshake), so it does not duplicate (and
32
32
  cannot stale-out) the config details.
33
33
 
34
+ ## First: load the version-current copy
35
+
36
+ The config details already come live from `rigor docs`; this keeps the
37
+ *workflow itself* current too. Prefer the copy of this skill that ships
38
+ with the **installed** Rigor over any vendored or frozen copy of this file:
39
+
40
+ ```sh
41
+ rigor skill --full rigor-mcp-setup
42
+ ```
43
+
44
+ If you already loaded this skill *via* `rigor skill` you have the current
45
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
46
+ **`rigor-next-steps`** to install Rigor first, then come back.
47
+
34
48
  ## When to use
35
49
 
36
50
  - A developer wants their AI agent to call Rigor's tools mid-session.
@@ -18,6 +18,21 @@ top-level helper). `pre_eval:` ([ADR-17](https://github.com/rigortype/rigor/blob
18
18
  fixes this: it pre-evaluates the listed project files and registers every
19
19
  method they define with a **literal** `def` / `def self.`.
20
20
 
21
+ ## First: load the version-current copy
22
+
23
+ This skill's exact commands, flags, and config keys drift between Rigor
24
+ releases, so follow the copy that ships with the **installed** Rigor rather
25
+ than any vendored or frozen copy of this file. Get the complete current
26
+ procedure in one call:
27
+
28
+ ```sh
29
+ rigor skill --full rigor-monkeypatch-resolve
30
+ ```
31
+
32
+ If you already loaded this skill *via* `rigor skill` you have the current
33
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
34
+ **`rigor-next-steps`** to install Rigor first, then come back.
35
+
21
36
  ## When to use
22
37
 
23
38
  - `rigor triage` shows a cluster of `call.undefined-method` /
@@ -27,6 +27,22 @@ environment.
27
27
  > covers the in-repo layout, `plugin_helpers.rb`, and `make verify`.
28
28
  > This skill is for plugins that live in *your* project.
29
29
 
30
+ ## First: load the version-current copy
31
+
32
+ The plugin contract is pre-1.0 and moving (see the next section), so this
33
+ skill's step detail — in its `references/` files — is exactly the kind that
34
+ drifts between releases. Follow the copy that ships with the **installed**
35
+ Rigor rather than any vendored or frozen copy of this file. Get the complete
36
+ current procedure (body + all references, inline) in one call:
37
+
38
+ ```sh
39
+ rigor skill --full rigor-plugin-author
40
+ ```
41
+
42
+ If you already loaded this skill *via* `rigor skill` you have the current
43
+ copy — just proceed. If the `rigor` command is not available, run
44
+ **`rigor-next-steps`** to install Rigor first, then come back.
45
+
30
46
  ## Important — the plugin contract is a preview (pre-1.0)
31
47
 
32
48
  Rigor's plugin contract (ADR-2) is **not yet frozen**. It stabilises
@@ -97,19 +113,22 @@ AST walk per file — hands every matching node to the block along with a
97
113
  `scope` it can query for inferred types. The block returns an array of
98
114
  `Rigor::Analysis::Diagnostic` (built via the `diagnostic` helper).
99
115
  Optionally the plugin also declares `dynamic_return(receivers:)` /
100
- `type_specifier(methods:)` to *supply* a return type or narrowing facts
101
- for call sites the core analyzer types as `Dynamic`. `#diagnostics_for_file`
116
+ `narrowing_facts(methods:)` to *supply* a return type or narrowing facts
117
+ for call sites the core analyzer types as `Dynamic`. (`narrowing_facts`
118
+ was renamed from `type_specifier` in ADR-80; `type_specifier` remains as a
119
+ deprecating alias removed in 0.3.0 — use `narrowing_facts` in new plugins.)
120
+ `#diagnostics_for_file`
102
121
  is the file-rule surface for whole-file diagnostics a per-node walk can't
103
122
  express. (`flow_contribution_for` was removed pre-1.0 in ADR-52 WD3 —
104
123
  defining it now raises `ArgumentError`; use `dynamic_return` /
105
- `type_specifier`. See Phase 2.)
124
+ `narrowing_facts`. See Phase 2.)
106
125
 
107
126
  ## Phase outline
108
127
 
109
128
  | Phase | What | Reference |
110
129
  | --- | --- | --- |
111
130
  | 1 | Package and scaffold — gem vs project-private layout, gemspec / Gemfile, the plugin class skeleton, `.rigor.yml` activation. | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) |
112
- | 2 | Node rules — `node_rule` (engine-owned walk), building `Diagnostic`s via `Base#diagnostic`, querying `scope.type_of`, calling the target library directly instead of reimplementing it (ADR-39: `Plugin::Inflector`, `Base.suggest`), optional `dynamic_return` / `type_specifier`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
131
+ | 2 | Node rules — `node_rule` (engine-owned walk), building `Diagnostic`s via `Base#diagnostic`, querying `scope.type_of`, calling the target library directly instead of reimplementing it (ADR-39: `Plugin::Inflector`, `Base.suggest`), optional `dynamic_return` / `narrowing_facts`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
113
132
  | 3 | Test and ship — fixture-based tests (RSpec / Minitest, no rigor internals), version pinning, README, publish or keep private. | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) |
114
133
 
115
134
  ## Reading order — modules
@@ -117,5 +136,5 @@ defining it now raises `ArgumentError`; use `dynamic_return` /
117
136
  | Module | Read | Covers |
118
137
  | --- | --- | --- |
119
138
  | 1 | [`references/01-plan-and-scaffold.md`](references/01-plan-and-scaffold.md) | **Phase 1.** The gem vs project-private packaging split, directory trees for both, gemspec template, project-private path-gem / `RUBYLIB` activation, the `Rigor::Plugin::Base` skeleton, `.rigor.yml` `plugins:` wiring. |
120
- | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `type_specifier` return-type hooks (`flow_contribution_for` was removed pre-1.0 in ADR-52 WD3), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
139
+ | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `node_rule` engine-owned AST walk over Prism nodes, the `Base#diagnostic` helper, asking the analyzer for inferred types via `scope.type_of`, two-pass / lexical context (`node_file_context` / `NodeContext`), the optional `dynamic_return` / `narrowing_facts` return-type hooks (`narrowing_facts` was renamed from `type_specifier` in ADR-80, alias removed in 0.3.0; `flow_contribution_for` was removed pre-1.0 in ADR-52 WD3), calling the target library's pure methods directly rather than reimplementing them (ADR-39: `Plugin::Inflector` over the real `ActiveSupport::Inflector`; `Base.suggest` for did-you-mean), and shipping `sig/*.rbs` so the DSL's types are visible. |
121
140
  | 3 | [`references/03-test-and-ship.md`](references/03-test-and-ship.md) | **Phase 3.** Testing a plugin from outside the monorepo — fixture projects driven through `rigor check --format json`, plus pure unit tests of dispatch tables — with RSpec or Minitest. Version pinning against the pre-1.0 contract. README. Publishing to RubyGems or keeping the plugin private. |
@@ -162,7 +162,7 @@ rather than hand-rolling Levenshtein:
162
162
  Rigor::Plugin::Base.suggest(typo, known_names) # nearest match, or nil
163
163
  ```
164
164
 
165
- ## Optional — contribute a return type with `dynamic_return` / `type_specifier`
165
+ ## Optional — contribute a return type with `dynamic_return` / `narrowing_facts`
166
166
 
167
167
  > **Critical — these hooks do NOT make a method "defined", so they do
168
168
  > NOT suppress `call.undefined-method`.** Method *existence* and call
@@ -197,11 +197,15 @@ end
197
197
  # Post-return NARROWING FACTS, gated on the call's method name.
198
198
  # Return an Array of facts (or nil). Used for assertion / predicate
199
199
  # narrowing (`assert_kind_of(Foo, x)` ⇒ x is Foo afterwards).
200
- type_specifier methods: [:assert_kind_of] do |call_node, scope|
200
+ narrowing_facts methods: [:assert_kind_of] do |call_node, scope|
201
201
  # ... build and return the post-return facts ...
202
202
  end
203
203
  ```
204
204
 
205
+ > **`narrowing_facts` was renamed from `type_specifier` in ADR-80.**
206
+ > `type_specifier` remains as a deprecating alias, removed in 0.3.0 — use
207
+ > `narrowing_facts` in new plugins.
208
+
205
209
  Build return types with `Rigor::Type::Combinator`:
206
210
 
207
211
  ```ruby
@@ -240,7 +244,7 @@ If the DSL introduces methods or classes that Rigor cannot see (a
240
244
  Rigor RBS declaring them so *core* inference — not just your plugin —
241
245
  treats them as **defined**. This is what removes the
242
246
  `call.undefined-method` diagnostics on those methods; nothing else
243
- (not a `node_rule`, not `dynamic_return` / `type_specifier`) makes a
247
+ (not a `node_rule`, not `dynamic_return` / `narrowing_facts`) makes a
244
248
  method exist in Rigor's view.
245
249
 
246
250
  Two ways to wire the RBS, depending on how the plugin is packaged:
@@ -288,6 +292,6 @@ They compose — many plugins ship both.
288
292
 
289
293
  A plugin whose `node_rule`(s) recognise the DSL and emit diagnostics
290
294
  with correct severities and rule ids — optionally a `dynamic_return` /
291
- `type_specifier` and a `sig/` bundle. Verify by eye with `rigor check`;
295
+ `narrowing_facts` and a `sig/` bundle. Verify by eye with `rigor check`;
292
296
  lock it down with tests in Phase 3
293
297
  ([`03-test-and-ship.md`](03-test-and-ship.md)).
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: rigor-plugin-review
3
+ description: |
4
+ Review an existing Rigor plugin's source against the current authoring contract and produce a prioritized upgrade path — the modernization counterpart to rigor-plugin-author. Audits config-default declaration (ADR-40), the AST-walk model (node_rule vs a hand-rolled traversal), return-type / narrowing hooks (dynamic_return / narrowing_facts, not the removed flow_contribution_for or deprecated type_specifier), the ADR-60 WD4 authoring helpers (diagnostic / diagnostics_for / suggest / producer_value / read_fact), engine-collaboration vs reimplementation, cache-producer soundness, manifest-field hygiene, and doc freshness. Triggers: "review this Rigor plugin", "does my plugin follow best practices", "upgrade our rigor-prefixed plugin to the latest contract", "modernize this plugin", "is this plugin using the current API". NOT for authoring a new plugin (use rigor-plugin-author), enabling bundled plugins on a project (use rigor-plugin-tune), or tuning plugin config.
5
+ license: MPL-2.0
6
+ metadata:
7
+ version: 0.1.0
8
+ homepage: https://github.com/rigortype/rigor
9
+ ---
10
+
11
+ # Rigor Plugin Review
12
+
13
+ Audit an **existing** Rigor plugin — a bundled one in the rigor
14
+ monorepo (`plugins/` or `examples/`), or your own `rigor-<id>` gem —
15
+ against the current `Rigor::Plugin::Base` authoring contract, and hand
16
+ back a **prioritized upgrade path**. This is the review / upgrade
17
+ counterpart to `rigor-plugin-author` (which creates new plugins).
18
+
19
+ Plugins written before a contract addition keep working — the gate is
20
+ compatibility, not currency — but they drift from the idiom other
21
+ authors copy. The commonest drift, in rough order of how often it
22
+ appears:
23
+
24
+ 1. **Config defaults** declared with a `DEFAULT_*` constant +
25
+ `config.fetch(k, DEFAULT)` instead of `config_schema {kind:,
26
+ default:}` (ADR-40).
27
+ 2. **Hand-rolled boilerplate** the ADR-60 WD4 authoring helpers now
28
+ own — a Levenshtein "did you mean", a `@table`/`@load_error` memo +
29
+ rescue, a `Diagnostic.new` where a node exists.
30
+ 3. **A hand-rolled AST walk** where the engine-owned `node_rule` now
31
+ fits (ADR-37 / ADR-52).
32
+ 4. **Removed / renamed hooks** still named — `flow_contribution_for`
33
+ (deleted, ADR-52 WD3) or `type_specifier` (deprecated alias for
34
+ `narrowing_facts`, ADR-80).
35
+ 5. **Stale docs** — archaeology about deleted hooks, pinned version
36
+ references that no longer mean anything.
37
+
38
+ ## First: load the version-current copy
39
+
40
+ This skill audits against a contract that moves release to release (hook
41
+ renames, new helpers, deprecations), so its checklist — in its
42
+ `references/` files — is only as good as the Rigor it ships with. Follow
43
+ the copy that ships with the **installed** Rigor rather than any vendored
44
+ or frozen copy of this file. Get the complete current procedure (body + all
45
+ references, inline) in one call:
46
+
47
+ ```sh
48
+ rigor skill --full rigor-plugin-review
49
+ ```
50
+
51
+ If you already loaded this skill *via* `rigor skill` you have the current
52
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
53
+ **`rigor-next-steps`** to install Rigor first, then come back.
54
+
55
+ ## When to use / not use
56
+
57
+ **Use it** when someone asks to review a plugin's quality, check it
58
+ against best practices, or upgrade it to the current contract — whether
59
+ it lives in the rigor monorepo or in an external repo.
60
+
61
+ **Do not use it** for:
62
+
63
+ - **Authoring a new plugin** → `rigor-plugin-author`.
64
+ - **Choosing / enabling bundled plugins on a project** →
65
+ `rigor-plugin-tune`.
66
+ - **A behavioural bug in a plugin** — that is ordinary debugging, not a
67
+ contract-conformance pass.
68
+
69
+ ## Read the plugin — and read the contract
70
+
71
+ Rigor is installed on disk, so both the plugin under review and the
72
+ worked-example plugins are readable source:
73
+
74
+ ```sh
75
+ rigor plugin list # every bundled + example plugin, with paths
76
+ rigor plugin print rigor-<id> # a plugin's main source, inline
77
+ rigor plugin path rigor-<id> # its directory, to browse
78
+ ```
79
+
80
+ The **authoritative** authoring surface — the one this review scores
81
+ against — is the internal spec, not this file:
82
+
83
+ - [`docs/internal-spec/plugin.md`](https://github.com/rigortype/rigor/blob/master/docs/internal-spec/plugin.md)
84
+ — manifest, `node_rule` / `node_file_context`, `dynamic_return` /
85
+ `narrowing_facts`, the `#diagnostic` / `#diagnostics_for` / `.suggest`
86
+ / `#read_fact` author helpers, `config_schema` `{kind:, default:}`.
87
+ - [`docs/internal-spec/plugin-cache-producers.md`](https://github.com/rigortype/rigor/blob/master/docs/internal-spec/plugin-cache-producers.md)
88
+ — `producer` / `#cache_for` / `#producer_value` / `#producer_error`,
89
+ ADR-60 WD3 record-and-validate.
90
+ - [`docs/internal-spec/plugin-trust.md`](https://github.com/rigortype/rigor/blob/master/docs/internal-spec/plugin-trust.md)
91
+ — `TrustPolicy` / `IoBoundary`.
92
+
93
+ When the checklist below and the spec disagree, **the spec binds** —
94
+ it tracks the installed `rigor` version; this skill is a snapshot.
95
+
96
+ ## Procedure
97
+
98
+ ### Phase 1 — Inventory
99
+
100
+ Read the plugin's `lib/**/*.rb`, its `README.md`, and its integration /
101
+ unit spec. Note: the `manifest(...)` block, every `config.fetch` /
102
+ `DEFAULT_*` constant, every `Rigor::Analysis::Diagnostic.new`, any
103
+ hand-rolled `levenshtein` / `each_child` walk / cross-plugin `@*_resolved`
104
+ flag, and every mention of `flow_contribution_for` / `type_specifier`.
105
+
106
+ ### Phase 2 — Score against the checklist
107
+
108
+ Walk [`references/01-best-practices-checklist.md`](references/01-best-practices-checklist.md)
109
+ concern by concern. For each finding, record: the smell, the modern
110
+ replacement, the authoritative citation, and — critically — whether the
111
+ change is **mechanical** (byte-identical diagnostics expected) or
112
+ **design-level** (needs judgment / may change behaviour).
113
+
114
+ ### Phase 3 — Establish the oracle BEFORE changing anything
115
+
116
+ The plugin's integration spec is the contract you must preserve. Run it
117
+ green first, so you can prove each later step is a faithful refactor:
118
+
119
+ ```sh
120
+ # external gem:
121
+ bundle exec rspec spec/
122
+ # in the rigor monorepo:
123
+ nix … develop --command bundle exec rspec spec/integration/<plugins|examples>/<id>_plugin_spec.rb
124
+ ```
125
+
126
+ If there is no spec, **write one first** (per `rigor-plugin-author`
127
+ Phase 3) — a modernization with no oracle is a guess.
128
+
129
+ ### Phase 4 — Apply the upgrade path, mechanical first
130
+
131
+ Order the work low-risk → high-risk, and **re-run the spec after every
132
+ step**:
133
+
134
+ 1. **Mechanical (expect byte-identical diagnostics):** ADR-40 config
135
+ defaults · helper swaps (`suggest` / `producer_value` / `diagnostic`
136
+ / `diagnostics_for` / `read_fact`) · manifest-field renames (ADR-60
137
+ WD1/WD2) · doc freshness. A spec that changes here means the swap was
138
+ not faithful — fix it, do not re-baseline.
139
+ 2. **Design-level (may change behaviour — validate empirically):** an
140
+ AST-walk migration onto `node_rule`; an engine-collaboration refactor
141
+ that reads `Scope#type_of` instead of a hand-rolled binding map.
142
+ **Do not delete hand-rolled state before proving the engine gives you
143
+ the same information** — see the `rigor-units` trap in the checklist
144
+ (the diagnostics-side `Scope` is a *seed entry scope* without
145
+ flow-accumulated local bindings, so a cross-statement binding map can
146
+ be necessary, not redundant).
147
+
148
+ ### Phase 5 — Verify
149
+
150
+ ```sh
151
+ rigor check <plugin>/lib # ADR-43 contract self-check — MUST be clean
152
+ rigor plugins --strict # the plugin still loads
153
+ rigor plugins --capabilities # node-rule types / dynamic_return receivers look right
154
+ bundle exec rspec … # the oracle spec, still green
155
+ ```
156
+
157
+ In the **rigor monorepo**, the gate is `make check-plugins` (runs
158
+ `rigor check` over every `plugins/*/lib` + `examples/*/lib`) plus
159
+ `make verify`; land the change as its own commit(s) with the spec as
160
+ the byte-identical gate.
161
+
162
+ ## Output
163
+
164
+ Hand the user a table — smell → replacement → authority → mechanical /
165
+ design — ranked so the mechanical, oracle-gated wins land first, and
166
+ call out any finding (like the units binding-map case) where the
167
+ "obvious" modernization is actually wrong. If nothing is stale, say so
168
+ plainly: a plugin that already tracks the current contract is a pass,
169
+ not an occasion to invent churn.
170
+
171
+ ## Next step
172
+
173
+ Re-run `rigor skill describe` for the next move, or `rigor-plugin-author`
174
+ if the review surfaced a *new* capability the plugin should grow.
@@ -0,0 +1,214 @@
1
+ # Plugin best-practices checklist
2
+
3
+ Score a plugin concern by concern. Each row is **smell → modern
4
+ replacement → authority**. The authority column names the binding
5
+ surface (`docs/internal-spec/*` or the ADR); when it disagrees with
6
+ this file, it wins.
7
+
8
+ `M` = mechanical (byte-identical diagnostics expected; oracle-gated).
9
+ `D` = design-level (may change behaviour; validate empirically).
10
+
11
+ ---
12
+
13
+ ## 1. Manifest & config defaults (ADR-40) — `M`
14
+
15
+ **Smell:**
16
+
17
+ ```ruby
18
+ DEFAULT_ROUTES_FILE = "config/routes.yml"
19
+ config_schema: { "routes_file" => :string }
20
+ # …
21
+ @routes_file = config.fetch("routes_file", DEFAULT_ROUTES_FILE)
22
+ ```
23
+
24
+ **Modern:**
25
+
26
+ ```ruby
27
+ config_schema: { "routes_file" => { kind: :string, default: "config/routes.yml" } }
28
+ # …
29
+ @routes_file = config["routes_file"] # default merged under user config
30
+ ```
31
+
32
+ `Base#config` merges `manifest.config_defaults` beneath the user config,
33
+ so the plugin reads the key directly and the `DEFAULT_*` constant goes
34
+ away. Keep a constant only where a value needs *validation the merged
35
+ default cannot express* (e.g. an allow-listed `severity` that must fall
36
+ back when a user supplies a bad value).
37
+
38
+ **Authority:** `docs/internal-spec/plugin.md` § "Declared config
39
+ defaults — `config_schema` `{ kind:, default: }`".
40
+
41
+ ---
42
+
43
+ ## 2. AST-walk ownership (ADR-37 / ADR-52) — `D`
44
+
45
+ **Smell:** a hand-rolled traversal for per-node checks —
46
+ `root.compact_child_nodes.each { … }`, a bespoke `Walker` that recurses
47
+ the tree, or a `#diagnostics_for_file` that re-walks to find call sites.
48
+
49
+ **Modern:** declare `node_rule(Prism::CallNode) { |node, scope, path| … }`
50
+ and let the engine own the single per-file walk. For a two-pass
51
+ (collect-then-validate) plugin, add `node_file_context { |root, scope| … }`
52
+ — it runs once before the node rules and threads a file-local value in
53
+ as the rule block's fourth argument.
54
+
55
+ **Keep `#diagnostics_for_file`** only for genuinely whole-file
56
+ diagnostics a per-node walk cannot express — see concern 5 for the case
57
+ where a stateful whole-file walk is *required*, not lazy.
58
+
59
+ **"Genuinely whole-file" is a sharp line — apply it, don't defer to
60
+ precedent.** A per-*class* or per-*def* contract check (does this class
61
+ define `#get`? does its body return the contracted type?) IS
62
+ node-expressible — migrate it to `node_rule(Prism::ClassNode)` /
63
+ `node_rule(Prism::DefNode)`, even if a shipped production plugin still
64
+ uses `#diagnostics_for_file` + a hand-rolled `class_nodes` walk for the
65
+ same job (`rigor-hanami`'s ADR-28 check half does — an equivalent,
66
+ older shape, not a reason to keep a new copy hand-rolled). The genuine
67
+ whole-file case is a diagnostic whose *identity or count is not tied to
68
+ any one node* — e.g. `rigor-routes`'s "routes file failed to load"
69
+ warning, which must fire exactly once per file (or run) even on a file
70
+ with zero matching nodes. That cannot be a node rule; a per-class check
71
+ can.
72
+
73
+ **Authority:** `docs/internal-spec/plugin.md` §§ "Node-scoped rules —
74
+ `node_rule`", "`node_file_context`".
75
+
76
+ ---
77
+
78
+ ## 3. Return-type & narrowing hooks (ADR-37 / ADR-52 / ADR-80) — `M`/`D`
79
+
80
+ **Smell:** `flow_contribution_for` (deleted in ADR-52 WD3 — *defining it
81
+ now raises `ArgumentError`*), or `type_specifier` (the pre-ADR-80 name).
82
+
83
+ **Modern:** `dynamic_return(receivers:/methods:/file_methods:)` to
84
+ *supply* a return type, `narrowing_facts(methods:)` to supply
85
+ post-return narrowing facts. `type_specifier` survives as a
86
+ deprecating alias removed in 0.3.0 — rename to `narrowing_facts`.
87
+
88
+ The gate resolves after `#init` when passed a callable
89
+ (`methods: -> { [@method_name] }`), so config-derived method names work.
90
+
91
+ **Authority:** `docs/internal-spec/plugin.md` § "Return-type and
92
+ narrowing contributions". `M` for the pure rename; `D` if you are adding
93
+ a contribution the plugin did not have.
94
+
95
+ ---
96
+
97
+ ## 4. Authoring helpers (ADR-37 / ADR-60 WD4) — `M`
98
+
99
+ The single richest source of drift. Each helper replaces a hand-rolled
100
+ shape and is expected to be diagnostics-preserving.
101
+
102
+ | Smell | Modern | Authority |
103
+ | --- | --- | --- |
104
+ | `Rigor::Analysis::Diagnostic.new(line:, column: loc.start_column + 1, …)` where a node exists | `#diagnostic(node, path:, message:, severity:, rule:)` (or `location: node.message_loc` for a sub-span) | plugin.md § "Positioning a diagnostic — `#diagnostic`" |
105
+ | `violations.map { |v| diagnostic(node, message: v.message, …) }` | `#diagnostics_for(violations, path:, node:)` — duck-types `#message` / `#node` / `#severity` / `#rule` / `#location` | plugin.md § author helpers (ADR-60 WD4) |
106
+ | A hand-rolled `levenshtein` / `closest_*` "did you mean" | `Rigor::Plugin::Base.suggest(name, candidates)` (`DidYouMean::SpellChecker`) — a **class** method, callable from an `Analyzer` too | plugin.md § "`Base.suggest`" |
107
+ | `@table ||= cache_for(id).call` + a multi-`rescue` ladder + an `@load_error` ivar | `#producer_value(id, params:)` (memoised incl. nil) + `#producer_error(id)` (the rescued exception, for a tailored message) | plugin-cache-producers.md § "Invalidation contract" |
108
+ | `@x_resolved` flag guarding `services.fact_store.read` | `#read_fact(plugin_id:, name:)` — nil-inclusive memo, retires the flag | plugin.md § author helpers (ADR-60 WD4) |
109
+ | Hand-parsing a `Prism::SymbolNode#value` / string literal | `Rigor::Source::Literals` | plugin.md § "Extracting argument literals" |
110
+ | A private reimplementation of a target library (own inflector, own pure helper) | Call the library's safe methods directly — `Plugin::Inflector` over real `ActiveSupport::Inflector` (ADR-39) | plugin.md § "Target-library invocation" |
111
+
112
+ **Note on tailored load-error messages:** `#producer_value` rescues
113
+ every `StandardError` into `#producer_error`, so a plugin that wants
114
+ class-specific messages ("not found" vs "failed to parse" vs
115
+ access-denied) switches on `producer_error(id)`'s class when building
116
+ the load-error diagnostic — cleaner than an inline rescue ladder and
117
+ still message-preserving. A *file-level* load-error (line 1, no node)
118
+ legitimately keeps a direct `Diagnostic.new` — `#diagnostic` needs a
119
+ node to position at.
120
+
121
+ ---
122
+
123
+ ## 5. Engine collaboration vs reimplementation — `D` (read the trap)
124
+
125
+ **Principle (rigor-pattern):** do not re-implement a fact the engine
126
+ already computes. If you need "is this a literal string?" / "what type
127
+ did inference give this expression?", read `Scope#type_of(node)` rather
128
+ than tracking it yourself. `rigor-pattern` reads the engine's
129
+ `LiteralStringFolding` result back instead of propagating strings by
130
+ hand.
131
+
132
+ **The trap (rigor-units) — when a hand-rolled binding map is NOT
133
+ redundant:** the `Scope` handed to the **diagnostics** side
134
+ (`#diagnostics_for_file` / a `node_rule`) is the **seed entry scope**.
135
+ `Scope#type_of` re-evaluates a *self-contained* expression on demand
136
+ (`scope.type_of(100.kilometers)` folds through the plugin's own
137
+ `dynamic_return`), but it carries **no flow-accumulated local
138
+ bindings**: for `speed = distance / time`, `scope.type_of(distance)` is
139
+ `untyped`, because the entry scope never bound the earlier assignment.
140
+ Only the **flow scope** handed to a `dynamic_return` block resolves such
141
+ locals. So a diagnostics-side check that must follow a dimension /
142
+ type across statements legitimately keeps its own single-pass binding
143
+ map — deleting it and reaching for `scope.type_of` collapses
144
+ cross-statement propagation.
145
+
146
+ **How to tell which case you are in:** if the value you need lives *at
147
+ the call site you are inspecting* (a literal argument, a self-contained
148
+ sub-expression) → read `Scope#type_of`. If it lives *in an earlier
149
+ statement's local binding* and you are on the diagnostics side → the
150
+ engine will not hand it to you; keep the binding map, and document why.
151
+ **Validate empirically before deleting state:** run the integration
152
+ spec after the change; a wave of failures on multi-statement fixtures is
153
+ this trap.
154
+
155
+ **Authority:** the reasoning is recorded in
156
+ `docs/notes/20260704-examples-plugin-modernization-survey.md` (the
157
+ `rigor-units` section) and the `rigor-units` class comment itself.
158
+
159
+ ---
160
+
161
+ ## 6. Cache producers (ADR-60 WD3) — `M`/`D`
162
+
163
+ **Smell:** a "prime the read before `cache_for` so the digest is
164
+ captured" comment, or a producer that globs a directory but does not
165
+ declare `watch:`.
166
+
167
+ **Modern:** record-and-validate — the `io_boundary.read_file` inside the
168
+ `producer` block is captured into the dependency descriptor *after* the
169
+ block runs, so there is nothing to prime. A producer that reads a *set*
170
+ of files by glob declares `watch:` so file *additions* invalidate too.
171
+
172
+ **Authority:** `docs/internal-spec/plugin-cache-producers.md` §§
173
+ "Invalidation contract", "`producer(... watch:)`".
174
+
175
+ ---
176
+
177
+ ## 7. Manifest-field hygiene (ADR-60 WD1 / WD2) — `M`
178
+
179
+ **Smell:** `external_files:` (never wired — removed in ADR-60 WD1);
180
+ `BlockAsMethod verbs:` (renamed → `method_names:`); `NestedClassTemplate
181
+ name_arg_position:` (renamed → `symbol_arg_position:`).
182
+
183
+ **Modern:** drop `external_files:`; use the renamed keys.
184
+
185
+ **Authority:** `docs/internal-spec/plugin.md` § "`Rigor::Plugin::Manifest`"
186
+ and `macro-substrate.md`.
187
+
188
+ ---
189
+
190
+ ## 8. Documentation freshness — `M`
191
+
192
+ **Smell:** "the former `flow_contribution_for` hook was removed"
193
+ archaeology in a README / class comment (a reader who never knew the
194
+ deleted hook does not need it named); pinned version references that no
195
+ longer carry meaning ("introduced in v0.0.9").
196
+
197
+ **Modern:** describe the *current* mechanism directly. Keep a historical
198
+ note only where a reader migrating an old plugin genuinely needs it —
199
+ and put that in a CHANGELOG / migration note, not the class docstring.
200
+
201
+ ---
202
+
203
+ ## 9. Verification (ADR-43) — always
204
+
205
+ - `rigor check <plugin>/lib` — the ADR-43 contract self-check resolves
206
+ the plugin's inherited `Plugin::Base` calls and warns on contract
207
+ misuse. MUST be clean; fix the cause, never disable the rule.
208
+ - `rigor plugins --strict` — the plugin still activates.
209
+ - `rigor plugins --capabilities` — `node_rule_types` /
210
+ `dynamic_return_receivers` / `type_specifier_methods` reflect the
211
+ declarations.
212
+ - The integration / unit spec — green, and byte-identical across every
213
+ mechanical step.
214
+ - In the monorepo: `make check-plugins` + `make verify`.
@@ -20,6 +20,21 @@ All `rigor-*` plugins ship **bundled inside the `rigortype` gem** — no
20
20
  separate install. Enabling one is just adding its id to `plugins:` in
21
21
  `.rigor.dist.yml`.
22
22
 
23
+ ## First: load the version-current copy
24
+
25
+ This skill's exact commands, flags, and config keys drift between Rigor
26
+ releases, so follow the copy that ships with the **installed** Rigor rather
27
+ than any vendored or frozen copy of this file. Get the complete current
28
+ procedure in one call:
29
+
30
+ ```sh
31
+ rigor skill --full rigor-plugin-tune
32
+ ```
33
+
34
+ If you already loaded this skill *via* `rigor skill` you have the current
35
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
36
+ **`rigor-next-steps`** to install Rigor first, then come back.
37
+
23
38
  ## When to use
24
39
 
25
40
  - The project added (or removed) a gem since it was onboarded.
@@ -43,9 +58,13 @@ dependencies in `Gemfile.lock`.
43
58
  ### Phase 2 — match against the bundled catalogue
44
59
 
45
60
  The authoritative, current list of bundled plugins (the count drifts as
46
- new ones land) is the catalogue:
61
+ new ones land) comes from the **installed** Rigor itself — read it there,
62
+ not from a web page that may describe a different version:
47
63
 
48
- <https://github.com/rigortype/rigor/blob/master/plugins/README.md>
64
+ ```sh
65
+ rigor docs manual/plugins/README # the catalogue of every bundled plugin, offline
66
+ rigor plugins # what is enabled in THIS project right now
67
+ ```
49
68
 
50
69
  For each Gemfile.lock dependency, check whether a bundled `rigor-<gem>`
51
70
  plugin exists and is **not** already in `plugins:`. Common matches:
@@ -23,6 +23,22 @@ chapter for the install channels (`mise` recommended). This skill
23
23
  references only public CLI flags and config keys — the same surface
24
24
  `rigor --help` documents.
25
25
 
26
+ ## First: load the version-current copy
27
+
28
+ This skill's step detail lives in its `references/` files, and its exact
29
+ commands, flags, and config keys drift between Rigor releases — so follow
30
+ the copy that ships with the **installed** Rigor rather than any vendored
31
+ or frozen copy of this file. Get the complete current procedure (body + all
32
+ references, inline) in one call:
33
+
34
+ ```sh
35
+ rigor skill --full rigor-project-init
36
+ ```
37
+
38
+ If you already loaded this skill *via* `rigor skill` you have the current
39
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
40
+ **`rigor-next-steps`** to install Rigor first, then come back.
41
+
26
42
  ## Phase 0 — When to use this skill
27
43
 
28
44
  Trigger when the user says "set up Rigor here", "configure rigor for
@@ -16,6 +16,21 @@ type. This skill *acts* on that surfacing under the discipline that keeps
16
16
  Rigor false-positive-safe: protection goes up, and not one line of
17
17
  working code starts reporting a new diagnostic.
18
18
 
19
+ ## First: load the version-current copy
20
+
21
+ This skill's exact commands, flags, carrier syntax, and rule ids drift
22
+ between Rigor releases, so follow the copy that ships with the **installed**
23
+ Rigor rather than any vendored or frozen copy of this file. Get the
24
+ complete current procedure in one call:
25
+
26
+ ```sh
27
+ rigor skill --full rigor-protection-uplift
28
+ ```
29
+
30
+ If you already loaded this skill *via* `rigor skill` you have the current
31
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
32
+ **`rigor-next-steps`** to install Rigor first, then come back.
33
+
19
34
  ## When to use
20
35
 
21
36
  - A user wants to raise how much of their code Rigor can actually catch
@@ -18,6 +18,21 @@ community RBS collection at
18
18
  [`ruby/gem_rbs_collection`](https://github.com/ruby/gem_rbs_collection);
19
19
  this skill wires it into the project.
20
20
 
21
+ ## First: load the version-current copy
22
+
23
+ This skill's exact commands, flags, and config keys drift between Rigor
24
+ releases, so follow the copy that ships with the **installed** Rigor rather
25
+ than any vendored or frozen copy of this file. Get the complete current
26
+ procedure in one call:
27
+
28
+ ```sh
29
+ rigor skill --full rigor-rbs-setup
30
+ ```
31
+
32
+ If you already loaded this skill *via* `rigor skill` you have the current
33
+ copy — just proceed. If `rigor` is not on `PATH`, this task needs it: run
34
+ **`rigor-next-steps`** to install Rigor first, then come back.
35
+
21
36
  ## When to use
22
37
 
23
38
  - `rigor check` ends with `info: N gem(s) in Gemfile.lock have no RBS