rigortype 0.4.4 → 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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +90 -17
  4. data/docs/manual/02-cli-reference.md +39 -9
  5. data/docs/manual/03-configuration.md +10 -8
  6. data/docs/manual/04-diagnostics.md +16 -5
  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 +8 -1
  12. data/docs/manual/18-removing-dead-code.md +18 -7
  13. data/docs/manual/plugins/rigor-rspec.md +101 -1
  14. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  15. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  16. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  18. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  19. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  20. data/lib/rigor/analysis/check_rules.rb +228 -15
  21. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  22. data/lib/rigor/analysis/incremental.rb +49 -0
  23. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  24. data/lib/rigor/analysis/incremental_session.rb +166 -24
  25. data/lib/rigor/analysis/path_expansion.rb +3 -1
  26. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  27. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  28. data/lib/rigor/analysis/rule_catalog.rb +17 -4
  29. data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
  30. data/lib/rigor/analysis/runner.rb +69 -65
  31. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  32. data/lib/rigor/analysis/worker_session.rb +12 -1
  33. data/lib/rigor/cache/descriptor.rb +40 -7
  34. data/lib/rigor/cache/engine_source.rb +3 -2
  35. data/lib/rigor/cache/incremental_snapshot.rb +26 -8
  36. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  37. data/lib/rigor/cache/store.rb +4 -3
  38. data/lib/rigor/cli/command.rb +3 -1
  39. data/lib/rigor/cli/docs_command.rb +3 -2
  40. data/lib/rigor/cli/doctor_command.rb +1 -1
  41. data/lib/rigor/cli/explain_command.rb +3 -2
  42. data/lib/rigor/cli/plugin_command.rb +2 -1
  43. data/lib/rigor/cli/plugins_command.rb +2 -1
  44. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  45. data/lib/rigor/cli/skill_command.rb +2 -1
  46. data/lib/rigor/cli/skill_describe.rb +3 -2
  47. data/lib/rigor/cli/unused_command.rb +10 -7
  48. data/lib/rigor/cli.rb +3 -2
  49. data/lib/rigor/config_audit.rb +60 -15
  50. data/lib/rigor/configuration/severity_profile.rb +20 -2
  51. data/lib/rigor/configuration.rb +29 -3
  52. data/lib/rigor/effects/signature_sources.rb +3 -1
  53. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  54. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  55. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  56. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  57. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  58. data/lib/rigor/environment/rbs_loader.rb +21 -1
  59. data/lib/rigor/inference/block_call_timing.rb +7 -1
  60. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  61. data/lib/rigor/inference/captured_locals.rb +44 -13
  62. data/lib/rigor/inference/clause_widening.rb +90 -0
  63. data/lib/rigor/inference/definer_resolution.rb +87 -20
  64. data/lib/rigor/inference/expression_typer.rb +35 -5
  65. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  66. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  68. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  69. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  70. data/lib/rigor/inference/module_function_state.rb +137 -42
  71. data/lib/rigor/inference/narrowing.rb +54 -10
  72. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  73. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  74. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  75. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  76. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  77. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  78. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  79. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  80. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  81. data/lib/rigor/inference/return_barrier.rb +16 -0
  82. data/lib/rigor/inference/scope_indexer.rb +675 -105
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
  84. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  85. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  86. data/lib/rigor/inference/statement_evaluator.rb +446 -139
  87. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  88. data/lib/rigor/lens/contract_reader.rb +45 -2
  89. data/lib/rigor/lens/document.rb +2 -1
  90. data/lib/rigor/lens/file_reader.rb +12 -2
  91. data/lib/rigor/lens/members.rb +16 -2
  92. data/lib/rigor/lens/run.rb +34 -7
  93. data/lib/rigor/mcp/server.rb +2 -2
  94. data/lib/rigor/plugin/base.rb +3 -2
  95. data/lib/rigor/plugin/io_boundary.rb +42 -6
  96. data/lib/rigor/plugin/isolation.rb +4 -3
  97. data/lib/rigor/protection/discovery_seed.rb +9 -36
  98. data/lib/rigor/protection/mutation_cache.rb +8 -2
  99. data/lib/rigor/rooted_glob.rb +27 -0
  100. data/lib/rigor/scope/discovery_index.rb +5 -0
  101. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  102. data/lib/rigor/scope/resolution_chain.rb +40 -5
  103. data/lib/rigor/scope.rb +45 -2
  104. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  105. data/lib/rigor/sig_gen/classification.rb +4 -1
  106. data/lib/rigor/sig_gen/generator.rb +167 -22
  107. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  108. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  109. data/lib/rigor/sig_gen/renderer.rb +13 -155
  110. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  111. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  112. data/lib/rigor/sig_gen/write_result.rb +3 -1
  113. data/lib/rigor/sig_gen/writer.rb +5 -3
  114. data/lib/rigor/signature_path_audit.rb +4 -2
  115. data/lib/rigor/type/constant.rb +10 -1
  116. data/lib/rigor/version.rb +1 -1
  117. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  118. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  119. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  120. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  121. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  122. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  123. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  124. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  125. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  126. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  127. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  128. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  129. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  130. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  131. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  132. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  133. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  134. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  135. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  136. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  137. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  138. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  139. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  140. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  141. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  142. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  143. data/sig/rigor/rooted_glob.rbs +7 -0
  144. data/sig/rigor/scope.rbs +4 -3
  145. data/skills/rigor-doctor/references/01-checks.md +23 -11
  146. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  147. metadata +13 -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: 4da9b991a97cd586806b416facb6dbf6c3a3b938c7c9433e330bc44c0757fdaf
4
+ data.tar.gz: 1167e8d91ccbc8740ac4094f67db82937bd3b374d2488a43e514f7922c7a98b1
5
5
  SHA512:
6
- metadata.gz: fb111fc503dbb60810cbcdf4bfb0a717f694f771fe16bf41007ef1b9eb2728014aa19d112183e79652e4a1ae03c09d6e227ad13ebc063a9804b7a6ba916ae71b
7
- data.tar.gz: 4c3d925ef588248fbd84529262632bcfa5e081eedd340c164f0de89b3a0f83abd5cdaad37be922a90571068966ec204f77e075993994db8b106abbaa8d135cd8
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.4`** (2026-10-16) — 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
@@ -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
@@ -765,16 +823,31 @@ by side without coordination.
765
823
  `def f`, a plain proposal over a `def self?.f`, or a
766
824
  method declared in a later opening is skipped. A
767
825
  `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.
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.
770
830
  - **Will not** touch files outside the configured signature
771
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.
772
845
  - **Will not** replace an existing method declaration
773
846
  unless `--overwrite` is set AND the candidate is a
774
847
  `tighter-return`, and then only its return changes: the
775
848
  rest of the declaration is kept. Without `--overwrite`,
776
849
  existing declarations are user-authored and the new method
777
- is silently skipped.
850
+ is skipped.
778
851
  - **Will not** change an existing method declaration that
779
852
  disagrees with the method's inline declaration unless
780
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
 
@@ -1163,8 +1173,17 @@ Runs a scoped analysis and audits:
1163
1173
  - **Plugin load errors** — whether every configured plugin loaded.
1164
1174
  - **Baseline drift** — whether the current diagnostics have drifted
1165
1175
  from the saved baseline.
1166
- - **Rails plugin gap** — whether `Gemfile.lock` contains Rails gems
1167
- 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.
1168
1187
  - **Gemfile install** — whether Rigor itself resolves as one of the
1169
1188
  project's dependencies, which
1170
1189
  [Installing Rigor](01-installation.md) tells you not to do. Only a
@@ -1177,6 +1196,16 @@ Runs a scoped analysis and audits:
1177
1196
  versioned together, so a copy from another installation can run
1178
1197
  the engine with a mismatched plugin. A warning, not a failure;
1179
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/`.
1180
1209
 
1181
1210
  Text output prints `[PASS]`, `[FAIL]`, or `[WARN]` per check plus a
1182
1211
  routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
@@ -1237,6 +1266,7 @@ diagnostics about Rigor's own inference cutoffs and memory — see
1237
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`). |
1238
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`). |
1239
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. |
1240
1270
 
1241
1271
  `rigor triage` is the exception: it is advisory and always
1242
1272
  exits `0`.
@@ -128,18 +128,20 @@ These are warnings, not errors — partial or optional bundles and
128
128
  forward-looking config are valid setups. The audit only fires on explicit,
129
129
  working-setup-safe signals: an unset default (auto-detected `<root>/sig`,
130
130
  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
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
135
  only in its qualified `plugin.<id>.<rule>` form, so a bare plugin rule
136
136
  (`rspec.let-binding`) is warned about. A qualified token is checked
137
137
  against `plugins:` only when every entry's plugin ID is known without
138
138
  loading it — an explicit `id:`, or a bundled plugin's gem name minus
139
139
  `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
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
143
145
  `--format=json` payload under `config_warnings` (each tagged with a
144
146
  `kind`), so CI can assert on them.
145
147
 
@@ -149,7 +151,7 @@ alone, other than the reserved run-level rows (`rbs.coverage.…`,
149
151
  | --- | --- | --- | --- |
150
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). |
151
153
  | `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 }`. |
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`). |
153
155
  | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
154
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). |
155
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,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
 
@@ -391,6 +391,13 @@ Generate RBS skeleton signatures inferred from Ruby source files.
391
391
  project's test roots: the configured `test_paths:`, or whichever of
392
392
  `spec/` and `test/` exist.
393
393
 
394
+ Under `params: "observed"` the response also carries a top-level
395
+ `observation` object, `{"paths": [...], "missing": [...]}`: the test
396
+ roots that were read and the declared ones that do not exist or that
397
+ the collector would not read (neither a directory nor a `.rb` file). Both
398
+ empty means no test root was found, so every parameter stays `untyped`.
399
+ The key is absent under the default `untyped` policy.
400
+
394
401
  **Returns:** JSON — the same as `rigor sig-gen --print --format json`.
395
402
 
396
403
  ```json
@@ -430,7 +437,7 @@ Look up the description of one or all Rigor diagnostic rules.
430
437
 
431
438
  Accepted values: a canonical rule ID (`call.undefined-method`), a
432
439
  legacy alias (`undefined-method`), or a family prefix (`call`, `flow`,
433
- `assert`, `dump`, `def`).
440
+ `def`, `assert`, `dump`, `global`, `suppression`, `static`, `effect`).
434
441
 
435
442
  **Returns:** JSON — an array of rule catalog entries.
436
443
 
@@ -6,7 +6,9 @@ into a finite list of specific questions. It does not answer them.
6
6
 
7
7
  The command reference — every flag, every output section — is in
8
8
  [CLI reference](02-cli-reference.md#rigor-unused). This chapter is the
9
- workflow around it.
9
+ workflow around it. For an AI coding agent, the bundled
10
+ [`rigor-unused-adjudicate`](08-skills.md) skill walks through this
11
+ adjudication workflow step by step.
10
12
 
11
13
  **Before you start** you need a working `rigor check` setup: a config
12
14
  file with `paths:`, and the plugins for your framework enabled. See
@@ -270,12 +272,21 @@ implicit, `self` or `self.class` inside a method (or a block in a class
270
272
  body, such as a lambda or `define_method`), the call may run on a
271
273
  subclass or on an includer, so a `Handler` declared in a descendant is
272
274
  kept reachable too. Descendants are the classes written `class D < Base`
273
- and the declarations whose body has a plain `include`, `prepend` or
274
- `extend` of the module; `K.include(Mix)`, `K.send(:include, Mix)`,
275
- `K.extend(Ext)` and `K = Class.new(Base)` are not followed yet. A call
276
- written directly in a class body is not widened. Inside
277
- `Other.instance_eval { }` (or `class_eval`, `module_eval` and their
278
- `_exec` forms) the receiver is `Other`. A name built into a
275
+ or `K = Class.new(Base)`, and the classes that `include`, `prepend` or
276
+ `extend` the module in their body or through a receiver Rigor can name:
277
+ `K.include(Mix)` in an initializer, its `send`, `public_send` and
278
+ `__send__` spellings, or `include Mix` inside `K.class_eval { }`. A call written directly in a
279
+ class body is not widened. Inside `Other.instance_eval { }` (or
280
+ `class_eval`, `module_eval` and their `_exec` forms) the receiver is
281
+ `Other`, and a method the block defines (`def`, `define_method`,
282
+ `define_singleton_method`) runs on `Other` or a descendant. When the
283
+ receiver cannot be named at all, the name is kept wherever it is
284
+ declared: an eval block on a local variable (`k.class_eval { }`), the
285
+ module's own lookups when such a receiver includes it, or a class that
286
+ includes a computed module (`K.include(mods.first)`). A class that gains
287
+ a module through a receiver Rigor cannot name (`[Foo].each { |k|
288
+ k.include(Consts) }`, an `include` in a `Class.new` block) is not
289
+ followed for its own lookups of that module's constants. A name built into a
279
290
  local variable first (`name = "V#{version}"; const_get(name)`) is read
280
291
  the same way. A
281
292
  class name appearing as a string in a `.yml`, `.json` or template file