rigortype 0.4.3 → 0.4.5

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b20cef50f4d076f1afefb8c20563cd21a95a56b01bf4db3257371adfe7c64d5d
4
- data.tar.gz: 9c4f5f8825106ece96fb2ac00dc1ad4ef43f302368f1a12f87521c725530b924
3
+ metadata.gz: 4da9b991a97cd586806b416facb6dbf6c3a3b938c7c9433e330bc44c0757fdaf
4
+ data.tar.gz: 1167e8d91ccbc8740ac4094f67db82937bd3b374d2488a43e514f7922c7a98b1
5
5
  SHA512:
6
- metadata.gz: 4418a4a75a22d27038d0607caa5a79723470e68f9662da1aec0b79e8056a1b157f78cf85f83cb51f646aa22d2634b0d851bb9f0132a548f5104acdee2372d1d2
7
- data.tar.gz: 026710fb13eebddf98776d6a9a3fb8e1738a7cc5fdd77c326afa0adb680c0760462d9e41b4aff57d1c41d1878fc05c41a8658be7b69ed4f265709aa27f6fc825
6
+ metadata.gz: db2047e49879c399812704702d4064304bf4ac9be8fe25132cc65ac441e2962f7b8c45cf59371727dffaf52caab9accbfb7c8ea0abe1976f264a7a4506e46e6f
7
+ data.tar.gz: e426da15aeb714ab906df710946ae3b2c96b35509632b9c35efba289d303da519f58e3cd362a8b47c4ba8a9f883e54e657edeb3e09c1934b4cd542f119440678
data/README.md CHANGED
@@ -106,7 +106,7 @@ https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install
106
106
  ```
107
107
 
108
108
  Ready-made prompts — 日本語, 简体中文, 繁體中文, 한국어, Español,
109
- Português, Français, Deutsch, Italiano, Tiếng Việt, ภาษาไทย,
109
+ Português (Brasil), Português, Français, Deutsch, Italiano, Tiếng Việt, ภาษาไทย,
110
110
  Bahasa Indonesia, Polski, Українська, Русский, Română, Türkçe,
111
111
  العربية, فارسی — are in
112
112
  the [installation guide](https://rigor.typedduck.fail/manual/01-installation/#set-up-in-your-language).
@@ -232,7 +232,7 @@ rigor docs --list # list every bundled page
232
232
 
233
233
  ## Status
234
234
 
235
- Current release: **`v0.4.3`** (2026-10-15) — on the
235
+ Current release: **`v0.4.5`** (2026-10-19) — on the
236
236
  `0.4.x` line, continuing the evaluation era opened by `v0.2.0`, the first
237
237
  publicly-announced (general / evaluation) release. The line publishes
238
238
  an enumerated [compatibility surface](docs/compatibility.md) as a
@@ -256,9 +256,9 @@ Mozilla Public License Version 2.0. See [`LICENSE`](LICENSE).
256
256
 
257
257
  ## Sponsors
258
258
 
259
- Rigor is built and maintained by one person, who has spent the better
260
- part of the last month — nearly every spare hour of personal time — on
261
- this project. Sustainable development depends on sustainable funding.
259
+ Rigor is built and maintained by one person, who has put nearly every
260
+ spare hour of personal time into it since development began in April
261
+ 2026. Sustainable development depends on sustainable funding.
262
262
 
263
263
  **The current monthly budget is well short of the $200 minimum needed
264
264
  to keep this pace going; $600 or more would let me work on it
@@ -0,0 +1,40 @@
1
+ # Rigor core overlay — supplemental core signatures.
2
+ #
3
+ # --- OpenStruct#method_missing (issue #1456) ---
4
+ #
5
+ # rbs 4.2 ships no `ostruct` signatures (since Ruby 4.0 `ostruct` is a bundled
6
+ # gem, not a default one), and the gem ships no `sig/`. The only `OpenStruct`
7
+ # declaration in the environment is json's add-on reopening in
8
+ # `stdlib/json/0/json.rbs` (`json_create` / `as_json` / `to_json`), so the
9
+ # class read as fully declared and `OpenStruct.new(name: "x").name` reported
10
+ # `call.undefined-method`.
11
+ #
12
+ # Upstream: ostruct 0.6.3 `lib/ostruct.rb` defines each field as a
13
+ # per-instance singleton method (`new_ostruct_member!` through
14
+ # `define_singleton_method!`) and answers the rest through
15
+ # `private def method_missing(mid, *args)`, which this file declares. It
16
+ # defines no `respond_to_missing?` of its own (Kernel's answers, and the
17
+ # fields are real singleton methods), so none is declared here.
18
+ #
19
+ # The per-field accessors cannot be declared on the class at all, so
20
+ # `Rigor::Analysis::CheckRules::CORE_OVERLAY_OPEN_RECEIVERS` names
21
+ # `OpenStruct` as an open receiver: an RBS `method_missing` alone does not
22
+ # stop the undefined-method rule. That list is what quiets `rigor check`;
23
+ # this file supplies the declaration the maintainer decision asked for.
24
+ #
25
+ # The declaration sits on a module the class includes, as `process.rbs`
26
+ # does, so a project `sig/` that declares `OpenStruct#method_missing` itself
27
+ # overrides it instead of raising `DuplicatedMethodDefinitionError`.
28
+ #
29
+ # Maintainer decision on #1456 (2026-10-16, option c): this narrow overlay.
30
+ # The general rule for a class declared only by another library's partial
31
+ # reopening is #1978.
32
+ module RigorCoreOverlay
33
+ module OpenStructMissing
34
+ private def method_missing: (Symbol mid, *untyped args) -> untyped
35
+ end
36
+ end
37
+
38
+ class OpenStruct
39
+ include RigorCoreOverlay::OpenStructMissing
40
+ end
@@ -165,6 +165,11 @@ The `sig.skipped.*` reasons are:
165
165
  RBS for it, so `--write` and `--check` are unaffected. Its
166
166
  JSON row has no `class` key. `def self.x`, a `def` inside a
167
167
  block body and one inside `class << obj` are not reported.
168
+ - `sig.skipped.uncertain-module-function` — a bare `public`,
169
+ `private` or `protected` that may not run (under an `if`, in
170
+ a block) sits between a bare `module_function` and the
171
+ method, so it is either a module function or an instance
172
+ method only. sig-gen writes neither.
168
173
  - `sig.skipped.unrenderable-rbs` — the signature Rigor
169
174
  rendered for this method does not parse as RBS. This one
170
175
  is a **bug in Rigor**, not a property of your code: every
@@ -527,7 +532,13 @@ A declared `untyped` that you wrote in `sig/` is treated
527
532
  the same way for a value literal: an Integer, String or
528
533
  Symbol the body proves is widened to its class (`-> 1`
529
534
  becomes `-> Integer`, `-> ("S" | nil)` becomes
530
- `-> (String | nil)`), while `nil`, `true`, `false` and a
535
+ `-> (String | nil)`), and `true` or `false` is widened to
536
+ `bool` (written `(false | true)`), so a subclass that
537
+ overrides the method with the other one is not reported as
538
+ widening the return. A body that returns only `nil` keeps
539
+ the `untyped`: it is the shape of a template method a
540
+ subclass fills in, and `-> nil` would make every override
541
+ that returns a value a widening. `nil` inside a union and a
531
542
  computed `Float` are proposed as they are. An `untyped` that
532
543
  only the inline reader synthesizes for a `def` you did not
533
544
  annotate keeps the literal, as does a method no `.rbs`
@@ -590,9 +601,16 @@ list is the current state):
590
601
  `attr_writer` / `attr_accessor` ivar reads untyped.
591
602
  The accessor, and any other method returning that
592
603
  ivar, is skipped even beside a concrete write such as
593
- `@logger = Logger.new`, unless `--params=observed`
594
- types the constructor parameter the ivar is assigned
595
- from. The
604
+ `@logger = Logger.new`. `--params=observed` types an
605
+ `attr_reader` from the constructor parameter its ivar
606
+ is assigned from (`Box.new(0)` gives
607
+ `def count: () -> 0`) only when that assignment is the
608
+ ivar's only write in the class's body in that file: an
609
+ attr writer, a hand-written setter, or a write in any
610
+ other instance method leaves both reader and writer
611
+ skipped. A writer declared in another file, in a
612
+ superclass or module, or through `class_eval` from
613
+ outside is not seen. The
596
614
  generator emits the long-form `def name: () -> T`
597
615
  spelling so the writer's merge path applies unchanged;
598
616
  existing short-form `attr_reader name: T` declarations
@@ -616,9 +634,28 @@ two are wired today, one is reserved.
616
634
  | Policy | Behaviour |
617
635
  | --- | --- |
618
636
  | `untyped` (default) | Every parameter is spelled `untyped`. No inference-derived parameter contract is imposed on future callers. The user retains complete authorship over parameter typing. |
619
- | `observed` | Collect argument types from every call site under `--observe=PATH...` (defaults to the configured `test_paths:`, or whichever of `spec/` and `test/` exist), union per parameter position, erase to RBS, emit the union. With no test root to observe, or a declared root that does not exist, sig-gen says so on stderr. |
637
+ | `observed` | Collect argument types from every call site under `--observe=PATH...` (defaults to the configured `test_paths:`, or whichever of `spec/` and `test/` exist), union per parameter position, erase to RBS, emit the union. With no test root to observe, or a declared root that does not exist or is not read, sig-gen says so on stderr, and `--format=json` carries the same fact in a top-level `observation` key (below). |
620
638
  | `observed-strict` | Reserved. Will additionally widen to capability roles (`_ToStr`, `_ToS`, …) once the role catalog ships. Currently rejected with a usage error. |
621
639
 
640
+ Under `--params=observed`, the `--print` / `--diff` JSON payload gains
641
+ a top-level `observation` key next to `candidates`, so a consumer that
642
+ never reads stderr (the MCP `rigor_sig_gen` tool included) can tell
643
+ "no test root was observed" from "the tests pass no typed arguments":
644
+
645
+ ```json
646
+ { "candidates": [ … ],
647
+ "observation": { "paths": ["/app/spec"], "missing": ["/app/tests"] } }
648
+ ```
649
+
650
+ `paths` are the roots the collector reads (a directory or a `.rb`
651
+ file); `missing` are the other declared roots: ones that do not exist,
652
+ or exist but are not read (`README.md`). An identical repeat is listed once. `paths: []` with `missing: []` means
653
+ there was no test root at all. Paths are spelled as the run resolved
654
+ them: `--observe=PATH` as typed, `test_paths:` as absolute paths, resolved against the
655
+ config file's directory, auto-detected `spec/` / `test/` absolute. The key is
656
+ absent under `--params=untyped`, and absent from the `--write` and
657
+ `--check` JSON payloads, which have their own shapes.
658
+
622
659
  The default deliberately favours `untyped` because of
623
660
  [ADR-5](../adr/5-robustness-principle.md)'s clause 2: a
624
661
  method's parameter contract should be the **most permissive**
@@ -704,7 +741,7 @@ output it got before.
704
741
  ## RSpec-aware observations
705
742
 
706
743
  When the observed test roots hold an RSpec suite, the
707
- generator recognises three RSpec-shaped binding patterns
744
+ generator recognises four RSpec-shaped binding patterns
708
745
  and uses them to type receivers that would otherwise
709
746
  degrade to `Dynamic[top]`:
710
747
 
@@ -712,10 +749,12 @@ degrade to `Dynamic[top]`:
712
749
  RSpec.describe Calc do
713
750
  subject { Calc.new } # binds :subject → Nominal[Calc]
714
751
  let(:other) { Calc.new } # binds :other → Nominal[Calc]
752
+ before { @calc = Calc.new } # binds @calc → Nominal[Calc]
715
753
 
716
754
  it "..." do
717
755
  subject.greet("Alice") # observed: Calc#greet receives String
718
756
  other.greet("Bob") # observed: same
757
+ @calc.greet("Carol") # observed: same
719
758
  described_class.new.add(1, 2) # observed: Calc#add receives Integer, Integer
720
759
  end
721
760
  end
@@ -724,17 +763,36 @@ end
724
763
  The recogniser handles `RSpec.describe Foo`, bare
725
764
  `describe Foo` (no `RSpec.` receiver), `subject { … }`,
726
765
  `subject(:name) { … }`, `let(:name) { … }`, `let!(:name)`,
727
- and `described_class.new(...)`. Same-name `let` bindings
728
- across nested scopes are last-wins; the recogniser does not
729
- re-implement RSpec's full scope rules — the typical
730
- one-spec-file shape is the target.
766
+ an ivar assigned by a top-level statement of a `before`
767
+ block, and `described_class.new(...)`. Same-name `subject` /
768
+ `let` bindings across nested scopes are last-wins; the
769
+ recogniser does not re-implement RSpec's full scope rules —
770
+ the typical one-spec-file shape is the target. A `before`
771
+ ivar is scoped: it types reads only inside the `describe` /
772
+ `context` block that holds the `before`, through nested
773
+ `describe` / `context` / `it` / hook blocks but not through
774
+ any other block (`Class.new { … }`, `instance_eval { … }`,
775
+ `each { … }`), and a read that two
776
+ enclosing `before`s assign different classes is not
777
+ observed.
731
778
 
732
779
  A Minitest suite under `test/` is observed too, with no
733
780
  recogniser of its own: a call whose receiver types to one
734
- class counts. A receiver assigned in `setup` is the gap —
735
- inside a `test_*` method it reads as `Foo | nil`, and such
736
- a call is not observed yet
737
- ([#1389](https://github.com/rigortype/rigor/issues/1389)).
781
+ class counts. A receiver assigned in `setup` reads as
782
+ `Foo | nil` inside a `test_*` method, since `setup` is not
783
+ `initialize`; a receiver of that shape — one class or class
784
+ object plus `nil` — is observed as that class when `nil`
785
+ does not answer the method, because such a call cannot
786
+ have been made on `nil`. A call of a method `nil` does
787
+ answer (`==`, `respond_to?`, `is_a?`) is not observed, as
788
+ it may have run on `nil`; with `&.` it is, since `&.` skips
789
+ `nil`. Which methods `nil` answers comes from the loaded
790
+ RBS for `NilClass`, `Object` and `Kernel`, so a method a
791
+ library adds to `nil` (ActiveSupport's `try`, `as_json`)
792
+ counts only when that library's RBS is loaded.
793
+ `@foo&.bar(1)` counts the same way. A receiver
794
+ that could be one of two classes (`Foo | Bar | nil`), or
795
+ `untyped | nil`, is still not observed.
738
796
 
739
797
  The recogniser is part of the generator itself; you do not
740
798
  need to install `rigor-rspec` to benefit from it. If you
@@ -751,14 +809,45 @@ by side without coordination.
751
809
  every other byte of the file verbatim.
752
810
  - **Will** append a new `class Foo … end` block when the
753
811
  target file does not declare the class yet.
812
+ - **Will** treat a method as already declared when a file
813
+ under the signature tree declares it on the same class
814
+ with a `def` or an `attr_*` of the same kind (instance or
815
+ singleton), in any opening of the class in that file. A
816
+ `def self?.` declares both kinds, and a `def self?.`
817
+ proposal counts as declared when either kind is. The
818
+ method is updated (`--overwrite`) or skipped in the file
819
+ that declares it instead of being added to the class's
820
+ first file. A replacement lands only on a `def` of the
821
+ proposal's own kind in the first opening of the class:
822
+ a `def self?.` proposal over a lone `def self.f` or
823
+ `def f`, a plain proposal over a `def self?.f`, or a
824
+ method declared in a later opening is skipped. A
825
+ `def f: ... | ...` overload is never replaced and never
826
+ counts as the file that declares `f`. An `alias new old`
827
+ (or `alias self.new self.old`) declares `new` under its own
828
+ kind and is never replaced, so a proposal for that name is
829
+ skipped.
754
830
  - **Will not** touch files outside the configured signature
755
831
  tree.
832
+ - **Will** name every proposal it skipped, one
833
+ ` skipped Class#method (reason): <proposed RBS>` line under
834
+ its target (`Class.method` for a singleton), with the same
835
+ rows under `skipped` in `--format=json`. The reason is
836
+ `user-authored` (the target declares the member and the
837
+ writer does not replace it) or `outside-sig-root` (the
838
+ member routes to a file in a signature path other than the
839
+ first, which `--write` does not modify, whether the member
840
+ is declared there or is new to a class that lives there).
841
+ A target left unchanged by skips alone prints
842
+ `unchanged <file>`. The exit status stays `0`: a skip is
843
+ not a refusal, and `--check` still reports `sig/` as up to
844
+ date.
756
845
  - **Will not** replace an existing method declaration
757
846
  unless `--overwrite` is set AND the candidate is a
758
847
  `tighter-return`, and then only its return changes: the
759
848
  rest of the declaration is kept. Without `--overwrite`,
760
849
  existing declarations are user-authored and the new method
761
- is silently skipped.
850
+ is skipped.
762
851
  - **Will not** change an existing method declaration that
763
852
  disagrees with the method's inline declaration unless
764
853
  `--overwrite` is set; it reports the method as `REFUSED`
@@ -224,7 +224,7 @@ rigor type-scan PATH...
224
224
  `--show-recognized` includes fully-covered classes,
225
225
  `--threshold=RATIO` makes the command exit non-zero when the
226
226
  unrecognized-node ratio exceeds `RATIO`, and `--format=text|json`
227
- selects the output format.
227
+ selects the output format. `--config=PATH` overrides config discovery.
228
228
 
229
229
  ## `rigor effects`
230
230
 
@@ -354,6 +354,8 @@ accept `--config=PATH`, `--format=text|json` and `--full`;
354
354
  configured one — `--baseline <(git show
355
355
  origin/main:.rigor-effects.yml)` in a bot),
356
356
  `--strict-tolerated` and `--no-tolerated-effects`.
357
+ `explain` additionally accepts `--symbol=KEY` to explain one
358
+ specific unit instead of the changed ones.
357
359
 
358
360
  The file records two tables. `methods:` holds each method's
359
361
  **direct** summary — what its own body does, block literals
@@ -476,12 +478,13 @@ Print the catalogue entry for a diagnostic rule, or list every
476
478
  rule when called with no argument.
477
479
 
478
480
  ```sh
479
- rigor explain [rule]
481
+ rigor explain [options] [rule]
480
482
  ```
481
483
 
482
484
  `rule` is a rule ID (`call.undefined-method`), a legacy alias,
483
- or a family wildcard (`call`, `flow`, `def`, `assert`, `dump`).
484
- `--format=json` is available. Exit `64` for an unknown rule.
485
+ or a family wildcard (`call`, `flow`, `def`, `assert`, `dump`, `global`,
486
+ `suppression`, `static`, `effect`).
487
+ `--format=text|json` selects the output format. Exit `64` for an unknown rule.
485
488
 
486
489
  It also answers the `sig.skipped.*` identifiers `rigor sig-gen`
487
490
  prints when it declines to write a signature — `rigor explain
@@ -495,11 +498,13 @@ Compare the current diagnostics against a saved baseline JSON
495
498
  and report only what is new.
496
499
 
497
500
  ```sh
498
- rigor diff <baseline.json> [paths...]
501
+ rigor diff [options] <baseline.json> [paths...]
499
502
  ```
500
503
 
501
504
  `--current=PATH` compares against a saved diagnostics JSON
502
- instead of running a fresh check. Exit `1` when new
505
+ instead of running a fresh check. `--format=text|json` selects the
506
+ output format (default `text`), and `--config=PATH` passes a custom
507
+ configuration to the implicit check. Exit `1` when new
503
508
  diagnostics appear.
504
509
 
505
510
  ## `rigor sig-gen`
@@ -533,6 +538,10 @@ refuse a file, so a tighter return `--write` declines without
533
538
  `--overwrite` does not fail it; `--check --overwrite` counts one.
534
539
  See [handbook chapter 11](../handbook/11-sig-gen.md#keeping-sig-current-in-ci).
535
540
 
541
+ `--write` names each proposal it skipped (a member `sig/` already declares, an `alias` included, or one that
542
+ routes to a signature path it does not write to, because it is declared there or its class lives there) on its own `skipped Class#method (reason)` line; the exit status is
543
+ unchanged. See [handbook chapter 11](../handbook/11-sig-gen.md#safety-what---write-will-and-will-not-do).
544
+
536
545
  A method declared inline with `# @rbs` / `#:` is written as that
537
546
  declaration, not as what its body infers; a parameter-only
538
547
  annotation keeps its parameters and takes the return from the body,
@@ -616,7 +625,8 @@ rigor baseline <generate|regenerate|dump|drift|prune> [options]
616
625
 
617
626
  `generate` and `regenerate` accept `--output=PATH` and
618
627
  `--match-mode=rule|message`; `dump`, `drift`, and `prune` accept
619
- `--baseline=PATH` to read a non-default baseline file.
628
+ `--baseline=PATH` to read a non-default baseline file. `dump`
629
+ also accepts `--format=text|json` for machine-readable output.
620
630
 
621
631
  ## `rigor triage`
622
632
 
@@ -913,10 +923,11 @@ rigor mcp [--transport=stdio] [--config=PATH]
913
923
  ```
914
924
 
915
925
  `stdio` is the only transport. The server is a pure-Ruby
916
- JSON-RPC 2.0 implementation exposing seven read-only tools:
917
- `rigor_check`, `rigor_type_of`, `rigor_triage`,
926
+ JSON-RPC 2.0 implementation exposing eight read-only tools:
927
+ `rigor_check`, `rigor_type_of`, `rigor_triage`, `rigor_lens`,
918
928
  `rigor_annotate`, `rigor_sig_gen`, `rigor_explain`,
919
- `rigor_coverage`.
929
+ `rigor_coverage`. `rigor_lens` writes only the incremental
930
+ cache `rigor check --incremental` writes.
920
931
 
921
932
  ## `rigor lsp` vs `rigor mcp`
922
933
 
@@ -1162,8 +1173,17 @@ Runs a scoped analysis and audits:
1162
1173
  - **Plugin load errors** — whether every configured plugin loaded.
1163
1174
  - **Baseline drift** — whether the current diagnostics have drifted
1164
1175
  from the saved baseline.
1165
- - **Rails plugin gap** — whether `Gemfile.lock` contains Rails gems
1166
- but no Rails plugin is enabled.
1176
+ - **Plugin gap** — whether the project's direct dependencies (the
1177
+ `DEPENDENCIES` section of `Gemfile.lock`, plus the Rails components
1178
+ a direct `rails` stands for) include gems a bundled plugin models —
1179
+ Rails, Devise, Pundit, Sidekiq, ... — while `plugins:` does not list
1180
+ that plugin ([ADR-96](../adr/96-plugin-target-gems.md)). An entry
1181
+ with `enabled: false` counts as listed. Each missing plugin is a
1182
+ warning, since leaving one out can be deliberate; it is a failure
1183
+ only when `plugins:` lists none of the bundled plugins that model
1184
+ gems (those declaring `target_gems:`).
1185
+ Skipped when no `.rigor.yml` or `.rigor.dist.yml` is discovered in
1186
+ the current directory.
1167
1187
  - **Gemfile install** — whether Rigor itself resolves as one of the
1168
1188
  project's dependencies, which
1169
1189
  [Installing Rigor](01-installation.md) tells you not to do. Only a
@@ -1176,6 +1196,16 @@ Runs a scoped analysis and audits:
1176
1196
  versioned together, so a copy from another installation can run
1177
1197
  the engine with a mismatched plugin. A warning, not a failure;
1178
1198
  third-party plugins from your own bundle are never flagged.
1199
+ - **Bundle layout** — whether Rigor can locate the bundle a
1200
+ `Gemfile.lock` in the current directory was installed into; when it
1201
+ cannot (typically gems in the active Ruby's default gem home), no
1202
+ gem-shipped `sig/` is discovered. Rigor finds a bundle only where the
1203
+ project records it. A `bundler.bundle_path:` in `.rigor.yml` is used
1204
+ alone and must exist. Without one — and unless
1205
+ `bundler.auto_detect: false` — Rigor reads `.bundle/config`'s
1206
+ `BUNDLE_PATH`, then an in-tree `vendor/bundle`, then the global
1207
+ Bundler config. A warning, not a failure: the default layout is
1208
+ correct, and most gems ship no `sig/`.
1179
1209
 
1180
1210
  Text output prints `[PASS]`, `[FAIL]`, or `[WARN]` per check plus a
1181
1211
  routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
@@ -1236,6 +1266,7 @@ diagnostics about Rigor's own inference cutoffs and memory — see
1236
1266
  | `0` | Success — no diagnostic at or above the exit threshold (`error` by default; `rigor check --fail-on=SEVERITY` lowers it to `warning` or `info`). |
1237
1267
  | `1` | Diagnostics found at or above the threshold, or a per-command failure (parse error, missing file, new diagnostics on `diff`, effect drift on `effects check`). |
1238
1268
  | `64` | Usage error — unknown command, bad flag, malformed argument, or a value in `.rigor.yml` the loader cannot proceed on. |
1269
+ | `70` | Internal fatal error — `SystemStackError` or `NoMemoryError` inside the analysis; Rigor could not finish the run, so its report on disk is empty rather than clean. |
1239
1270
 
1240
1271
  `rigor triage` is the exception: it is advisory and always
1241
1272
  exits `0`.
@@ -78,7 +78,7 @@ cache:
78
78
  | --- | --- | --- | --- |
79
79
  | `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
80
80
  | `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
81
- | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. The walker does not follow `require` / `require_relative`, so list the files in load order: a method declared in two files takes the last-listed one, and a file that requires a later-listed one is still read in list order. |
81
+ | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. The walker does not follow `require` / `require_relative`, so list the files in load order: a method declared in two files takes the last-listed one, and a file that requires a later-listed one is still read in list order. `type-of`, `type-scan`, `trace`, `annotate` and `sig-gen` read the patches too, so they type a patched method as `rigor check` does. |
82
82
  | `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
83
83
 
84
84
  ### Signature generation
@@ -103,6 +103,8 @@ rigor: signature_paths: "/path/to/sig" does not exist (no signatures loaded from
103
103
  rigor: signature_paths: "/path/to/sig" matched 0 signature files
104
104
  rigor: libraries: "csb" is not an available RBS library (no signatures loaded from it)
105
105
  rigor: disable: "call.undefined-methdo" is not a recognized rule id; the suppression has no effect
106
+ rigor: disable: "plugin_loader.load-error" names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline
107
+ rigor: disable: "plugin.shoulda-matchers.unknown-column" names no plugin `plugins:` configures; the suppression has no effect
106
108
  rigor: severity_overrides: "flow.bogus" is not a recognized rule id; the override has no effect
107
109
  rigor: bundler.lockfile: "./missing/Gemfile.lock" does not exist
108
110
  ```
@@ -125,10 +127,21 @@ the JSON schema as you type rather than at check time.
125
127
  These are warnings, not errors — partial or optional bundles and
126
128
  forward-looking config are valid setups. The audit only fires on explicit,
127
129
  working-setup-safe signals: an unset default (auto-detected `<root>/sig`,
128
- auto-detected bundle) is never warned about, and a `disable:` /
129
- `severity_overrides:` token under a *plugin* family (`rspec.…`,
130
- `rbs_extended.…`) is left alone, since its rule id cannot be enumerated
131
- statically and may resolve at run time. The same findings appear in the
130
+ auto-detected bundle) is never warned about, and a
131
+ `rbs_extended.…`, `rbs.…` or `dynamic.…` key in `severity_overrides:` or
132
+ `disable:` is left alone, since the engine emits those ids outside the
133
+ catalogue. `disable:` and `severity_overrides:` are both checked
134
+ closely, because what they can reach is known: a plugin's rule counts
135
+ only in its qualified `plugin.<id>.<rule>` form, so a bare plugin rule
136
+ (`rspec.let-binding`) is warned about. A qualified token is checked
137
+ against `plugins:` only when every entry's plugin ID is known without
138
+ loading it — an explicit `id:`, or a bundled plugin's gem name minus
139
+ `rigor-`; one third-party gem listed without `id:` turns that check
140
+ off. A `disable:` token under `rbs_extended.…`, `rbs.…` or `dynamic.…`
141
+ is left alone, other than the reserved run-level rows (`rbs.coverage.…`,
142
+ `dynamic.dependency-source.…`), which are flagged; in `severity_overrides:`
143
+ those rows are the intended keys and are never flagged, while a bare plugin
144
+ rule, `all`, or a `plugin.`-qualified key for an unconfigured plugin is. The same findings appear in the
132
145
  `--format=json` payload under `config_warnings` (each tagged with a
133
146
  `kind`), so CI can assert on them.
134
147
 
@@ -136,9 +149,9 @@ statically and may resolve at run time. The same findings appear in the
136
149
 
137
150
  | Key | Type | Default | Meaning |
138
151
  | --- | --- | --- | --- |
139
- | `disable` | Array | `[]` | Rule IDs or families to suppress project-wide. |
152
+ | `disable` | Array | `[]` | Rule IDs or families to suppress project-wide; `all` suppresses every built-in check rule. A plugin's diagnostics are named by qualified ID (`plugin.<id>.<rule>`, or `plugin.<id>` for the whole plugin); a bare ID or `all` does not reach them. The run-level `.rigor.yml` rows (`plugin_loader.*`, `dynamic.dependency-source.*`, `rbs.coverage.*`, `plugin_trust.*`, `pool-degraded`) are not reached by any spelling; use `severity_overrides:` for those. See [Diagnostics](04-diagnostics.md#suppressing-a-diagnostic). |
140
153
  | `severity_profile` | String | `"balanced"` | `lenient`, `balanced`, or `strict` — see [Diagnostics](04-diagnostics.md). |
141
- | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. |
154
+ | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. A plugin's diagnostics are named by qualified ID (`plugin.<id>.<rule>`, or `plugin.<id>` for the whole plugin); a bare rule or family does not reach them. The plugin loader's own rows are `plugin_loader.load-error` / `plugin_loader.runtime-error` (or the bare `load-error` / `runtime-error`). |
142
155
  | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
143
156
  | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued changes early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). |
144
157
 
@@ -162,6 +162,15 @@ the diagnostic from the result entirely, which makes
162
162
  below — both silence a rule; the override reads as "this one
163
163
  rule, at this severity" alongside the rest of the profile.
164
164
 
165
+ A plugin's diagnostic is named by its qualified ID, as in `disable:`:
166
+ `plugin.<id>.<rule>` for one rule (`plugin.rails-i18n.load-error: off`),
167
+ or `plugin.<id>` for every row of that plugin. A bare rule or a family
168
+ never reaches a plugin's row, so `load-error: off` leaves each plugin's
169
+ own `load-error` alone. The plugin loader's rows are
170
+ `plugin_loader.load-error` and `plugin_loader.runtime-error`, which also
171
+ answer to the bare `load-error` / `runtime-error`. `rigor check` warns
172
+ about a key it can tell reaches no row.
173
+
165
174
  YAML reserves the bareword `off` as a boolean. If an override
166
175
  that names it seems not to apply, quote it — `"off"` — and the
167
176
  same for `on`.
@@ -252,8 +261,14 @@ named rules on that line:
252
261
  config.merge(extra) # rigor:disable call.undefined-method
253
262
  ```
254
263
 
255
- It accepts qualified IDs, family wildcards (`call`), a
256
- comma- or space-separated list, or `all`. The comment must sit
264
+ It accepts qualified IDs, family wildcards (`call`, `flow`, `def`, `assert`,
265
+ `dump`, `global`, `suppression`, `static`, `effect`, `pre-eval`), a
266
+ comma- or space-separated list, or `all`. A plugin's diagnostic
267
+ is reached only by its qualified ID as printed in brackets
268
+ (`plugin.shoulda-matchers.unknown-column`) or by the plugin's
269
+ whole family (`plugin.shoulda-matchers`); its bare rule
270
+ (`unknown-column`) and `all` leave it alone, because several
271
+ plugins share bare rules. The comment must sit
257
272
  on the line the diagnostic points at; there is no
258
273
  `disable-block` form, so an expression spread over several
259
274
  lines needs the comment on each line that fires.
@@ -283,7 +298,8 @@ suppressible like any other rule.
283
298
 
284
299
  **In-source, whole file.** `# rigor:disable-file <rules>`
285
300
  anywhere in a file suppresses those rules for every line;
286
- `# rigor:disable-file all` silences the file. Convention is to
301
+ `# rigor:disable-file all` silences every built-in rule in the file
302
+ (a plugin's diagnostics need their qualified ID, as above). Convention is to
287
303
  put it near the top — typically on a generated file, a fixture,
288
304
  or a vendored snippet — but every comment in the file is
289
305
  scanned, so any placement works.
@@ -300,6 +316,43 @@ disable:
300
316
  - flow.dead-assignment
301
317
  ```
302
318
 
319
+ `disable: [all]` turns off every built-in check rule, including
320
+ `effect.annotations-unchecked`, which is reported at a signature
321
+ file.
322
+
323
+ A plugin's own diagnostics are turned off by their qualified ID —
324
+ `plugin.<id>.<rule>` for one rule, `plugin.<id>` for every rule
325
+ of that plugin:
326
+
327
+ ```yaml
328
+ disable:
329
+ - plugin.shoulda-matchers.unknown-column
330
+ - plugin.rails-i18n
331
+ ```
332
+
333
+ A bare rule (`unknown-column`, `load-error`) and `all` do not
334
+ reach them, so one plugin's suppression never hides another
335
+ plugin's finding of the same name.
336
+
337
+ The list does not reach the run-level rows reported at
338
+ `.rigor.yml` — a plugin that failed to load or raised in
339
+ `prepare` (`plugin_loader.*`), a `dependencies.source_inference:`
340
+ gem that did not resolve (`dynamic.dependency-source.*`), the
341
+ `rbs.coverage.*` notices, `plugin_trust.*`, `pool-degraded` —
342
+ under any spelling, `plugin_loader.load-error` included. Turn
343
+ those off with [`severity_overrides:`](#severity-profiles) set
344
+ to `off`, or record them in a [baseline](06-baseline.md).
345
+ In `severity_overrides:` the plugin loader's rows are
346
+ `plugin_loader.load-error` and `plugin_loader.runtime-error`, or the
347
+ bare `load-error` / `runtime-error` (the qualified key wins if both
348
+ are set); a bare key does not reach a
349
+ plugin's own row of the same name (`plugin.<id>.load-error`). `rigor check` warns when a
350
+ `disable:` entry names one of these rows, names no rule at all,
351
+ or names a plugin `plugins:` does not configure (see
352
+ [Config validation warnings](03-configuration.md#config-validation-warnings)).
353
+ A path or parse error, which has no rule ID, is always
354
+ reported.
355
+
303
356
  For a *known backlog* you want to keep visible but not fail
304
357
  on, prefer a [baseline](06-baseline.md) over a blanket
305
358
  `disable:` — `disable:` also hides any new occurrences.
@@ -2,8 +2,8 @@
2
2
 
3
3
  Rigor's analysis is invisible by default — it only speaks up to
4
4
  report a diagnostic. When you want to *see* what type the
5
- engine assigns an expression, there are four tools: two source
6
- helpers and two CLI commands.
5
+ engine assigns an expression, there are six tools: two source
6
+ helpers and four CLI commands.
7
7
 
8
8
  ## `dump_type` — print a type from source
9
9
 
@@ -73,7 +73,7 @@ a diagnostic another layer has already suppressed.
73
73
  | --- | --- |
74
74
  | `rigor baseline generate` | Write a fresh baseline from the current diagnostics. Refuses to clobber an existing file without `--force`. |
75
75
  | `rigor baseline regenerate` | Rewrite unconditionally — run it after fixing diagnostics so the file shrinks. |
76
- | `rigor baseline dump` | Print the baseline, filterable with `--rule` and `--file`. |
76
+ | `rigor baseline dump` | Print the baseline, filterable with `--rule` and `--file` (`--format=json` for tooling). |
77
77
  | `rigor baseline drift` | Show how buckets have moved — `--only=over` for buckets that grew, `reducible` for ones you have already improved past, `cleared` for empty ones. |
78
78
  | `rigor baseline prune` | Drop buckets that match nothing any more. `--dry-run` previews. |
79
79
 
@@ -5,7 +5,8 @@ application DSL that plain inference cannot see — Rails route
5
5
  helpers, RSpec `let` bindings, dry-rb struct attributes, and
6
6
  so on. This page is about *activating* plugins. Writing one is
7
7
  covered by [`examples/`](../../examples/README.md) and the
8
- [`rigor-plugin-author` skill](08-skills.md).
8
+ [`rigor-plugin-author` skill](08-skills.md); reviewing and modernising
9
+ an existing plugin is [`rigor-plugin-review`](08-skills.md).
9
10
 
10
11
  ## Activating a plugin
11
12
 
@@ -95,7 +96,7 @@ with the `plugins_isolation:` configuration key or the
95
96
  | --- | --- |
96
97
  | `process` (default) | Run the call in a forked, crash-contained worker, so the target library's monkey-patches and any crash never contaminate Rigor. Falls back to `none` where `fork` is unavailable (Windows / JRuby). |
97
98
  | `none` | Load the library into Rigor's own process and call it directly. |
98
- | `ruby_box` | Run inside an experimental `Ruby::Box` sandbox. This needs the `RUBY_BOX=1` start flag, so the `rigor` launcher re-execs itself with it set when you select this strategy. It also needs a Ruby that fixes [Ruby Bug #22260](https://bugs.ruby-lang.org/issues/22260), which no release up to 4.0.7 does. The launcher checks for the fix first, and without it prints a warning and uses the configured strategy instead. **Environment variable only** — the configuration file is read long after Ruby has booted, so `plugins_isolation: ruby_box` is reported as a configuration error instead. |
99
+ | `ruby_box` | Run inside an experimental `Ruby::Box` sandbox. This needs the `RUBY_BOX=1` start flag, so the `rigor` launcher re-execs itself with it set when you select this strategy. It also needs a Ruby that fixes [Ruby Bug #22260](https://bugs.ruby-lang.org/issues/22260), which no release up to 4.0.7 does. The launcher checks for the fix first, and without it prints a warning and uses the configured strategy instead. The same check runs when `RUBY_BOX=1` is already in your environment: without the fix, the launcher prints a notice and restarts itself without `RUBY_BOX`, so the run completes but plugins are not boxed. The check result is cached per Ruby build under `$XDG_CACHE_HOME/rigor` (or `~/.cache/rigor`) and never expires, so if you rebuild a Ruby in place with the fix, delete `$XDG_CACHE_HOME/rigor/ruby-box-probe` (or `~/.cache/rigor/ruby-box-probe`). A check that times out is not cached, so it costs up to ten seconds on every run. **Environment variable only** — the configuration file is read long after Ruby has booted, so `plugins_isolation: ruby_box` is reported as a configuration error instead. |
99
100
 
100
101
  The environment variable wins over `plugins_isolation:`, so you can
101
102
  override a project's committed choice for one invocation. The legacy
@@ -87,6 +87,10 @@ is not repeated here.)
87
87
  - **`rigor-monkeypatch-resolve`** — resolves an `undefined-method`
88
88
  cluster that is really your project's own monkey-patches by wiring the
89
89
  defining files into `pre_eval:`.
90
+ - **`rigor-unused-adjudicate`** — adjudicates a `rigor unused` report
91
+ safely before proposing dead-code removals, checking framework roots
92
+ and false-positive shapes. See
93
+ [Removing dead code with `rigor unused`](18-removing-dead-code.md).
90
94
  - **`rigor-type-oracle`** — sources every type an agent writes from
91
95
  Rigor (`type-of` / `annotate` / `sig-gen`) instead of from reading the
92
96
  code, and reports the gaps rather than filling them. It is triggered by
@@ -119,6 +123,9 @@ is not repeated here.)
119
123
  repository to teach Rigor an application DSL or metaprogramming
120
124
  pattern it cannot infer. Reach for it when no bundled plugin covers a
121
125
  framework or DSL your project depends on.
126
+ - **`rigor-plugin-review`** — audits an existing Rigor plugin against
127
+ the current authoring contract and produces a prioritized upgrade
128
+ path. The review/upgrade counterpart to `rigor-plugin-author`.
122
129
 
123
130
  ## Discovering skills from the CLI
124
131