rigortype 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ ---
2
+ name: rigor-baseline-reduce
3
+ description: |
4
+ Work a Rigor project's `.rigor-baseline.yml` down rule by rule: prioritise with `rigor triage`, sample call sites, classify each as real bug / stylistic-safe / false positive, then fix, `# rigor:disable`, or open a Rigor issue — and regenerate the baseline. Triggers: "reduce the rigor baseline", "fix some baseline diagnostics", "what rigor issue should I fix next?". NOT for first-time setup (use rigor-project-init) 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 Baseline Reduce
12
+
13
+ Opportunistic, session-bounded quality improvement for a project
14
+ that already has a `.rigor-baseline.yml`. Each session picks a rule,
15
+ walks its sites, and lands a mix of fixes, intentional suppressions,
16
+ and issue reports — then refreshes the baseline so the gains stick.
17
+
18
+ This skill is for **users improving their own project**. It uses the
19
+ published `rigor` executable on `PATH` and references only public CLI
20
+ flags and config keys.
21
+
22
+ ## Phase 0 — When to use this skill
23
+
24
+ Trigger when the user says "reduce the rigor baseline", "fix some
25
+ baseline diagnostics", "what should I fix next in rigor?", or asks to
26
+ work down the diagnostics a previous onboarding parenthesised.
27
+
28
+ **Precondition: the project is in acknowledge mode.** This skill
29
+ operates on a `.rigor-baseline.yml` that `.rigor.yml` /
30
+ `.rigor.dist.yml` declares via `baseline:`. A strict-mode project
31
+ (`severity_profile: strict`, no baseline) has nothing for this skill
32
+ to reduce — its diagnostics are already all live; fix them as
33
+ ordinary `rigor check` output. If no baseline exists, the user wants
34
+ `rigor-project-init`, not this skill.
35
+
36
+ Do NOT trigger for:
37
+
38
+ - **First-time onboarding** — no `.rigor.yml` yet → `rigor-project-init`.
39
+ - **Writing a plugin** for the project's DSL → `rigor-plugin-author`.
40
+
41
+ ## What baseline reduction is
42
+
43
+ `.rigor-baseline.yml` records `(file, rule, count)` buckets — the
44
+ diagnostics that existed when the project adopted Rigor. They are
45
+ suppressed while their count holds. Reduction means: go through them
46
+ deliberately, decide what each one really is, and shrink the file.
47
+ Three outcomes per site:
48
+
49
+ - **Real bug** — Rigor caught a genuine defect. Fix the code.
50
+ - **Stylistic / safe** — the static reading is worst-case-sound but
51
+ the site is fine in practice (an idiom the analyzer's narrowing
52
+ doesn't follow, a slot the project always initialises). Mark it
53
+ `# rigor:disable <rule>` with intent.
54
+ - **False positive** — Rigor is wrong; the rule should narrow
55
+ further. Leave it baselined and open a Rigor issue.
56
+
57
+ Shrinking the baseline is progress whichever outcome applies — the
58
+ file gets smaller, and "reduce the baseline by N this sprint" is a
59
+ trackable goal.
60
+
61
+ ## Phase outline
62
+
63
+ | Phase | What | Reference |
64
+ | --- | --- | --- |
65
+ | 1 | Prioritise — `rigor triage --format json` + `rigor baseline dump` pick the rule to work. | [`references/01-classify.md`](references/01-classify.md) |
66
+ | 2 | Per rule: sample 3–5 sites, classify each (real bug / stylistic / FP). | [`references/01-classify.md`](references/01-classify.md) |
67
+ | 3 | Act — fix, `# rigor:disable`, or open a Rigor issue. | [`references/02-fix-or-suppress.md`](references/02-fix-or-suppress.md) |
68
+ | 4 | Refresh — `rigor baseline drift`, then `rigor baseline regenerate`. | [`references/02-fix-or-suppress.md`](references/02-fix-or-suppress.md) |
69
+
70
+ Phases 2–4 repeat per rule until a stop condition (below) fires.
71
+
72
+ ## Reading order — modules
73
+
74
+ | Module | Read | Covers |
75
+ | --- | --- | --- |
76
+ | 1 | [`references/01-classify.md`](references/01-classify.md) | **Phases 1–2.** Priority order from the `rigor triage` hints + ascending bucket count. The sample-and-classify protocol; the three-way real-bug / stylistic / FP triage and how to tell them apart. |
77
+ | 2 | [`references/02-fix-or-suppress.md`](references/02-fix-or-suppress.md) | **Phases 3–4.** Acting on each classification — fix patterns, `# rigor:disable` placement (per-line vs per-file), FP escalation as a Rigor GitHub issue. Refreshing the baseline with `drift` + `regenerate`. |
78
+
79
+ ## Stop conditions
80
+
81
+ End the session — do not grind indefinitely — when any of these hit:
82
+
83
+ - The user signals halt.
84
+ - The next rule's site count exceeds the session budget (default:
85
+ ~20 sites). A 200-site rule is systemic; escalate it as a decision
86
+ (below), do not walk it site by site.
87
+ - A wall-time budget is reached (default: ~60 minutes).
88
+
89
+ Always finish with Phase 4 (refresh the baseline) before stopping, so
90
+ the landed work is recorded.
91
+
92
+ ## Decision points to escalate to the user
93
+
94
+ - "This rule has 200 sites across 14 files — it looks systemic. A
95
+ plugin or an engine fix would clear them in bulk; want me to
96
+ investigate that instead of walking sites one by one?"
97
+ - "This file's diagnostics would be cleaner under one per-file
98
+ `# rigor:disable-file <rule>` than 30 per-line comments — switch?"
99
+ - "The diagnostic wording changed between Rigor versions and the
100
+ baseline no longer matches cleanly — regenerate now?"
@@ -0,0 +1,107 @@
1
+ # 01 — Prioritise and classify
2
+
3
+ Covers **Phase 1** (pick the rule) and **Phase 2** (sample and
4
+ classify its sites).
5
+
6
+ ## Phase 1 — Pick the rule to work
7
+
8
+ Do not walk the baseline top to bottom. Two commands decide the
9
+ order; run both.
10
+
11
+ ### `rigor triage --format json` — the priority signal
12
+
13
+ ```sh
14
+ rigor triage --format json
15
+ ```
16
+
17
+ `rigor triage` runs the analysis and returns a structured summary —
18
+ a rule `distribution`, file `hotspots`, and a `hints` catalogue. It
19
+ is the deterministic diagnosis layer; use it rather than counting the
20
+ raw `rigor check` stream by hand. The `hints` array is the priority
21
+ signal — each hint has an `id`:
22
+
23
+ | Hint `id` | What it means for reduction | Priority |
24
+ | --- | --- | --- |
25
+ | `genuine-bugs` | Low-count, scattered rules — the localised bugs Rigor caught. | **Work these first.** Highest value per fix. |
26
+ | `systemic-file-cluster` | One file × one rule, large count. | One fix may clear many — high leverage. Or escalate as systemic. |
27
+ | `activesupport-core-ext`, `gem-without-rbs` | Config gaps, not code bugs. | **Not reduction work** — these are `rigor-project-init` territory (wire the RBS bundle). Flag to the user; do not walk site by site. |
28
+ | `project-monkey-patch` | A DSL / monkey-patch Rigor can't see. | Escalate — a `pre_eval:` entry or a plugin clears the whole cluster. |
29
+ | `activerecord-relation-misinference` | Likely an engine gap. | Treat sites as candidate false positives (Phase 2). |
30
+
31
+ ### `rigor baseline dump --format json` — the bucket list
32
+
33
+ ```sh
34
+ rigor baseline dump --format json
35
+ ```
36
+
37
+ This is the authoritative list of `(file, rule, count)` buckets in
38
+ `.rigor-baseline.yml`. Within the priority tier the triage hints set,
39
+ **sort rules by ascending total count** — the smallest rules first.
40
+ A rule with 3 sites is either a quick win or a contained pattern;
41
+ either way it is finishable in one session, and finishing a rule is
42
+ more motivating than half-clearing a 200-site one.
43
+
44
+ So the working order is: rules flagged `genuine-bugs` first, then the
45
+ rest by ascending count, with config-gap hints handed back to the
46
+ user instead of walked.
47
+
48
+ ## Phase 2 — Sample and classify
49
+
50
+ Pick the chosen rule. Surface its actual diagnostics — run
51
+ `rigor check` scoped to the affected files so the user sees real
52
+ messages, not baseline rows:
53
+
54
+ ```sh
55
+ rigor check app/models/account.rb app/services/feed_service.rb
56
+ ```
57
+
58
+ (The baseline still suppresses these in a full run; naming the files
59
+ and reading the stream shows them. `--no-baseline` also works if you
60
+ want the whole project's live stream.)
61
+
62
+ From the rule's sites, **sample 3–5 distinct ones** — different
63
+ files, different shapes, not five copies of the same line. For each
64
+ sampled site, read the code and classify it. Ask the user to confirm
65
+ when the call is not clear-cut.
66
+
67
+ ### The three classifications
68
+
69
+ **Real bug** — Rigor found a genuine defect.
70
+
71
+ - A `possible-nil-receiver` where the value genuinely can be `nil` on
72
+ some path and the code would crash.
73
+ - An `undefined-method` that is a real typo or a removed method.
74
+ - An argument-type mismatch that would raise or misbehave.
75
+ - Tell: trace the value's origin and you find a path that actually
76
+ produces the bad case.
77
+
78
+ **Stylistic / safe** — the static reading is worst-case-sound, but
79
+ the site is fine in practice.
80
+
81
+ - `T | nil` where the slot is always initialised before this point
82
+ by code Rigor's narrowing doesn't follow (a memoised getter, a
83
+ framework lifecycle guarantee).
84
+ - A dynamic `send` over a known-finite, known-safe tag set.
85
+ - An idiom repeated across dozens of files — at that scale the
86
+ pattern *is* the project's style.
87
+ - Tell: you can articulate *why* the bad case never reaches this
88
+ line, and that reason is a real invariant, not a hope.
89
+
90
+ **False positive** — Rigor is simply wrong; a correct analyzer would
91
+ not flag this site.
92
+
93
+ - The narrowing should have followed an `&.` chain or an early
94
+ `return`/`raise` guard and didn't.
95
+ - The receiver type is misinferred (e.g. an ActiveRecord relation
96
+ typed as `Array`).
97
+ - Tell: the code is correct *and* a reasonable type checker should
98
+ see that it is correct — the gap is in Rigor, not the code.
99
+
100
+ The distinction between "stylistic / safe" and "false positive"
101
+ matters: stylistic-safe sites get a `# rigor:disable` (the code stays
102
+ as is, the suppression is intentional); false positives get a Rigor
103
+ issue (the analyzer should improve). Both stay out of the count, but
104
+ only one of them is feedback Rigor's maintainers can act on.
105
+
106
+ Carry the per-site classifications into Phase 3
107
+ ([`02-fix-or-suppress.md`](02-fix-or-suppress.md)).
@@ -0,0 +1,133 @@
1
+ # 02 — Act on the classification, then refresh
2
+
3
+ Covers **Phase 3** (act per site) and **Phase 4** (refresh the
4
+ baseline). Input: the per-site classifications from
5
+ [`01-classify.md`](01-classify.md).
6
+
7
+ ## Phase 3 — Act
8
+
9
+ ### Real bug → fix the code
10
+
11
+ Propose the fix and offer to apply it. The fix is ordinary code work
12
+ — the value is that Rigor surfaced a defect that was latent because
13
+ the line is rarely exercised. Common shapes:
14
+
15
+ - `possible-nil-receiver` → add the missing guard (`return unless x`,
16
+ `x&.method`, an early raise), or fix the upstream code so the value
17
+ is never `nil` here.
18
+ - `undefined-method` typo → correct the method name.
19
+ - argument-type mismatch → pass the right type, or fix the signature.
20
+
21
+ After a real-bug fix the diagnostic is *gone*, not suppressed — the
22
+ bucket count drops on its own.
23
+
24
+ ### Stylistic / safe → `# rigor:disable` with intent
25
+
26
+ The code is staying as it is; the suppression is a deliberate,
27
+ recorded decision. Place a per-line comment at the end of the
28
+ offending line:
29
+
30
+ ```ruby
31
+ config.fetch(:timeout) # rigor:disable call.possible-nil-receiver — set in initializer
32
+ ```
33
+
34
+ Placement rules:
35
+
36
+ - **Per-line `# rigor:disable <rule>`** is the default. It keeps the
37
+ suppression visible exactly where it applies, and a future reader
38
+ sees the intent. Always name the specific rule, never `all`.
39
+ - Add a short reason after the rule — *why* the site is safe. A bare
40
+ `# rigor:disable` rots; `# rigor:disable … — set in initializer`
41
+ survives review.
42
+ - **Per-file `# rigor:disable-file <rule>`** only when one file has
43
+ many sites of the same rule and they are all the same safe idiom.
44
+ Escalate this to the user as a decision (it is coarser — it also
45
+ silences *future* sites of that rule in the file).
46
+
47
+ A `# rigor:disable`d diagnostic is suppressed *before* the baseline
48
+ filter, so once the comment is in place the site no longer counts
49
+ toward its bucket.
50
+
51
+ ### False positive → open a Rigor issue
52
+
53
+ Leave the site baselined (do not `# rigor:disable` it — that would
54
+ imply the code is the thing to live with, when actually Rigor is).
55
+ Open an issue on the Rigor project:
56
+
57
+ <https://github.com/rigortype/rigor/issues>
58
+
59
+ A useful issue includes:
60
+
61
+ - The rule id and the exact diagnostic message.
62
+ - A **minimal** code snippet that reproduces the false positive —
63
+ reduce it to the smallest shape that still mis-fires.
64
+ - What the correct inference should be, and why.
65
+ - The `rigor version` output.
66
+
67
+ This is the feedback loop that makes the analyzer better — a false
68
+ positive reported with a minimal repro is far more actionable than
69
+ one buried in a baseline. While the issue is open the baseline keeps
70
+ the site quiet.
71
+
72
+ ## Phase 4 — Refresh the baseline
73
+
74
+ After working a rule (fixes applied, `# rigor:disable` comments
75
+ added, issues filed), the live diagnostic count for the touched
76
+ buckets has dropped below the recorded count. Make the baseline
77
+ reflect reality.
78
+
79
+ First, inspect the drift:
80
+
81
+ ```sh
82
+ rigor baseline drift
83
+ ```
84
+
85
+ This reports each bucket as `within` / `over` / `cleared` /
86
+ `reducible`. After a reduction session you expect `cleared` (the
87
+ bucket is now empty) and `reducible` (the bucket shrank but is not
88
+ empty) rows.
89
+
90
+ Then refresh:
91
+
92
+ ```sh
93
+ rigor baseline regenerate
94
+ ```
95
+
96
+ `regenerate` rewrites `.rigor-baseline.yml` from a fresh `rigor
97
+ check` run — cleared buckets disappear, reducible buckets get their
98
+ new lower counts. This is the command that *banks* the session's
99
+ gains: until you regenerate, the baseline still records the old,
100
+ higher numbers.
101
+
102
+ `rigor baseline prune` is the narrower tool — it drops only the
103
+ fully-`cleared` buckets and leaves reducible ones at their old count.
104
+ Prefer `regenerate` at the end of a reduction session; it captures
105
+ both kinds of progress in one step.
106
+
107
+ Commit the updated `.rigor-baseline.yml` together with the code
108
+ fixes and `# rigor:disable` comments, so the smaller baseline and the
109
+ work that earned it land together.
110
+
111
+ ## Verifying the session
112
+
113
+ Confirm the gains held:
114
+
115
+ ```sh
116
+ rigor baseline drift # expect "No drift detected" after regenerate
117
+ rigor check # the project's gate — should still pass
118
+ ```
119
+
120
+ If the project's CI uses `rigor check --baseline-strict`, the run
121
+ after `regenerate` should be clean — a stale baseline (numbers not
122
+ refreshed) is exactly the deficit drift that gate fails on.
123
+
124
+ ## Output of this module — session complete
125
+
126
+ - Code fixes for the real bugs.
127
+ - Intentional, reasoned `# rigor:disable` comments for the
128
+ stylistic-safe sites.
129
+ - Rigor issues filed for the false positives.
130
+ - A regenerated, smaller `.rigor-baseline.yml`, committed with the
131
+ work.
132
+
133
+ Run the skill again next session to take the next rule.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: rigor-plugin-author
3
+ description: |
4
+ Author a Rigor plugin in your own repository — a standalone rigor-prefixed gem or a project-private plugin — to teach Rigor about an application DSL, framework, or metaprogramming pattern. Covers gemspec / Gemfile wiring, the plugin class and AST walker, return-type contributions, fixture-based testing (RSpec or Minitest), and version pinning against the pre-1.0 plugin contract. Triggers: "write a Rigor plugin for our DSL", "extend Rigor for X in this project", "make Rigor understand our macro". NOT for onboarding a project (use rigor-project-init) or reducing a baseline (use rigor-baseline-reduce).
5
+ license: MPL-2.0
6
+ metadata:
7
+ version: 0.1.0
8
+ homepage: https://github.com/rigortype/rigor
9
+ ---
10
+
11
+ # Rigor Plugin Author (external)
12
+
13
+ Author a Rigor plugin **in your own repository** — to teach Rigor a
14
+ DSL, framework convention, or metaprogramming pattern its core
15
+ analyzer cannot follow. The result is either a standalone
16
+ `rigor-<id>` gem or a project-private plugin.
17
+
18
+ This skill targets **external authors**: you depend on the published
19
+ `rigortype` gem (`bundle add rigortype`) and use the public plugin
20
+ API surface — `Rigor::Plugin::Base` and friends. It does not assume
21
+ the rigor monorepo's `Makefile`, `spec/integration/` helpers, or Nix
22
+ environment.
23
+
24
+ > **Authoring inside the rigor monorepo instead?** If you are adding
25
+ > a plugin to rigor's own `plugins/` or `examples/` tree, use the
26
+ > contributor `rigor-plugin-author` skill bundled in that repo — it
27
+ > covers the in-repo layout, `plugin_helpers.rb`, and `make verify`.
28
+ > This skill is for plugins that live in *your* project.
29
+
30
+ ## Important — the plugin contract is a preview (pre-1.0)
31
+
32
+ Rigor's plugin contract (ADR-2) is **not yet frozen**. It stabilises
33
+ at `rigortype` v0.2.0. Until then, treat each `rigortype` minor
34
+ release as potentially contract-changing:
35
+
36
+ - **Pin `rigortype` tightly** — `>= 0.1.0, < 0.2.0` in your gemspec
37
+ or Gemfile. Do not float across the v0.2.0 boundary blind.
38
+ - Expect to **revisit your plugin** when you bump `rigortype` to the
39
+ next minor. The walker hook signature, the `Diagnostic` shape, and
40
+ the type carriers may shift.
41
+ - After v0.2.0 the contract is stable and ordinary semver applies.
42
+
43
+ Tell the user this up front. A plugin written today is a preview
44
+ artefact, valuable but not yet on a stable foundation.
45
+
46
+ ## Phase 0 — Standalone gem or project-private?
47
+
48
+ Decide where the plugin lives before scaffolding anything.
49
+
50
+ | Signal | Build it as |
51
+ | --- | --- |
52
+ | The DSL/library is reusable across projects, or you want to publish it | **Standalone `rigor-<id>` gem** — own repo, own gemspec, RubyGems-publishable |
53
+ | The pattern is specific to *one* application's own code / in-house DSL | **Project-private plugin** — lives inside the app repo, never published |
54
+
55
+ Both produce the same plugin class and walker; they differ only in
56
+ packaging and activation (Phase 1). When unsure, default to
57
+ **project-private** — it is less ceremony, and a plugin can always
58
+ be extracted into a gem later.
59
+
60
+ Do NOT use this skill for:
61
+
62
+ - **Onboarding a project to Rigor** (writing `.rigor.yml`, choosing
63
+ plugins) → `rigor-project-init`.
64
+ - **Reducing a baseline** → `rigor-baseline-reduce`.
65
+ - **Editing an existing, already-working plugin** — that is ordinary
66
+ code work; modify the plugin class directly.
67
+
68
+ ## How a plugin works — the one-paragraph model
69
+
70
+ A Rigor plugin is a Ruby class that subclasses `Rigor::Plugin::Base`,
71
+ declares a `manifest(id:, version:, …)`, and calls
72
+ `Rigor::Plugin.register(self)` at load time. When `.rigor.yml` lists
73
+ the plugin under `plugins:`, Rigor `require`s it and, for every
74
+ analysed file, calls the plugin's `#diagnostics_for_file(path:,
75
+ scope:, root:)` — handing it the file's Prism AST (`root`) and a
76
+ `scope` it can query for inferred types. The plugin walks the AST and
77
+ returns an array of `Rigor::Analysis::Diagnostic`. Optionally it also
78
+ implements `#flow_contribution_for(call_node:, scope:)` to *supply* a
79
+ return type for call sites the core analyzer types as `Dynamic`.
80
+
81
+ ## Phase outline
82
+
83
+ | Phase | What | Reference |
84
+ | --- | --- | --- |
85
+ | 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) |
86
+ | 2 | The walker — `diagnostics_for_file`, building `Diagnostic`s, querying `scope.type_of`, optional `flow_contribution_for`, RBS for the DSL. | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) |
87
+ | 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) |
88
+
89
+ ## Reading order — modules
90
+
91
+ | Module | Read | Covers |
92
+ | --- | --- | --- |
93
+ | 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. |
94
+ | 2 | [`references/02-walker-and-types.md`](references/02-walker-and-types.md) | **Phase 2.** The `diagnostics_for_file` AST walk over Prism nodes, the `Diagnostic` constructor shape, asking the analyzer for inferred types via `scope.type_of`, the optional `flow_contribution_for` return-type hook, and shipping `sig/*.rbs` so the DSL's types are visible. |
95
+ | 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. |
@@ -0,0 +1,195 @@
1
+ # 01 — Package and scaffold
2
+
3
+ Covers **Phase 1**. Output: a directory tree, a plugin class
4
+ skeleton that registers itself, and an `.rigor.yml` that activates
5
+ it.
6
+
7
+ ## Naming
8
+
9
+ - **Plugin id** — kebab-case, starts with a letter, matches
10
+ `/\A[a-z][a-z0-9._-]*\z/`. Descriptive: `units`, `myapp-dsl`,
11
+ `legacy-macros`.
12
+ - **Require name** — Rigor activates a plugin by `require`-ing the
13
+ name in the `.rigor.yml` `plugins:` entry. The convention is
14
+ `rigor-<id>`, and the file that name resolves to must, when
15
+ loaded, call `Rigor::Plugin.register`.
16
+ - **Ruby class** — CamelCase under `Rigor::Plugin`, e.g.
17
+ `Rigor::Plugin::MyappDsl`.
18
+
19
+ ## Standalone gem layout
20
+
21
+ ```text
22
+ rigor-<id>/ # its own repository
23
+ ├── README.md
24
+ ├── rigor-<id>.gemspec
25
+ ├── Gemfile # gem "rigortype" (dev); gemspec
26
+ ├── lib/
27
+ │ ├── rigor-<id>.rb # require name → require_relative the class
28
+ │ └── rigor/plugin/
29
+ │ └── <id>.rb # the plugin class + Rigor::Plugin.register
30
+ │ └── <id>/ # only if it needs helpers
31
+ │ └── analyzer.rb # AST walker extracted out of the class
32
+ ├── sig/ # optional — RBS for the DSL (Phase 2)
33
+ └── spec/ or test/ # fixture tests (Phase 3)
34
+ ```
35
+
36
+ ### Gemspec template
37
+
38
+ ```ruby
39
+ # rigor-<id>.gemspec
40
+ # frozen_string_literal: true
41
+
42
+ Gem::Specification.new do |spec|
43
+ spec.name = "rigor-<id>"
44
+ spec.version = "0.1.0"
45
+ spec.authors = ["Your Name"]
46
+ spec.summary = "Rigor plugin: <one line>."
47
+ spec.description = "<two sentences naming the DSL / API the plugin types>."
48
+ spec.license = "MIT" # your choice
49
+ spec.required_ruby_version = ">= 3.2"
50
+
51
+ spec.files = Dir.glob(%w[README.md lib/**/*.rb sig/**/*.rbs])
52
+ spec.require_paths = ["lib"]
53
+
54
+ spec.add_dependency "prism", ">= 1.0", "< 2.0"
55
+ # Pin tightly — the plugin contract is pre-1.0 (see SKILL.md).
56
+ spec.add_dependency "rigortype", ">= 0.1.0", "< 0.2.0"
57
+ end
58
+ ```
59
+
60
+ `prism` is Rigor's parser; a plugin walks `Prism::Node`s, so depend
61
+ on it directly.
62
+
63
+ ## Project-private layout
64
+
65
+ A project-private plugin lives inside the application repo and is
66
+ never published. Two ways to make `require "rigor-<id>"` succeed:
67
+
68
+ ### Recommended — a path gem
69
+
70
+ Keep the plugin in a subdirectory with its own gemspec, and reference
71
+ it from the app's `Gemfile` by path:
72
+
73
+ ```text
74
+ your-app/
75
+ ├── Gemfile
76
+ ├── rigor-plugin/ # the plugin, unpublished
77
+ │ ├── rigor-myapp.gemspec
78
+ │ └── lib/
79
+ │ ├── rigor-myapp.rb
80
+ │ └── rigor/plugin/myapp.rb
81
+ └── .rigor.yml
82
+ ```
83
+
84
+ ```ruby
85
+ # your-app/Gemfile
86
+ gem "rigortype", "~> 0.1.0"
87
+ gem "rigor-myapp", path: "rigor-plugin"
88
+ ```
89
+
90
+ `bundle install` then puts `rigor-myapp` on the load path, so Rigor's
91
+ `require "rigor-myapp"` resolves. This keeps the plugin versioned,
92
+ testable, and trivially promotable to a real gem later.
93
+
94
+ ### Simplest — a bare file on the load path
95
+
96
+ If you do not want a gemspec at all, drop `rigor-myapp.rb` somewhere
97
+ and run `rigor` with that directory on `RUBYLIB`:
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.
109
+
110
+ ## The plugin class skeleton
111
+
112
+ `lib/rigor-<id>.rb` — the require entry point:
113
+
114
+ ```ruby
115
+ # frozen_string_literal: true
116
+
117
+ require_relative "rigor/plugin/<id>"
118
+ ```
119
+
120
+ `lib/rigor/plugin/<id>.rb` — the plugin class:
121
+
122
+ ```ruby
123
+ # frozen_string_literal: true
124
+
125
+ require "rigor/plugin"
126
+
127
+ module Rigor
128
+ module Plugin
129
+ class MyappDsl < Rigor::Plugin::Base
130
+ manifest(
131
+ id: "<id>",
132
+ version: "0.1.0",
133
+ description: "<one line>",
134
+ # Optional: declare config keys the user may set under
135
+ # `.rigor.yml` plugins: [{ gem:, config: { … } }].
136
+ config_schema: {
137
+ # "module_name" => :string,
138
+ # "rules" => :array,
139
+ }
140
+ )
141
+
142
+ # Called once at load time with the service container.
143
+ # Read config defaults here. `config` is the validated
144
+ # user config Hash.
145
+ def init(_services)
146
+ @module_name = config.fetch("module_name", "Default")
147
+ end
148
+
149
+ # Called per analysed file. `root` is the file's Prism AST,
150
+ # `scope` answers inferred-type queries. Return an Array of
151
+ # Rigor::Analysis::Diagnostic. See 02-walker-and-types.md.
152
+ def diagnostics_for_file(path:, scope:, root:)
153
+ []
154
+ end
155
+ end
156
+
157
+ Rigor::Plugin.register(MyappDsl)
158
+ end
159
+ end
160
+ ```
161
+
162
+ `Rigor::Plugin.register` at the bottom is mandatory — the loader
163
+ `require`s the gem, then looks for a freshly-registered plugin
164
+ class. A gem that registers nothing fails to load with a clear
165
+ error.
166
+
167
+ ## Activate the plugin in `.rigor.yml`
168
+
169
+ ```yaml
170
+ plugins:
171
+ - rigor-<id>
172
+
173
+ # Hash form when the plugin takes config:
174
+ # plugins:
175
+ # - gem: rigor-<id>
176
+ # config:
177
+ # module_name: MyApp
178
+ ```
179
+
180
+ Confirm activation with the public CLI:
181
+
182
+ ```sh
183
+ rigor check
184
+ ```
185
+
186
+ A misconfigured plugin surfaces as a `plugin-loader` diagnostic
187
+ rather than crashing the run — read that message if the plugin seems
188
+ inert.
189
+
190
+ ## Output of this module
191
+
192
+ A scaffolded plugin that loads (even if `diagnostics_for_file` still
193
+ returns `[]`) and is activated in `.rigor.yml`. Proceed to Phase 2
194
+ ([`02-walker-and-types.md`](02-walker-and-types.md)) to make it
195
+ actually analyse.