rigortype 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules.rb +96 -3
  3. data/lib/rigor/analysis/erb_template_detector.rb +38 -0
  4. data/lib/rigor/analysis/runner.rb +6 -1
  5. data/lib/rigor/analysis/worker_session.rb +6 -1
  6. data/lib/rigor/cli/plugins_command.rb +308 -0
  7. data/lib/rigor/cli/plugins_renderer.rb +173 -0
  8. data/lib/rigor/cli/skill_command.rb +170 -0
  9. data/lib/rigor/cli.rb +37 -1
  10. data/lib/rigor/configuration/severity_profile.rb +3 -0
  11. data/lib/rigor/inference/block_parameter_binder.rb +35 -0
  12. data/lib/rigor/inference/expression_typer.rb +69 -30
  13. data/lib/rigor/inference/indexed_narrowing.rb +187 -0
  14. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +24 -0
  15. data/lib/rigor/inference/method_dispatcher.rb +23 -0
  16. data/lib/rigor/inference/mutation_widening.rb +285 -0
  17. data/lib/rigor/inference/narrowing.rb +72 -4
  18. data/lib/rigor/inference/scope_indexer.rb +409 -12
  19. data/lib/rigor/inference/statement_evaluator.rb +256 -4
  20. data/lib/rigor/scope.rb +195 -4
  21. data/lib/rigor/version.rb +1 -1
  22. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +22 -1
  23. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +94 -6
  24. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +11 -1
  25. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +7 -1
  26. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +135 -11
  27. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +94 -43
  28. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +138 -35
  29. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +17 -3
  30. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -0
  31. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +13 -3
  32. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +6 -2
  33. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +83 -7
  34. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +4 -1
  35. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +16 -1
  36. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +81 -5
  37. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +11 -3
  38. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +194 -5
  39. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +264 -0
  40. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +100 -0
  41. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +175 -0
  42. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +64 -3
  43. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1107 -59
  44. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +81 -4
  45. data/sig/rigor/scope.rbs +23 -0
  46. data/skills/rigor-baseline-reduce/SKILL.md +100 -0
  47. data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
  48. data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
  49. data/skills/rigor-plugin-author/SKILL.md +95 -0
  50. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
  51. data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
  52. data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
  53. data/skills/rigor-project-init/SKILL.md +129 -0
  54. data/skills/rigor-project-init/references/01-detect.md +101 -0
  55. data/skills/rigor-project-init/references/02-configure.md +185 -0
  56. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
  57. data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
  58. metadata +22 -1
@@ -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.
@@ -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.