rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a09068aa2810a8d01758e3ce78f52dc6020f0d197bb83a561684b96969e5eddf
4
- data.tar.gz: 0105753f063477bea3a85ba3dea3fe660fbe58d846d9388867eafd3b6fed1055
3
+ metadata.gz: 608dc6b03dfc9267330c24c6493f3f4ee2d025ac5ce01a044202828fc282e258
4
+ data.tar.gz: 395692680ea16ec6dc42d4187fc57c10ce00731eea4cd1d03176fcc2daa07c14
5
5
  SHA512:
6
- metadata.gz: fb111fc503dbb60810cbcdf4bfb0a717f694f771fe16bf41007ef1b9eb2728014aa19d112183e79652e4a1ae03c09d6e227ad13ebc063a9804b7a6ba916ae71b
7
- data.tar.gz: 4c3d925ef588248fbd84529262632bcfa5e081eedd340c164f0de89b3a0f83abd5cdaad37be922a90571068966ec204f77e075993994db8b106abbaa8d135cd8
6
+ metadata.gz: c7145aafeaf9fd22fe3759970c9b4eaafd8720796048641a387ec7a02d4e579689fde37540db686da9904b7d8419a6338b9db7caa38997d7184960c346253f5a
7
+ data.tar.gz: bff814da943de361c7c1e5bee99fd0e4847d16bb8a9bd3e36609e69246e8eb0e141c6c6e87b425720d709e2277195d9a14100a3018fef75670361fe2acd2160b
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.4`** (2026-10-16) — on the
235
+ Current release: **`v0.4.6`** (2026-10-23) — 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
@@ -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,8 +532,23 @@ 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
531
- computed `Float` are proposed as they are. An `untyped` that
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. A literal inside a tuple, a
542
+ record or a type argument is widened the same way while the
543
+ shape stays (`-> [1, "a"]` becomes `-> [Integer, String]`,
544
+ `-> { a: 1 }` becomes `-> { a: Integer }`); of the generic
545
+ types, only the arguments of `Array`, `Hash`, `Set`, `Range`
546
+ and `Enumerator` are widened. `nil` inside a union or a shape
547
+ and a computed `Float` are proposed as they are. When the
548
+ method also carries an inline `# @rbs` annotation for its
549
+ parameters only, a return holding a literal that would be
550
+ widened, nested or not, is not proposed at all and the
551
+ `untyped` stays. An `untyped` that
532
552
  only the inline reader synthesizes for a `def` you did not
533
553
  annotate keeps the literal, as does a method no `.rbs`
534
554
  declares — the literal is still the strictest thing the body
@@ -590,9 +610,16 @@ list is the current state):
590
610
  `attr_writer` / `attr_accessor` ivar reads untyped.
591
611
  The accessor, and any other method returning that
592
612
  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
613
+ `@logger = Logger.new`. `--params=observed` types an
614
+ `attr_reader` from the constructor parameter its ivar
615
+ is assigned from (`Box.new(0)` gives
616
+ `def count: () -> 0`) only when that assignment is the
617
+ ivar's only write in the class's body in that file: an
618
+ attr writer, a hand-written setter, or a write in any
619
+ other instance method leaves both reader and writer
620
+ skipped. A writer declared in another file, in a
621
+ superclass or module, or through `class_eval` from
622
+ outside is not seen. The
596
623
  generator emits the long-form `def name: () -> T`
597
624
  spelling so the writer's merge path applies unchanged;
598
625
  existing short-form `attr_reader name: T` declarations
@@ -616,9 +643,28 @@ two are wired today, one is reserved.
616
643
  | Policy | Behaviour |
617
644
  | --- | --- |
618
645
  | `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. |
646
+ | `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
647
  | `observed-strict` | Reserved. Will additionally widen to capability roles (`_ToStr`, `_ToS`, …) once the role catalog ships. Currently rejected with a usage error. |
621
648
 
649
+ Under `--params=observed`, the `--print` / `--diff` JSON payload gains
650
+ a top-level `observation` key next to `candidates`, so a consumer that
651
+ never reads stderr (the MCP `rigor_sig_gen` tool included) can tell
652
+ "no test root was observed" from "the tests pass no typed arguments":
653
+
654
+ ```json
655
+ { "candidates": [ … ],
656
+ "observation": { "paths": ["/app/spec"], "missing": ["/app/tests"] } }
657
+ ```
658
+
659
+ `paths` are the roots the collector reads (a directory or a `.rb`
660
+ file); `missing` are the other declared roots: ones that do not exist,
661
+ or exist but are not read (`README.md`). An identical repeat is listed once. `paths: []` with `missing: []` means
662
+ there was no test root at all. Paths are spelled as the run resolved
663
+ them: `--observe=PATH` as typed, `test_paths:` as absolute paths, resolved against the
664
+ config file's directory, auto-detected `spec/` / `test/` absolute. The key is
665
+ absent under `--params=untyped`, and absent from the `--write` and
666
+ `--check` JSON payloads, which have their own shapes.
667
+
622
668
  The default deliberately favours `untyped` because of
623
669
  [ADR-5](../adr/5-robustness-principle.md)'s clause 2: a
624
670
  method's parameter contract should be the **most permissive**
@@ -704,7 +750,7 @@ output it got before.
704
750
  ## RSpec-aware observations
705
751
 
706
752
  When the observed test roots hold an RSpec suite, the
707
- generator recognises three RSpec-shaped binding patterns
753
+ generator recognises four RSpec-shaped binding patterns
708
754
  and uses them to type receivers that would otherwise
709
755
  degrade to `Dynamic[top]`:
710
756
 
@@ -712,10 +758,12 @@ degrade to `Dynamic[top]`:
712
758
  RSpec.describe Calc do
713
759
  subject { Calc.new } # binds :subject → Nominal[Calc]
714
760
  let(:other) { Calc.new } # binds :other → Nominal[Calc]
761
+ before { @calc = Calc.new } # binds @calc → Nominal[Calc]
715
762
 
716
763
  it "..." do
717
764
  subject.greet("Alice") # observed: Calc#greet receives String
718
765
  other.greet("Bob") # observed: same
766
+ @calc.greet("Carol") # observed: same
719
767
  described_class.new.add(1, 2) # observed: Calc#add receives Integer, Integer
720
768
  end
721
769
  end
@@ -724,17 +772,36 @@ end
724
772
  The recogniser handles `RSpec.describe Foo`, bare
725
773
  `describe Foo` (no `RSpec.` receiver), `subject { … }`,
726
774
  `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.
775
+ an ivar assigned by a top-level statement of a `before`
776
+ block, and `described_class.new(...)`. Same-name `subject` /
777
+ `let` bindings across nested scopes are last-wins; the
778
+ recogniser does not re-implement RSpec's full scope rules —
779
+ the typical one-spec-file shape is the target. A `before`
780
+ ivar is scoped: it types reads only inside the `describe` /
781
+ `context` block that holds the `before`, through nested
782
+ `describe` / `context` / `it` / hook blocks but not through
783
+ any other block (`Class.new { … }`, `instance_eval { … }`,
784
+ `each { … }`), and a read that two
785
+ enclosing `before`s assign different classes is not
786
+ observed.
731
787
 
732
788
  A Minitest suite under `test/` is observed too, with no
733
789
  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)).
790
+ class counts. A receiver assigned in `setup` reads as
791
+ `Foo | nil` inside a `test_*` method, since `setup` is not
792
+ `initialize`; a receiver of that shape — one class or class
793
+ object plus `nil` — is observed as that class when `nil`
794
+ does not answer the method, because such a call cannot
795
+ have been made on `nil`. A call of a method `nil` does
796
+ answer (`==`, `respond_to?`, `is_a?`) is not observed, as
797
+ it may have run on `nil`; with `&.` it is, since `&.` skips
798
+ `nil`. Which methods `nil` answers comes from the loaded
799
+ RBS for `NilClass`, `Object` and `Kernel`, so a method a
800
+ library adds to `nil` (ActiveSupport's `try`, `as_json`)
801
+ counts only when that library's RBS is loaded.
802
+ `@foo&.bar(1)` counts the same way. A receiver
803
+ that could be one of two classes (`Foo | Bar | nil`), or
804
+ `untyped | nil`, is still not observed.
738
805
 
739
806
  The recogniser is part of the generator itself; you do not
740
807
  need to install `rigor-rspec` to benefit from it. If you
@@ -765,16 +832,31 @@ by side without coordination.
765
832
  `def f`, a plain proposal over a `def self?.f`, or a
766
833
  method declared in a later opening is skipped. A
767
834
  `def f: ... | ...` overload is never replaced and never
768
- counts as the file that declares `f`. An `alias` in
769
- `sig/` is not yet recognised as declaring its name.
835
+ counts as the file that declares `f`. An `alias new old`
836
+ (or `alias self.new self.old`) declares `new` under its own
837
+ kind and is never replaced, so a proposal for that name is
838
+ skipped.
770
839
  - **Will not** touch files outside the configured signature
771
840
  tree.
841
+ - **Will** name every proposal it skipped, one
842
+ ` skipped Class#method (reason): <proposed RBS>` line under
843
+ its target (`Class.method` for a singleton), with the same
844
+ rows under `skipped` in `--format=json`. The reason is
845
+ `user-authored` (the target declares the member and the
846
+ writer does not replace it) or `outside-sig-root` (the
847
+ member routes to a file in a signature path other than the
848
+ first, which `--write` does not modify, whether the member
849
+ is declared there or is new to a class that lives there).
850
+ A target left unchanged by skips alone prints
851
+ `unchanged <file>`. The exit status stays `0`: a skip is
852
+ not a refusal, and `--check` still reports `sig/` as up to
853
+ date.
772
854
  - **Will not** replace an existing method declaration
773
855
  unless `--overwrite` is set AND the candidate is a
774
856
  `tighter-return`, and then only its return changes: the
775
857
  rest of the declaration is kept. Without `--overwrite`,
776
858
  existing declarations are user-authored and the new method
777
- is silently skipped.
859
+ is skipped.
778
860
  - **Will not** change an existing method declaration that
779
861
  disagrees with the method's inline declaration unless
780
862
  `--overwrite` is set; it reports the method as `REFUSED`
@@ -28,15 +28,25 @@ rigor check [paths...]
28
28
  `paths` are files or directories; when omitted, Rigor checks
29
29
  the `paths:` list from the configuration file.
30
30
 
31
+ Only the files you name are reported on, but Rigor reads the
32
+ declarations in the configured `paths:` too, so a file resolves a
33
+ class or constant that another file declares. A file outside
34
+ `paths:` also reads the files it loads with a literal
35
+ `require_relative "..."`, and the files those load in turn. Their
36
+ classes, modules and constants resolve and their methods count as
37
+ defined, but Rigor does not infer what those methods return or
38
+ check their arity, since another file may redefine them.
39
+
31
40
  | Option | Effect |
32
41
  | --- | --- |
33
- | `--config=PATH` | Use a specific config file instead of auto-discovery. |
42
+ | `--config=PATH` | Use a specific config file instead of auto-discovery. A path that does not exist is an error (exit 64), not a run with the defaults. |
34
43
  | `--format=FORMAT` | Output format. Default `text`. Also `json` (structured stream), plus the CI-native renderings `sarif`, `github`, `gitlab`, `checkstyle`, `junit`, and `teamcity` — see [Running Rigor in CI](11-ci.md). |
35
44
  | `--no-ci-detect` | Disable CI auto-detection — by default `text` output also emits the running CI's native annotations / hint (`RIGOR_CI_DETECT=0` does the same). See [Running Rigor in CI § auto-detection](11-ci.md). |
36
45
  | `--explain` | Surface fail-soft fallback events as `info` diagnostics. |
37
46
  | `--no-cache` | Skip the persistent cache for this run. With `--incremental` it is a cold full analysis: the incremental snapshot and the run-result cache are neither read nor written, so the next plain `--incremental` run still uses the snapshot it had before. |
38
47
  | `--incremental` | Re-analyse only the files changed since the last run plus the files that depend on them, serving the rest from a cross-process disk snapshot (ADR-46). Diagnostics are identical to a full run; a config / gem / version change (or a file added or removed) transparently forces a full re-analysis. A run with nothing changed is answered from the cache without starting the analyzer. See [Caching](12-caching.md). |
39
- | `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics agree as a set and in the same order, then exit (0 on match, 1 on mismatch: the differing diagnostics for a set mismatch, or the first index where the order diverges for a same-set, different-order mismatch). Used in CI to guarantee `--incremental` never serves a stale result. |
48
+ | `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics agree as a set and in the same order, then exit (0 on match, 1 on mismatch: the differing diagnostics for a set mismatch, or the first index where the order diverges for a same-set, different-order mismatch). It re-analyses half of an unchanged tree and serves the other half from cache, so it checks the cache-merge path but not that an edit re-checks every file it affects; `--verify-incremental-edits` adds that. |
49
+ | `--verify-incremental-edits=N` | With `--verify-incremental` (and implying it): after the check above, replay two edits on each of `N` files spread over the analysed list, through an editor buffer, never on disk: the file emptied, then the file reduced to its `class` / `module` declarations with every body dropped (skipped when that is the file itself, or holds no declaration). After each edit, the warm recheck of the baseline must equal a full analysis of the edited tree, or the run fails naming the edit and the file. Each replay costs a recheck and a full analysis. A replay the recheck declines (it would have been a full analysis) is counted in the OK line, and the run fails if every replay declined. Only these two removals are replayed, never an addition. Default 0. |
40
50
  | `--clear-cache` | Delete the cache directory before running. |
41
51
  | `--cache-stats` | Print the on-disk cache inventory when finished — on stdout under `--format text`, on stderr under every other format, so machine-readable output stays a parseable document. `--clear-cache`'s and `--verify-incremental`'s notes follow the same rule. |
42
52
  | `--[no-]stats` | Print a run summary (files, classes, memory, wall time) to stderr. Default on. |
@@ -60,7 +70,9 @@ that want the stricter reading.
60
70
  ### Editor mode scope
61
71
 
62
72
  `--tmp-file` / `--instead-of` on their own analyse **only** the
63
- buffer's file: fast, and blind to what the unsaved edit does to the
73
+ buffer's file. It still reads the rest of the project's declarations
74
+ (the classes and methods other files define), as `rigor check FILE`
75
+ does, but it reports nothing about what the unsaved edit does to the
64
76
  rest of the project.
65
77
 
66
78
  Adding `--incremental` analyses the **whole project with the buffer
@@ -138,6 +150,19 @@ truncation explicit. `--trace` records fail-soft fallbacks,
138
150
  after the rows of a line table in text output. The editor-mode
139
151
  `--tmp-file` / `--instead-of` pair is accepted as on `check`.
140
152
 
153
+ A class or constant that another project file declares reads as
154
+ `rigor check` types it (`singleton(User)`) when a bare
155
+ `rigor check --incremental` has left a snapshot for the project:
156
+ `type-of` folds the cross-file declarations from that snapshot's
157
+ per-file bundles, re-reading any file whose bytes changed since,
158
+ and never writes it. With no reusable snapshot (none yet, or one a
159
+ change made unusable, for example a changed configuration, lockfile, `sig/`, `pre_eval:`
160
+ file or required feature, an engine upgrade, or a snapshot written
161
+ for other roots) the probe types the one file on its own, so such a name reads
162
+ `Dynamic[top]`, and it prints nothing about it. Only those
163
+ declarations are shared: the types `check` infers for an unannotated
164
+ parameter are not.
165
+
141
166
  A template a plugin compiles into Ruby (an ERB view under
142
167
  `rigor-actionpack`) is probed the way `rigor check` analyses
143
168
  it: the compiled Ruby is typed under the view's declared
@@ -162,7 +187,9 @@ and is probed as plain Ruby, parse error included.
162
187
 
163
188
  The four probe commands — `type-of`, `type-scan`, `trace` and
164
189
  `annotate` — build their environment fresh on every invocation
165
- and never read or write the persistent cache. That is why none
190
+ and never read or write the persistent cache (apart from the
191
+ read-only snapshot lookup `type-of` makes for the project's
192
+ declarations, above). That is why none
166
193
  of them takes `--no-cache`: the flag would have nothing to skip.
167
194
  A probe therefore types against the environment `rigor check
168
195
  --no-cache` analyses with. The environment a cached (default)
@@ -224,7 +251,7 @@ rigor type-scan PATH...
224
251
  `--show-recognized` includes fully-covered classes,
225
252
  `--threshold=RATIO` makes the command exit non-zero when the
226
253
  unrecognized-node ratio exceeds `RATIO`, and `--format=text|json`
227
- selects the output format.
254
+ selects the output format. `--config=PATH` overrides config discovery.
228
255
 
229
256
  ## `rigor effects`
230
257
 
@@ -354,6 +381,8 @@ accept `--config=PATH`, `--format=text|json` and `--full`;
354
381
  configured one — `--baseline <(git show
355
382
  origin/main:.rigor-effects.yml)` in a bot),
356
383
  `--strict-tolerated` and `--no-tolerated-effects`.
384
+ `explain` additionally accepts `--symbol=KEY` to explain one
385
+ specific unit instead of the changed ones.
357
386
 
358
387
  The file records two tables. `methods:` holds each method's
359
388
  **direct** summary — what its own body does, block literals
@@ -476,12 +505,13 @@ Print the catalogue entry for a diagnostic rule, or list every
476
505
  rule when called with no argument.
477
506
 
478
507
  ```sh
479
- rigor explain [rule]
508
+ rigor explain [options] [rule]
480
509
  ```
481
510
 
482
511
  `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.
512
+ or a family wildcard (`call`, `flow`, `def`, `assert`, `dump`, `global`,
513
+ `suppression`, `static`, `effect`).
514
+ `--format=text|json` selects the output format. Exit `64` for an unknown rule.
485
515
 
486
516
  It also answers the `sig.skipped.*` identifiers `rigor sig-gen`
487
517
  prints when it declines to write a signature — `rigor explain
@@ -495,11 +525,13 @@ Compare the current diagnostics against a saved baseline JSON
495
525
  and report only what is new.
496
526
 
497
527
  ```sh
498
- rigor diff <baseline.json> [paths...]
528
+ rigor diff [options] <baseline.json> [paths...]
499
529
  ```
500
530
 
501
531
  `--current=PATH` compares against a saved diagnostics JSON
502
- instead of running a fresh check. Exit `1` when new
532
+ instead of running a fresh check. `--format=text|json` selects the
533
+ output format (default `text`), and `--config=PATH` passes a custom
534
+ configuration to the implicit check. Exit `1` when new
503
535
  diagnostics appear.
504
536
 
505
537
  ## `rigor sig-gen`
@@ -527,12 +559,22 @@ rigor sig-gen [paths]
527
559
  | `--no-cache` | Do not read or write the analysis cache. Only effect collection uses it. |
528
560
  | `--format=text\|json` | Output format. |
529
561
 
562
+ A `paths` argument that does not exist, or is neither a directory nor a `.rb` file, is reported the way
563
+ `rigor check` reports it: on stderr as `path:1:1: error: …`, or under `diagnostics` in `--print` and `--diff`
564
+ JSON output. When no argument is readable this exits `1`, under every mode, `--write` and `--check` included.
565
+ The configured default `paths:` count too, so `paths: [lib]` with no `lib/` directory exits `1`. A `--config`
566
+ that names no file, is a directory, or is empty is a usage error and exits `64`.
567
+
530
568
  `--print`, `--diff`, `--write` and `--check` are mutually exclusive.
531
569
  `--check` fails exactly when `--write` would create, change or
532
570
  refuse a file, so a tighter return `--write` declines without
533
571
  `--overwrite` does not fail it; `--check --overwrite` counts one.
534
572
  See [handbook chapter 11](../handbook/11-sig-gen.md#keeping-sig-current-in-ci).
535
573
 
574
+ `--write` names each proposal it skipped (a member `sig/` already declares, an `alias` included, or one that
575
+ 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
576
+ unchanged. See [handbook chapter 11](../handbook/11-sig-gen.md#safety-what---write-will-and-will-not-do).
577
+
536
578
  A method declared inline with `# @rbs` / `#:` is written as that
537
579
  declaration, not as what its body infers; a parameter-only
538
580
  annotation keeps its parameters and takes the return from the body,
@@ -616,7 +658,8 @@ rigor baseline <generate|regenerate|dump|drift|prune> [options]
616
658
 
617
659
  `generate` and `regenerate` accept `--output=PATH` and
618
660
  `--match-mode=rule|message`; `dump`, `drift`, and `prune` accept
619
- `--baseline=PATH` to read a non-default baseline file.
661
+ `--baseline=PATH` to read a non-default baseline file. `dump`
662
+ also accepts `--format=text|json` for machine-readable output.
620
663
 
621
664
  ## `rigor triage`
622
665
 
@@ -630,7 +673,7 @@ rigor triage [paths]
630
673
 
631
674
  `--top=N` sets the hotspot count (default 10); `--hints-only`,
632
675
  `--selectors-only`, and `--no-hints` select which sections print.
633
- `triage` is advisory and always exits `0` — it never gates a build.
676
+ `triage` is advisory and exits `0` once it runs — it never gates a build; a usage error exits `64`.
634
677
 
635
678
  By default the distribution, selectors, and hotspots sections count
636
679
  only the **actionable** diagnostics (`error` + `warning`). `info`
@@ -936,6 +979,11 @@ loaded from (a `path:` line in text, a `"path"` key in JSON), so
936
979
  if a stale installed gem is shadowing a newer checkout's bundled
937
980
  plugin copy the mismatch is visible at a glance.
938
981
 
982
+ A plugin that declares `per_invocation_classes:` or
983
+ `per_invocation_non_action_macros:` ([ADR-125](../adr/125-per-invocation-classes.md))
984
+ also gets a line for each in text, and a key in JSON (an empty
985
+ array when it declares none).
986
+
939
987
  ```sh
940
988
  rigor plugins [--format=text|json] [--strict] [--capabilities] [--config=PATH]
941
989
  ```
@@ -1163,8 +1211,17 @@ Runs a scoped analysis and audits:
1163
1211
  - **Plugin load errors** — whether every configured plugin loaded.
1164
1212
  - **Baseline drift** — whether the current diagnostics have drifted
1165
1213
  from the saved baseline.
1166
- - **Rails plugin gap** — whether `Gemfile.lock` contains Rails gems
1167
- but no Rails plugin is enabled.
1214
+ - **Plugin gap** — whether the project's direct dependencies (the
1215
+ `DEPENDENCIES` section of `Gemfile.lock`, plus the Rails components
1216
+ a direct `rails` stands for) include gems a bundled plugin models —
1217
+ Rails, Devise, Pundit, Sidekiq, ... — while `plugins:` does not list
1218
+ that plugin ([ADR-96](../adr/96-plugin-target-gems.md)). An entry
1219
+ with `enabled: false` counts as listed. Each missing plugin is a
1220
+ warning, since leaving one out can be deliberate; it is a failure
1221
+ only when `plugins:` lists none of the bundled plugins that model
1222
+ gems (those declaring `target_gems:`).
1223
+ Skipped when no `.rigor.yml` or `.rigor.dist.yml` is discovered in
1224
+ the current directory.
1168
1225
  - **Gemfile install** — whether Rigor itself resolves as one of the
1169
1226
  project's dependencies, which
1170
1227
  [Installing Rigor](01-installation.md) tells you not to do. Only a
@@ -1177,6 +1234,16 @@ Runs a scoped analysis and audits:
1177
1234
  versioned together, so a copy from another installation can run
1178
1235
  the engine with a mismatched plugin. A warning, not a failure;
1179
1236
  third-party plugins from your own bundle are never flagged.
1237
+ - **Bundle layout** — whether Rigor can locate the bundle a
1238
+ `Gemfile.lock` in the current directory was installed into; when it
1239
+ cannot (typically gems in the active Ruby's default gem home), no
1240
+ gem-shipped `sig/` is discovered. Rigor finds a bundle only where the
1241
+ project records it. A `bundler.bundle_path:` in `.rigor.yml` is used
1242
+ alone and must exist. Without one — and unless
1243
+ `bundler.auto_detect: false` — Rigor reads `.bundle/config`'s
1244
+ `BUNDLE_PATH`, then an in-tree `vendor/bundle`, then the global
1245
+ Bundler config. A warning, not a failure: the default layout is
1246
+ correct, and most gems ship no `sig/`.
1180
1247
 
1181
1248
  Text output prints `[PASS]`, `[FAIL]`, or `[WARN]` per check plus a
1182
1249
  routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
@@ -1237,9 +1304,11 @@ diagnostics about Rigor's own inference cutoffs and memory — see
1237
1304
  | `0` | Success — no diagnostic at or above the exit threshold (`error` by default; `rigor check --fail-on=SEVERITY` lowers it to `warning` or `info`). |
1238
1305
  | `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`). |
1239
1306
  | `64` | Usage error — unknown command, bad flag, malformed argument, or a value in `.rigor.yml` the loader cannot proceed on. |
1307
+ | `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. |
1240
1308
 
1241
- `rigor triage` is the exception: it is advisory and always
1242
- exits `0`.
1309
+ `rigor triage` is the exception: it is advisory and exits `0`
1310
+ whenever it runs to completion. A usage error, such as a
1311
+ `--config` that names no file, still exits `64`.
1243
1312
 
1244
1313
  A configuration mistake prints one `rigor:` line naming the key
1245
1314
  and nothing else — no backtrace, on every command:
@@ -67,7 +67,7 @@ cache:
67
67
  | `target_ruby` | String | `"4.0"` | The lowest Ruby version *your* project runs on — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. Every value selects the syntax Rigor parses. A version you set explicitly also turns on the Ruby-deprecation rules for that version ([`call.deprecated-ruby2-keywords`](04-diagnostics.md#rule-call-deprecated-ruby2-keywords) from `"4.1"`); the default and `"latest"` never do, which is why `rigor init` writes the key commented out. |
68
68
  | `paths` | Array | `["lib"]` | Directories or files to analyse. |
69
69
  | `exclude` | Array | `[]` | Glob patterns to skip. A relative pattern is matched against the file's path relative to the directory of the config file in use (the working directory when no config file is used), so it excludes the same files whether the path came from `paths:` or from a command-line argument, and it never matches on a directory above that base (a file outside it is seen as `../…`, so `../shared/lib/gen/**` reaches a sibling directory that `paths:` names). A leading `./` is ignored, and a pattern starting with `.` (such as `.*`) also matches files outside the base, which appear as `../…`. A symlink to a file is matched only under its own name: the realpath pass resolves directories, not files. `*` and `**` also cross `/`, so `lib/*.rb` matches nested files too. An absolute pattern is matched against the absolute path. Patterns from an `includes:` file resolve against the top-level config's directory, not the included file's. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
70
- | `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
70
+ | `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist in the config file's directory, or in the working directory when the config file's directory has neither or no config file is used; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
71
71
  | `includes` | Array | `[]` | Other config files to layer underneath this one. |
72
72
  | `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
73
73
  | `parameter_inference` | Boolean | `false` | Opt-in call-site parameter type inference on the `check` walk ([ADR-67](../adr/67-parameter-type-inference.md) WD6). When `true`, an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved call-site argument types, sharpening downstream ivar reads, folds, and protection coverage. Precision-additive only — the negative rules never fire against an inferred parameter. Cannot be combined with `--incremental`. |
@@ -106,6 +106,7 @@ rigor: disable: "call.undefined-methdo" is not a recognized rule id; the suppres
106
106
  rigor: disable: "plugin_loader.load-error" names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline
107
107
  rigor: disable: "plugin.shoulda-matchers.unknown-column" names no plugin `plugins:` configures; the suppression has no effect
108
108
  rigor: severity_overrides: "flow.bogus" is not a recognized rule id; the override has no effect
109
+ rigor: severity_overrides: "undefined-method" is a legacy alias of `call.undefined-method`; the override has no effect, so write `call.undefined-method`
109
110
  rigor: bundler.lockfile: "./missing/Gemfile.lock" does not exist
110
111
  ```
111
112
 
@@ -128,18 +129,20 @@ These are warnings, not errors — partial or optional bundles and
128
129
  forward-looking config are valid setups. The audit only fires on explicit,
129
130
  working-setup-safe signals: an unset default (auto-detected `<root>/sig`,
130
131
  auto-detected bundle) is never warned about, and a
131
- `severity_overrides:` key under a non-built-in family (`rspec.…`,
132
- `rbs_extended.…`) is left alone, since its rule id cannot be enumerated
133
- statically and may resolve at run time. `disable:` is checked more
134
- closely, because what it can reach is known: a plugin's rule counts
132
+ `rbs_extended.…`, `rbs.…` or `dynamic.…` key in `severity_overrides:` or
133
+ `disable:` is left alone, since the engine emits those ids outside the
134
+ catalogue. `disable:` and `severity_overrides:` are both checked
135
+ closely, because what they can reach is known: a plugin's rule counts
135
136
  only in its qualified `plugin.<id>.<rule>` form, so a bare plugin rule
136
137
  (`rspec.let-binding`) is warned about. A qualified token is checked
137
138
  against `plugins:` only when every entry's plugin ID is known without
138
139
  loading it — an explicit `id:`, or a bundled plugin's gem name minus
139
140
  `rigor-`; one third-party gem listed without `id:` turns that check
140
- off. A token under `rbs_extended.…`, `rbs.…` or `dynamic.…` is left
141
- alone, other than the reserved run-level rows (`rbs.coverage.…`,
142
- `dynamic.dependency-source.…`), which are flagged. The same findings appear in the
141
+ off. A `disable:` token under `rbs_extended.…`, `rbs.…` or `dynamic.…`
142
+ is left alone, other than the reserved run-level rows (`rbs.coverage.…`,
143
+ `dynamic.dependency-source.…`), which are flagged; in `severity_overrides:`
144
+ those rows are the intended keys and are never flagged, while a bare plugin
145
+ rule, `all`, or a `plugin.`-qualified key for an unconfigured plugin is. The same findings appear in the
143
146
  `--format=json` payload under `config_warnings` (each tagged with a
144
147
  `kind`), so CI can assert on them.
145
148
 
@@ -149,7 +152,7 @@ alone, other than the reserved run-level rows (`rbs.coverage.…`,
149
152
  | --- | --- | --- | --- |
150
153
  | `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). |
151
154
  | `severity_profile` | String | `"balanced"` | `lenient`, `balanced`, or `strict` — see [Diagnostics](04-diagnostics.md). |
152
- | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. |
155
+ | `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`). |
153
156
  | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
154
157
  | `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). |
155
158
 
@@ -67,7 +67,7 @@ carries no `documentation_url`.
67
67
  | <a id="rule-flow-return-in-ensure"></a>`flow.return-in-ensure` | An explicit `return` inside an `ensure` clause — it overrides the method's in-flight return value and silently swallows any in-flight exception. A `return` in a nested `def`, lambda, or `define_method` block inside the `ensure` does not fire (it exits that inner frame). | high |
68
68
  | <a id="rule-flow-shadowed-rescue-clause"></a>`flow.shadowed-rescue-clause` | A `rescue` clause can never run because an earlier clause of the same chain already catches a superclass (or the same class) of every exception class it names. | high |
69
69
  | <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
70
- | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. | high |
70
+ | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
71
71
  | <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
72
72
  | <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
73
73
  | <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
@@ -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,7 +261,8 @@ 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
264
+ It accepts qualified IDs, family wildcards (`call`, `flow`, `def`, `assert`,
265
+ `dump`, `global`, `suppression`, `static`, `effect`, `pre-eval`), a
256
266
  comma- or space-separated list, or `all`. A plugin's diagnostic
257
267
  is reached only by its qualified ID as printed in brackets
258
268
  (`plugin.shoulda-matchers.unknown-column`) or by the plugin's
@@ -332,10 +342,11 @@ gem that did not resolve (`dynamic.dependency-source.*`), the
332
342
  under any spelling, `plugin_loader.load-error` included. Turn
333
343
  those off with [`severity_overrides:`](#severity-profiles) set
334
344
  to `off`, or record them in a [baseline](06-baseline.md).
335
- `severity_overrides:` matches the row's rule ID without the
336
- source prefix, so the plugin loader's rows are `load-error` and
337
- `runtime-error` there — and that key also re-stamps any plugin
338
- diagnostic whose bare rule is the same. `rigor check` warns when a
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
339
350
  `disable:` entry names one of these rows, names no rule at all,
340
351
  or names a plugin `plugins:` does not configure (see
341
352
  [Config validation warnings](03-configuration.md#config-validation-warnings)).
@@ -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
 
@@ -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