rigortype 0.2.8 → 0.3.0

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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/03-narrowing.md +2 -1
  8. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  9. data/docs/handbook/06-classes.md +18 -9
  10. data/docs/handbook/08-understanding-errors.md +4 -3
  11. data/docs/handbook/11-sig-gen.md +11 -1
  12. data/docs/handbook/appendix-mypy.md +4 -3
  13. data/docs/install.md +14 -6
  14. data/docs/manual/01-installation.md +99 -4
  15. data/docs/manual/02-cli-reference.md +65 -19
  16. data/docs/manual/03-configuration.md +34 -2
  17. data/docs/manual/04-diagnostics.md +23 -0
  18. data/docs/manual/07-plugins.md +5 -5
  19. data/docs/manual/09-editor-integration.md +4 -4
  20. data/docs/manual/11-ci.md +51 -0
  21. data/docs/manual/12-caching.md +65 -0
  22. data/docs/manual/14-rails-quickstart.md +4 -2
  23. data/docs/manual/15-type-protection-coverage.md +21 -0
  24. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  25. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  26. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  27. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  28. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  29. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  30. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  31. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  32. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  33. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  34. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  35. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  36. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  37. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  38. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  39. data/lib/rigor/analysis/check_rules.rb +557 -95
  40. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  42. data/lib/rigor/analysis/incremental_session.rb +456 -51
  43. data/lib/rigor/analysis/path_expansion.rb +42 -0
  44. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  45. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  46. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  47. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  48. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  49. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  50. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  51. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  52. data/lib/rigor/analysis/runner.rb +343 -68
  53. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  54. data/lib/rigor/analysis/worker_session.rb +30 -2
  55. data/lib/rigor/bleeding_edge.rb +37 -8
  56. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  57. data/lib/rigor/cache/descriptor.rb +88 -28
  58. data/lib/rigor/cache/file_digest.rb +158 -0
  59. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  60. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  61. data/lib/rigor/cache/store.rb +207 -60
  62. data/lib/rigor/ci_detector.rb +90 -0
  63. data/lib/rigor/cli/annotate_command.rb +15 -9
  64. data/lib/rigor/cli/baseline_command.rb +4 -1
  65. data/lib/rigor/cli/check_command.rb +212 -27
  66. data/lib/rigor/cli/coverage_command.rb +56 -30
  67. data/lib/rigor/cli/diagnostic_formats.rb +4 -1
  68. data/lib/rigor/cli/docs_command.rb +0 -30
  69. data/lib/rigor/cli/doctor_command.rb +163 -23
  70. data/lib/rigor/cli/lsp_command.rb +5 -0
  71. data/lib/rigor/cli/mcp_command.rb +5 -0
  72. data/lib/rigor/cli/plugins_command.rb +36 -6
  73. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  74. data/lib/rigor/cli/probe_environment.rb +85 -0
  75. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  76. data/lib/rigor/cli/protection_report.rb +7 -1
  77. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  78. data/lib/rigor/cli/skill_command.rb +1 -32
  79. data/lib/rigor/cli/trace_command.rb +5 -6
  80. data/lib/rigor/cli/type_of_command.rb +9 -8
  81. data/lib/rigor/cli/type_scan_command.rb +8 -8
  82. data/lib/rigor/cli.rb +15 -4
  83. data/lib/rigor/config_audit.rb +48 -2
  84. data/lib/rigor/configuration/severity_profile.rb +31 -0
  85. data/lib/rigor/configuration.rb +156 -8
  86. data/lib/rigor/environment/default_libraries.rb +35 -0
  87. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  88. data/lib/rigor/environment/rbs_loader.rb +164 -5
  89. data/lib/rigor/environment.rb +71 -43
  90. data/lib/rigor/inference/budget_trace.rb +77 -2
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  92. data/lib/rigor/inference/def_handle.rb +23 -0
  93. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  94. data/lib/rigor/inference/def_return_typer.rb +2 -1
  95. data/lib/rigor/inference/expression_typer.rb +379 -113
  96. data/lib/rigor/inference/fork_map.rb +87 -0
  97. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  98. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  99. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  100. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  101. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  102. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  103. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  104. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  105. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  106. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  107. data/lib/rigor/inference/mutation_widening.rb +42 -8
  108. data/lib/rigor/inference/narrowing.rb +245 -17
  109. data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
  110. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  111. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  112. data/lib/rigor/inference/scope_indexer.rb +460 -49
  113. data/lib/rigor/inference/statement_evaluator.rb +111 -17
  114. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  115. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  116. data/lib/rigor/inference/void_origin.rb +25 -0
  117. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  118. data/lib/rigor/language_server/completion_provider.rb +2 -1
  119. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  120. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  121. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  122. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  123. data/lib/rigor/plugin/base.rb +9 -22
  124. data/lib/rigor/plugin/inflector.rb +12 -3
  125. data/lib/rigor/plugin/io_boundary.rb +5 -1
  126. data/lib/rigor/plugin/isolation.rb +81 -11
  127. data/lib/rigor/plugin/load_error.rb +10 -1
  128. data/lib/rigor/plugin/loader.rb +96 -14
  129. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  130. data/lib/rigor/plugin/registry.rb +32 -23
  131. data/lib/rigor/plugin.rb +26 -0
  132. data/lib/rigor/protection/mutator.rb +3 -2
  133. data/lib/rigor/reflection.rb +64 -0
  134. data/lib/rigor/runtime/jit.rb +128 -0
  135. data/lib/rigor/scope/discovery_index.rb +12 -2
  136. data/lib/rigor/scope.rb +122 -28
  137. data/lib/rigor/sig_gen/classification.rb +5 -1
  138. data/lib/rigor/sig_gen/generator.rb +38 -6
  139. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  140. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  141. data/lib/rigor/sig_gen/renderer.rb +10 -0
  142. data/lib/rigor/sig_gen/write_result.rb +9 -4
  143. data/lib/rigor/sig_gen/writer.rb +24 -2
  144. data/lib/rigor/source/node_children.rb +116 -0
  145. data/lib/rigor/source/node_locator.rb +3 -1
  146. data/lib/rigor/source/node_walker.rb +4 -2
  147. data/lib/rigor/source.rb +1 -0
  148. data/lib/rigor/type/difference.rb +28 -24
  149. data/lib/rigor/type/hash_shape.rb +34 -10
  150. data/lib/rigor/version.rb +1 -1
  151. data/lib/rigortype.rb +24 -0
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  154. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  163. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  165. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  166. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  169. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  170. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  171. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  172. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  173. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  174. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
  175. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  176. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
  177. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
  178. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
  179. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  180. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  181. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  182. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  184. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  185. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  186. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  187. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  188. data/sig/prism_node_children.rbs +9 -0
  189. data/sig/rigor/environment.rbs +1 -0
  190. data/sig/rigor/inference.rbs +7 -0
  191. data/sig/rigor/plugin/base.rbs +2 -3
  192. data/sig/rigor/reflection.rbs +1 -0
  193. data/sig/rigor/scope.rbs +12 -1
  194. data/skills/rigor-ci-setup/SKILL.md +10 -0
  195. data/skills/rigor-editor-setup/SKILL.md +11 -0
  196. data/skills/rigor-next-steps/SKILL.md +23 -2
  197. data/skills/rigor-plugin-author/SKILL.md +3 -3
  198. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  199. data/skills/rigor-plugin-review/SKILL.md +2 -2
  200. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  201. data/skills/rigor-project-init/SKILL.md +29 -3
  202. data/skills/rigor-project-init/references/01-detect.md +1 -1
  203. data/skills/rigor-project-init/references/02-configure.md +39 -5
  204. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  205. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  206. metadata +49 -24
  207. data/lib/rigor/cli/ci_detector.rb +0 -89
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d38773edf003d6de8ea6ec3de2f19aac4c2d74e404c56f39d1ed9595f388a648
4
- data.tar.gz: beb2b7aea30bf74f4cc8d22e66d441d63ecb906689d6a22c6ca647a7a087a719
3
+ metadata.gz: 7cea93d0b50519bad210f33907d4135c61043d910618153ad76f1f25b0163b4d
4
+ data.tar.gz: 45cba56af68f2ee34c7e8674310fb35bb74e366a3fc67bc89262ad8cb9556cc7
5
5
  SHA512:
6
- metadata.gz: 1c33b18e310c956f9cc9bbd4806228a095bcf95a137efba50affd49ffca17decac9107006e4a7fd350a9783f289e7d9e336ebc99755e7d69ce321d60b21c731b
7
- data.tar.gz: c9f7286bc00f3419083ced2e35678062e8508dfe03b6064fe6c9f4cf4ad752479ca8801cce059cb3fc6bc0ee0eaaa216824a9d43cc93332feb7e267ab988253c
6
+ metadata.gz: 626ef3d0d0967cfd0f8e887a7ab2f53ed741a848fd25f90239c69e7a40e84c3b4a658acd945da0fd4d344d9de9cc650c3be8236f107468b70e2d32441ee9d7d3
7
+ data.tar.gz: e23af83633c18c05dce1a01814f235838143a31f3a34cf719ca35239a40bc854fff8f8e4bd55f4d0c4f39547b4b600c7c543019055576e297fed36ef60a70537
data/README.md CHANGED
@@ -231,8 +231,8 @@ rigor docs --list # list every bundled page
231
231
 
232
232
  ## Status
233
233
 
234
- Current release: **`v0.2.8`** (2026-07-06) — the latest cut on the
235
- `0.2.x` evaluation line opened by `v0.2.0`, the first
234
+ Current release: **`v0.3.0`** (2026-07-19) — the first cut on the
235
+ `0.3.x` line, continuing the evaluation era opened by `v0.2.0`, the first
236
236
  publicly-announced (general / evaluation) release. The line publishes
237
237
  an enumerated [compatibility surface](docs/compatibility.md) as a
238
238
  minor-non-break trial, rehearsing the contract that hard-freezes at
@@ -0,0 +1,28 @@
1
+ # Rigor core overlay — supplemental core signatures.
2
+ #
3
+ # --- CSV::MalformedCSVError.new(message, line_number) ---
4
+ #
5
+ # The pinned `rbs` gem's `csv.rbs` never declares `MalformedCSVError`'s own
6
+ # constructor, so it falls back to the inherited `StandardError#initialize`
7
+ # (arity 0..1). But the `csv` gem's real, documented constructor takes two
8
+ # arguments — the message and the 1-based line number:
9
+ #
10
+ # raise CSV::MalformedCSVError.new("Unquoted fields...", line)
11
+ #
12
+ # Without the two-arg form a raise like GitLab's
13
+ # `import_csv/base_service.rb` false-fires `call.wrong-arity`
14
+ # ("expected 0..1, got 2"). The line number is optional here so the
15
+ # inherited single-arg form still type-checks.
16
+
17
+ # `CSV` is a CLASS in Ruby / RBS (a `module CSV` wrapper collides with upstream
18
+ # once the `csv` stdlib is in scope). It must be declared EXPLICITLY here — the
19
+ # overlay loads unconditionally, but `csv` is a stdlib library loaded only on
20
+ # demand, so a bare `class CSV::MalformedCSVError` would make RBS *synthesize*
21
+ # the `CSV` parent namespace (ADR-5) in every env that does not load `csv`,
22
+ # polluting `synthesized_namespaces`. Declaring `class CSV` reopens it cleanly
23
+ # when `csv` is loaded and declares it plainly when it is not.
24
+ class CSV
25
+ class MalformedCSVError
26
+ def initialize: (String message, ?Integer line_number) -> void
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # Rigor core overlay — supplemental core signatures.
2
+ #
3
+ # --- Psych.parse / Psych.parse_stream ---
4
+ #
5
+ # The pinned `rbs` gem's `psych.rbs` declares `Psych.load` / `load_file` /
6
+ # `safe_load` / `unsafe_load*` but omits `Psych.parse` — a real, documented
7
+ # stdlib method (`YAML.parse` is the same method, `YAML` being an alias of
8
+ # `Psych`). It parses one YAML document into a `Psych::Nodes::Document` AST
9
+ # (used for structure-preserving inspection) and raises on a syntax error.
10
+ #
11
+ # YAML.parse(File.read("config.yml")) # => Psych::Nodes::Document
12
+ #
13
+ # Without the declaration a call like GitLab's
14
+ # `release_highlights/validator.rb` `YAML.parse(...)` false-fires
15
+ # `call.undefined-method`. The return is left `untyped` — the node AST is
16
+ # rarely consumed with per-method precision, and depending on
17
+ # `Psych::Nodes::Document` being present in the pinned RBS would be brittle.
18
+
19
+ module Psych
20
+ def self.parse: (String yaml, ?filename: untyped) -> untyped
21
+ def self.parse_stream: (String yaml, ?filename: untyped) -> untyped
22
+ end
@@ -87,6 +87,14 @@ silent everywhere it cannot prove a narrower type.
87
87
  A diagnostic only fires when Rigor has enough static
88
88
  information to be confident.
89
89
 
90
+ Where a *signature* does appear in this book — like
91
+ `def foo: (String) -> Integer` — that is **RBS**, Ruby's standard
92
+ signature language; read it as "takes a `String`, returns an
93
+ `Integer`." You write none of it to get started; Rigor consumes
94
+ whatever RBS your gems and the standard library already ship.
95
+ [Chapter 7](07-rbs-and-extended.md) covers RBS in full — if you
96
+ have never seen it, skim that chapter first.
97
+
90
98
  ## The smallest working session
91
99
 
92
100
  Drop into your project root and run:
@@ -191,7 +199,7 @@ assert_type(":int | :str | nil", kind(7)) # union of all case branches
191
199
 
192
200
  ```ruby
193
201
  greeting = "Hello, " # Constant<"Hello, ">
194
- name = ARGV.first # String? (RBS-declared)
202
+ name = ARGV.first # String? (String or nil — RBS-declared)
195
203
  hello = "#{greeting}#{name}!" # literal-string carrier:
196
204
  # every interpolated part
197
205
  # is itself literal-string-
@@ -42,7 +42,10 @@ this chapter is the carrier zoo.
42
42
  One note on notation before the zoo: angle brackets hold a
43
43
  concrete value or bound — `Constant<3>`, `int<0, max>` —
44
44
  while square brackets hold type parameters, exactly as in RBS
45
- — `Nominal[String]`, `Hash[K, V]`, `Dynamic[top]`.
45
+ — `Nominal[String]`, `Hash[K, V]`, `Dynamic[top]`. A type
46
+ parameter names the type of a *part*: `Hash[K, V]` is a Hash
47
+ whose keys are type `K` and values type `V`, just as
48
+ `Array[String]` is "an array of strings."
46
49
 
47
50
  ## Seeing carriers yourself — `rigor annotate`
48
51
 
@@ -275,7 +275,8 @@ end
275
275
  (Narrowing the truthy edge further to specific refinement
276
276
  carriers — so `\d{4}` would produce `decimal-int-string` — is a
277
277
  demand-driven follow-up on the regex-pattern → refinement-name
278
- recogniser track; see [`docs/ROADMAP.md`](../ROADMAP.md).)
278
+ recogniser track, tracked as
279
+ [#164](https://github.com/rigortype/rigor/issues/164).)
279
280
 
280
281
  ## Negation and `unless`
281
282
 
@@ -221,9 +221,13 @@ arr = [1, ARGV.first]
221
221
  # Array[Constant<1> | String?]
222
222
  ```
223
223
 
224
- The same goes for hashes whose keys are not provably symbol /
225
- string literals Rigor produces `Hash[K, V]` rather than
226
- `HashShape`.
224
+ The same goes for hashes whose keys are not provably scalar
225
+ literals (symbols, plain strings, integers, floats, `true` /
226
+ `false` / `nil`) — Rigor produces `Hash[K, V]` rather than
227
+ `HashShape`. Within a shape, key identity follows `Hash#eql?`:
228
+ `1` and `1.0` are distinct keys, and a repeated literal key
229
+ keeps the last entry, exactly like the runtime. Non-symbol,
230
+ non-string keys display in hashrocket form (`{ 1 => 2 }`).
227
231
 
228
232
  ## Deriving new shapes — `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of`
229
233
 
@@ -170,8 +170,8 @@ discovery composes uniformly across both shapes.
170
170
 
171
171
  `Struct.new(*Symbol)` produces a positional-arg constructor
172
172
  plus the same accessors as `Data.define`. Rigor folds struct
173
- member reads too, but — because a `Struct` is mutable — only
174
- where the value cannot have changed:
173
+ member reads too, and — because a `Struct` is mutable — tracks a
174
+ straight-line setter so the read stays precise:
175
175
 
176
176
  ```ruby
177
177
  Coord = Struct.new(:x, :y)
@@ -181,18 +181,27 @@ c = Coord.new(10, 20)
181
181
  assert_type("10", c.x)
182
182
  assert_type("20", c.y)
183
183
 
184
- # A local that is mutated, aliased, or escapes is not fold-safe —
185
- # its reads degrade to Dynamic, never a stale value.
184
+ # A straight-line setter re-types the member; the sibling stays precise.
186
185
  m = Coord.new(1, 2)
187
186
  m.x = 9
188
- assert_type("Dynamic[top]", m.x)
187
+ assert_type("9", m.x)
188
+ assert_type("2", m.y)
189
+
190
+ # A local that is aliased, escapes, or is mutated inside a loop or
191
+ # block is not fold-safe — its reads degrade to Dynamic, never a
192
+ # stale value, because a write Rigor cannot see could have changed it.
193
+ a = Coord.new(1, 2)
194
+ alias_ref = a
195
+ a.x = 9
196
+ assert_type("Dynamic[top]", a.x)
189
197
  ```
190
198
 
191
199
  Because `Struct` accessors are also writers, the fold is gated:
192
- a member read off a freshly constructed instance (`Coord.new(1,
193
- 2).x`) or a local the analysis proves is never written, aliased,
194
- or passed away folds to the member's type; anything else widens
195
- to `Dynamic[top]`. `Data` is read-only, so its reads always fold.
200
+ a member read folds off a freshly constructed instance (`Coord.new(1,
201
+ 2).x`), off a local the analysis proves is never written, aliased, or
202
+ passed away, or off one whose only writes are straight-line member
203
+ setters (their assigned type flows into the later read); anything else
204
+ widens to `Dynamic[top]`. `Data` is read-only, so its reads always fold.
196
205
 
197
206
  ## Inheritance and method resolution
198
207
 
@@ -87,6 +87,7 @@ Fire when the control flow itself is unsound.
87
87
  | `flow.always-truthy-condition` | The predicate of an `if` / `unless` / ternary is provably truthy (or falsey) by inferred type, with surgical skips inside loop bodies and on defensive predicate calls. | warning |
88
88
  | `flow.unreachable-clause` | A `case <local>; when <Class>` (or bare-class `case`/`in`) clause whose subject narrowing proves it can never match — disjoint from the subject's type, or already exhausted by an earlier clause. | info under `balanced`, warning under `strict`, info under `lenient` |
89
89
  | `flow.dead-assignment` | A plain local-variable write whose target name is never read in the same `def` body. | warning |
90
+ | `flow.duplicate-hash-key` | Two entries of one Hash literal carry the same literal key (`{ a: 1, a: 2 }`, `m("x" => 1, "x" => 2)`) — the last entry wins silently at runtime. Literal keys only; `:a` vs `"a"` and `1` vs `1.0` are distinct keys and never compared. | warning |
90
91
 
91
92
  `flow.unreachable-branch`, `flow.always-truthy-condition`, and
92
93
  `flow.unreachable-clause` are the **reachability family** — each
@@ -142,9 +143,9 @@ shipped severities:
142
143
 
143
144
  | Profile | Behaviour |
144
145
  | --- | --- |
145
- | `lenient` | Most rules `warning`; uncertain rules drop to `info`. CI-friendly for legacy code. |
146
- | `balanced` (default) | Most rules → `error`; `dump.type` → `info`. The shipped behaviour. |
147
- | `strict` | Everything → `error` including the `:warning` rules under `balanced`. Suitable for new projects with no legacy noise. |
146
+ | `lenient` | Only proven rules stay `error` (`call.undefined-method`, `wrong-arity`, `assert.type-mismatch`); uncertain rules drop to `warning`, and several to `off`. For incremental adoption on legacy code. |
147
+ | `balanced` (default) | Most rules → `error`; uncertain rules → `warning`; `dump.type` → `info`. The shipped behaviour. |
148
+ | `strict` | Nearly every rule → `error`. The exceptions: `call.self-undefined-method` stays `off` (opt-in only), and `flow.unreachable-clause` is `warning` (pending its false-positive gate). Suitable for new projects with no legacy noise. |
148
149
 
149
150
  Set in `.rigor.yml`:
150
151
 
@@ -93,7 +93,7 @@ states:
93
93
  | `equivalent` | The inferred return is not a strict subtype of the declared one — identical, wider, or unrelated — so there is nothing to tighten. Silently skipped. |
94
94
  | `skipped` | Disqualified for one of the reasons below. |
95
95
 
96
- The three `sig.skipped.*` reasons are:
96
+ The `sig.skipped.*` reasons are:
97
97
 
98
98
  - `sig.skipped.complex-shape` — the method has optional, rest,
99
99
  keyword, block, or forwarding parameters. The MVP's
@@ -105,6 +105,16 @@ The three `sig.skipped.*` reasons are:
105
105
  - `sig.skipped.user-authored` — `--overwrite` was not set
106
106
  and the method's existing RBS declaration would have to
107
107
  be replaced.
108
+ - `sig.skipped.unrenderable-rbs` — the signature Rigor
109
+ rendered for this method does not parse as RBS. This one
110
+ is a **bug in Rigor**, not a property of your code: every
111
+ generated line is parsed before it is emitted, and a line
112
+ `rbs` rejects is dropped rather than written, because an
113
+ unparseable `.rbs` is quarantined *whole* by `rigor check`
114
+ — one bad line would take every other type in the file
115
+ down with it. The rest of the signatures are unaffected;
116
+ the skipped method is reported on stderr, and it is worth
117
+ reporting to us.
108
118
 
109
119
  The three `sig.generated.*` identifiers
110
120
  (`sig.generated.new-file` / `new-method` / `tighter-return`)
@@ -345,9 +345,10 @@ def shout: (String s) -> non-empty-string
345
345
 
346
346
  You gain: `s.empty?` is a recognised refinement narrower (no
347
347
  need for `assert s`). `match?(/\A\d+\z/)` does not yet narrow
348
- to `numeric-string` (this is on the v0.1.1 roadmap — see
349
- [`docs/ROADMAP.md`](../ROADMAP.md)), but the eventual
350
- behaviour will mirror `s.isdigit()` narrowing in Pyright.
348
+ to `numeric-string` (queued as
349
+ [#164](https://github.com/rigortype/rigor/issues/164)), but the
350
+ eventual behaviour will mirror `s.isdigit()` narrowing in
351
+ Pyright.
351
352
 
352
353
  ## What's next
353
354
 
data/docs/install.md CHANGED
@@ -37,19 +37,27 @@ Then proceed to the **first** matching case below.
37
37
  and pins language runtimes (Ruby, Node, Python, …) and tool gems
38
38
  (like `rigortype`) per project, recording versions in a `mise.toml`
39
39
  that can be committed alongside the code. Other contributors — and CI
40
- — run `mise install` to restore the exact same versions with no
41
- `Gemfile` involvement.
40
+ — run `mise install` to restore those versions with no `Gemfile`
41
+ involvement.
42
42
 
43
43
  Run in the project root:
44
44
 
45
45
  ```sh
46
46
  mise use ruby@4.0
47
- mise use gem:rigortype
47
+ mise use --pin gem:rigortype
48
48
  ```
49
49
 
50
50
  `mise use` installs the tools and writes their versions to `mise.toml`
51
51
  in one step. Commit `mise.toml` so the version is shared.
52
52
 
53
+ `--pin` records the exact Rigor version (`"gem:rigortype" = "0.2.9"`).
54
+ Without it mise writes `"gem:rigortype" = "latest"`, which every
55
+ machine re-resolves to whatever is newest when it first installs — so
56
+ a committed `latest` does not give the team one shared version. The
57
+ trade-off: a pin will not move on its own, and `mise outdated` cannot
58
+ report a pinned tool as behind. Upgrade with `mise upgrade --bump
59
+ gem:rigortype`.
60
+
53
61
  Then verify:
54
62
 
55
63
  ```sh
@@ -184,6 +192,6 @@ project is set up, re-run `rigor skill describe` for the step after
184
192
  that.
185
193
 
186
194
  If `rigor skill describe` is not recognised, your Rigor version predates
187
- it. Run `rigor --version` and upgrade with `mise use gem:rigortype` (or
188
- `gem update rigortype` for Case C/B); on an older version, run
189
- `rigor skill rigor-project-init` directly.
195
+ it. Run `rigor --version` and upgrade with `mise upgrade --bump
196
+ gem:rigortype` (or `gem update rigortype` for Case B/C); on an older
197
+ version, run `rigor skill rigor-project-init` directly.
@@ -244,9 +244,23 @@ A few things worth knowing if you have not used mise before:
244
244
  *current directory* recording the chosen versions, and installs
245
245
  the tools as part of the same command — there is no separate
246
246
  install step. (mise also reads the asdf-style `.tool-versions`.)
247
- - **Commit the config to share versions.** Check the generated
248
- `mise.toml` into Git so every contributor — and every CI run —
249
- resolves the same Ruby 4.0 and the same Rigor version.
247
+ - **Commit the config to share versions and pass `--pin` if you
248
+ mean it.** Check the generated `mise.toml` into Git so every
249
+ contributor, and every CI run, resolves the same versions. Note
250
+ what the two commands above actually record, because they differ:
251
+ `mise use ruby@4.0` preserves the precision you asked for and
252
+ writes `ruby = "4.0"` (any 4.0.x), but `mise use gem:rigortype` has
253
+ no requested version to preserve and writes `"gem:rigortype" =
254
+ "latest"` — which resolves to whatever is newest *on each machine,
255
+ whenever that machine first installs it*. A committed `latest` is
256
+ not a shared version. To share one, pin it:
257
+
258
+ ```sh
259
+ mise use --pin gem:rigortype # records e.g. "gem:rigortype" = "0.2.9"
260
+ ```
261
+
262
+ Then read [Keeping Rigor up to date](#keeping-rigor-up-to-date):
263
+ a pin is a version that will not move until you move it.
250
264
  - **For a machine-wide install, add `-g`.** `mise use -g
251
265
  gem:rigortype` writes mise's global config
252
266
  (`~/.config/mise/config.toml`) instead of a project `mise.toml`,
@@ -284,6 +298,48 @@ explicitly with `mise exec gem:rigortype -- rigor`. See
284
298
  [Editor integration](09-editor-integration.md) for the editor
285
299
  side.
286
300
 
301
+ ### Keeping Rigor up to date
302
+
303
+ Rigor ships often. How you upgrade depends on what your config
304
+ records — and in one case, on knowing that mise will not tell you an
305
+ upgrade exists.
306
+
307
+ - **`"gem:rigortype" = "latest"`** — what a plain `mise use
308
+ gem:rigortype` writes. `mise upgrade gem:rigortype` moves you to the
309
+ newest release.
310
+ - **An exact pin** — `"gem:rigortype" = "0.2.9"`, from `--pin` or from
311
+ `mise use gem:rigortype@0.2.9`. Here `mise upgrade` **will not move
312
+ it, and `mise outdated` will not report it.** Both compare the
313
+ installed version against the range the config asks for, and an
314
+ exact pin is a range containing only itself, so a pinned Rigor
315
+ reports itself up to date forever, however far behind it has fallen.
316
+ Use `--bump`, which installs the newest release *and* rewrites the
317
+ pin:
318
+
319
+ ```sh
320
+ mise upgrade --bump gem:rigortype
321
+ ```
322
+
323
+ Neither is a bug to work around — a pin doing nothing is a pin working.
324
+ But it does mean a pinned setup has no passive signal that a new Rigor
325
+ exists: `--bump` is both how you look and how you move.
326
+
327
+ **After a Ruby change.** mise's `gem:` backend installs each tool
328
+ against the Ruby that was active at install time, and its
329
+ [gem backend docs](https://mise.jdx.dev/dev-tools/backends/gem.html)
330
+ note that "if the ruby version used by a gem package changes, (by mise
331
+ or system ruby), you may need to reinstall the gem". Rigor's pinned
332
+ `ruby@4.0` makes this rare — patch upgrades within 4.0.x are followed
333
+ automatically — but if you remove or replace that Ruby, reinstall:
334
+
335
+ ```sh
336
+ mise install -f gem:rigortype # or, for every gem-backend tool:
337
+ mise install -f "gem:*"
338
+ ```
339
+
340
+ On a plain `gem install` (below), the equivalent is `gem update
341
+ rigortype`.
342
+
287
343
  ## asdf
288
344
 
289
345
  `asdf` follows the same model. Install a Ruby 4.0.x with the
@@ -291,6 +347,7 @@ side.
291
347
  it for the project, then install the gem into that Ruby:
292
348
 
293
349
  ```sh
350
+ asdf plugin add ruby # once, if the ruby plugin isn't added yet
294
351
  asdf install ruby latest:4.0
295
352
  asdf local ruby latest:4.0
296
353
  gem install rigortype
@@ -299,7 +356,8 @@ gem install rigortype
299
356
  `asdf` has no general-purpose gem backend, so the gem itself is
300
357
  installed with `gem install` rather than an `asdf` command. `mise`
301
358
  (above) is the more integrated option because its `gem:` backend
302
- pins the gem the same way it pins Ruby.
359
+ records the gem in the same config that records Ruby, and — as the
360
+ next section explains — keeps the two from interfering.
303
361
 
304
362
  ## Simple alternative — gem install
305
363
 
@@ -315,6 +373,43 @@ quickest path, but it records nothing per project: a version
315
373
  manager keeps the Rigor version pinned next to the project, so
316
374
  local runs and CI cannot drift apart.
317
375
 
376
+ It also leaves Rigor sharing a Ruby with your work, which is the
377
+ deeper reason the version managers are listed first. The executable
378
+ RubyGems installs begins with `#!/usr/bin/env ruby`, so it runs under
379
+ whatever `ruby` is first on `PATH` *at the moment you invoke it* — and
380
+ because Rigor requires Ruby 4.0 while your projects pin their own,
381
+ running `rigor` inside a project on Ruby 3.x fails before it starts.
382
+ mise's `gem:` backend does not have this failure mode: it installs each
383
+ tool into its own private gem directory and points the executable at
384
+ the Ruby the tool was installed with, so a project's Ruby pin cannot
385
+ reach it. A `gem install` under a Ruby 4.0 you never switch away from
386
+ is perfectly fine; one that shares a Ruby with projects you switch
387
+ between is a footgun.
388
+
389
+ ## If you want Bundler to manage the version
390
+
391
+ The rule at the top of this chapter is about your *application's*
392
+ `Gemfile` — the one Bundler resolves against your app's gems, and whose
393
+ entries `Bundler.require` loads at boot. It is not a rule against
394
+ Bundler. A `Gemfile` holding nothing but Rigor, resolved separately and
395
+ selected with `BUNDLE_GEMFILE`, keeps every property the prohibition
396
+ exists to protect: your application's dependency graph never meets
397
+ Rigor's, and your application's Ruby is never constrained by Rigor's.
398
+
399
+ That arrangement — a `Gemfile` under `.github/rigor/`, plus the
400
+ Dependabot config that keeps it current — is written up under
401
+ [Pinning Rigor's version](11-ci.md#pinning-rigors-version) in the CI
402
+ chapter. Nothing about it is CI-specific:
403
+
404
+ ```sh
405
+ BUNDLE_GEMFILE=.github/rigor/Gemfile bundle exec rigor check
406
+ ```
407
+
408
+ It is more moving parts than `mise use --pin gem:rigortype`, which
409
+ records the same version in one line and needs no `bundle exec`. Reach
410
+ for it when your team already runs everything through Bundler and wants
411
+ Rigor on the same footing — not as a first choice.
412
+
318
413
  ## Nix
319
414
 
320
415
  If you use Nix, Rigor's flake exposes the executable as a package,
@@ -41,12 +41,12 @@ the `paths:` list from the configuration file.
41
41
  | `--cache-stats` | Print the on-disk cache inventory when finished. |
42
42
  | `--[no-]stats` | Print a run summary (files, classes, memory, wall time) to stderr. Default on. |
43
43
  | `--coverage` | Add a type-precision coverage block to the output (`coverage` object under `--format json`; a one-line summary in text mode). Off by default — it is a second precision pass over the analyzed files, the same scan [`rigor coverage`](#rigor-coverage) runs, so it is opt-in. |
44
- | `--workers=N` | Dispatch analysis across `N` parallel worker processes (fork-based pool today; ADR-15). Default `0` (sequential). |
44
+ | `--workers=N` | Dispatch analysis across `N` parallel worker processes (fork-based pool today; ADR-15). Default `0` (sequential). Applies to `--incremental` re-checks as well as full runs. |
45
45
  | `--baseline=PATH` | Load a baseline file, overriding config. |
46
46
  | `--no-baseline` | Ignore any configured baseline. |
47
47
  | `--baseline-strict` | Fail the run on any baseline drift — a CI gate. |
48
48
  | `--treat-all-as-inline-rbs` | Force-load `rigor-rbs-inline` with `require_magic_comment: false`, so every analysed file is treated as inline-RBS without the `# rbs_inline: enabled` comment (ADR-32). |
49
- | `--bleeding-edge[=ids]` | Adopt the bleeding-edge overlay for this run, overriding the configured [`bleeding_edge:`](03-configuration.md) selection (ADR-50 § WD2). Bare adopts every queued feature; `--bleeding-edge=a,b` adopts only the named feature ids. The overlay is empty in this release, so this is currently a no-op. Inspect it with [`rigor show-bleedingedge`](#rigor-show-bleedingedge). |
49
+ | `--bleeding-edge[=ids]` | Adopt the bleeding-edge overlay for this run, overriding the configured [`bleeding_edge:`](03-configuration.md) selection (ADR-50 § WD2). Bare adopts every queued feature; `--bleeding-edge=a,b` adopts only the named feature ids. Inspect it with [`rigor show-bleedingedge`](#rigor-show-bleedingedge). |
50
50
  | `--no-bleeding-edge` | Ignore any configured `bleeding_edge:` selection for this run (adopt none). |
51
51
  | `--tmp-file=PATH --instead-of=PATH` | Editor mode: analyse `PATH` using the buffer in `--tmp-file`. Both required together. |
52
52
 
@@ -183,6 +183,17 @@ rigor sig-gen [paths]
183
183
  | `--new-files` / `--new-methods` / `--tighter-returns` | Emit only that classification. |
184
184
  | `--format=text\|json` | Output format. |
185
185
 
186
+ Every signature is parsed before it is emitted. A method whose
187
+ generated RBS does not parse is **skipped** (`sig.skipped.unrenderable-rbs`)
188
+ and reported on stderr rather than written — an unparseable
189
+ `.rbs` is quarantined *whole* by `rigor check`, so one bad line
190
+ would take every other type in the file down with it. Under
191
+ `--write`, a file whose assembled content does not parse is
192
+ **refused** (the existing file is left untouched) and the command
193
+ exits `1`: you asked for a write and did not get one. Such a skip
194
+ is a bug in Rigor's RBS rendering, not in your code — please
195
+ report it.
196
+
186
197
  ## `rigor lsp`
187
198
 
188
199
  Run the Language Server over stdio. See
@@ -279,6 +290,10 @@ inferring".
279
290
  rigor coverage [paths]
280
291
  ```
281
292
 
293
+ `paths` are files or directories; when omitted, Rigor uses the
294
+ `paths:` list from the configuration file (default `lib`), the same
295
+ as [`rigor check`](#rigor-check).
296
+
282
297
  `--format=text|json` selects the output format and
283
298
  `--config=PATH` overrides config discovery. `--threshold=RATIO`
284
299
  exits `1` when the precision ratio falls below `RATIO`
@@ -295,7 +310,13 @@ ranked "add a type here" list (the methods most often called on
295
310
  an untyped receiver), then the least-protected files;
296
311
  `--threshold` and `--format=json` work the same. It is a sound
297
312
  upper bound on real protection — a concrete receiver is necessary
298
- but not sufficient for a diagnostic to fire.
313
+ but not sufficient for a diagnostic to fire. `--workers=N`
314
+ fork-parallelizes the protection scan (both the parameter-inference
315
+ pre-pass and the per-file scan) with output byte-identical to a
316
+ sequential run; it resolves the worker count the same way `check`
317
+ does — `--workers` › `RIGOR_RACTOR_WORKERS` ›
318
+ [`parallel.workers:`](03-configuration.md) › `0` (sequential
319
+ default).
299
320
 
300
321
  Adding `--mutation` (with `--protection`) switches to the
301
322
  **effectiveness** tier: it measures whether Rigor *does* catch a
@@ -390,6 +411,11 @@ Report the activation status of every plugin configured in
390
411
  `.rigor.yml` — loaded, load-error (with reason), and each
391
412
  plugin's declared extension surfaces. See [Plugins](07-plugins.md).
392
413
 
414
+ Each loaded plugin's row also reports the resolved file it
415
+ loaded from (a `path:` line in text, a `"path"` key in JSON), so
416
+ if a stale installed gem is shadowing a newer checkout's bundled
417
+ plugin copy the mismatch is visible at a glance.
418
+
393
419
  ```sh
394
420
  rigor plugins [--format=text|json] [--strict] [--capabilities] [--config=PATH]
395
421
  ```
@@ -403,7 +429,7 @@ catalogue** ([ADR-37](../adr/37-plugin-interface-segregation.md)):
403
429
  a focused, machine-readable map of what each loaded plugin
404
430
  contributes — the AST node types its `node_rule`s match, the
405
431
  receiver classes its `dynamic_return`s gate on, the methods its
406
- `narrowing_facts`s narrow, and the facts it `produces` /
432
+ `narrowing_facts` hooks narrow, and the facts it `produces` /
407
433
  `consumes`. Combine with `--format=json` for tooling (an AI
408
434
  agent can enumerate every plugin's behaviour without reading a
409
435
  line of plugin source). The same narrow surfaces also appear in
@@ -464,8 +490,9 @@ rigor skill [<name>] [--full <name>] [--path <name>] [--list] [--describe]
464
490
  | `--describe` | Probe the project's state (config / baseline / `sig/` / CI — presence only, never runs `rigor check`) and recommend the next skill to run. Also spelled `describe`; surfaced top-level as [`rigor describe`](#rigor-describe) below. |
465
491
 
466
492
  The verb spellings `rigor skill list` / `print <name>` / `path <name>`
467
- are **deprecated** (a one-line stderr notice; removed in v0.3.0)use
468
- the forms above. `describe` / `--describe` stay first-class.
493
+ were **removed in v0.3.0**the positional slot is a skill name, so
494
+ they now read as an unknown skill. Use the forms above.
495
+ `describe` / `--describe` stay first-class.
469
496
 
470
497
  ## `rigor describe`
471
498
 
@@ -508,8 +535,9 @@ rigor docs [<name>] [--path <name>] [--list [<category>]]
508
535
  | `--path <name>` | Print the single-line absolute path of a doc, suitable as input to a file-reading tool. |
509
536
  | `--list [<category>]` | Table of every bundled doc (name + absolute path); pass `manual` or `handbook` to filter. |
510
537
 
511
- The verb spellings `rigor docs list` / `path <name>` are **deprecated**
512
- (a one-line stderr notice; removed in v0.3.0)use `--list` / `--path`.
538
+ The verb spellings `rigor docs list` / `path <name>` were **removed in
539
+ v0.3.0**the positional slot is a doc name, so they now read as an
540
+ unknown doc. Use `--list` / `--path`.
513
541
 
514
542
  The canonical web copy of the index is
515
543
  <https://rigor.typedduck.fail/llms.txt>; `rigor docs` serves the same
@@ -533,12 +561,16 @@ rigor show-bleedingedge [--config PATH] [--format text|json]
533
561
  | `--config PATH` | Use this `.rigor.yml` instead of auto-discovery. |
534
562
  | `--format text\|json` | Output format. Default `text`. |
535
563
 
536
- The overlay is **empty in this release** the mechanism is wired and
537
- ready, but no discipline has been queued for a major yet, so the command
538
- currently reports an empty set. When a feature is queued it appears here
539
- with its stable id, the severity it would impose, and whether your config
540
- adopts it. See [`docs/compatibility.md`](../compatibility.md) for how
541
- bleeding-edge fits the stability model.
564
+ Each queued feature appears with its stable id, the severity it imposes,
565
+ and whether your configuration adopts it. See
566
+ [`docs/compatibility.md`](../compatibility.md) for how bleeding-edge fits
567
+ the stability model.
568
+
569
+ Queued today:
570
+
571
+ | Feature id | What it changes |
572
+ | --- | --- |
573
+ | `reject-unparseable-signatures` | An unparseable `.rbs` under `signature_paths:` **fails the run** (`rbs.coverage.quarantined-signature` → `error`) instead of being skipped with a warning. |
542
574
 
543
575
  ## `rigor doctor`
544
576
 
@@ -557,8 +589,7 @@ rigor doctor [--config PATH] [--format text|json]
557
589
  Runs a scoped analysis and audits:
558
590
 
559
591
  - **Configuration audit** — unresolved `signature_paths:`, unknown
560
- `libraries:`, inert `disable:` / `severity_overrides:` tokens
561
- ({ConfigAudit}).
592
+ `libraries:`, inert `disable:` / `severity_overrides:` tokens.
562
593
  - **RBS environment health** — whether the RBS class universe built
563
594
  successfully (`0` classes means a broken setup).
564
595
  - **Plugin load errors** — whether every configured plugin loaded.
@@ -566,9 +597,21 @@ Runs a scoped analysis and audits:
566
597
  from the saved baseline.
567
598
  - **Rails plugin gap** — whether `Gemfile.lock` contains Rails gems
568
599
  but no Rails plugin is enabled.
600
+ - **Gemfile install** — whether Rigor itself resolves as one of the
601
+ project's dependencies, which
602
+ [Installing Rigor](01-installation.md) tells you not to do. Only a
603
+ `rigortype` resolved from a **GEM** remote counts: a `PATH` or `GIT`
604
+ source means you are developing or vendoring Rigor deliberately,
605
+ and Rigor's own repository looks exactly like that.
606
+ - **Plugin installation skew** — whether a plugin Rigor bundles
607
+ loaded from a different `rigortype` installation than the engine,
608
+ naming both paths. The engine and its bundled plugins are
609
+ versioned together, so a copy from another installation can run
610
+ the engine with a mismatched plugin. A warning, not a failure;
611
+ third-party plugins from your own bundle are never flagged.
569
612
 
570
613
  Text output prints `[PASS]`, `[FAIL]`, or `[WARN]` per check plus a
571
- routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
614
+ routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
572
615
  is a stable contract:
573
616
 
574
617
  ```json
@@ -585,7 +628,7 @@ Exits `1` when any check fails, `0` when all pass.
585
628
  ## `rigor upgrade`
586
629
 
587
630
  Migration command skeleton ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
588
- WD7). The real body lands when a concrete backwards-compatibility
631
+ WD7). The real body lands when a concrete backwards-compatibility
589
632
  break gives it a target (e.g. re-running `baseline regenerate`
590
633
  against a strengthened default profile, surfacing renamed
591
634
  suppression ids, reporting `bleeding_edge:` graduations).
@@ -595,7 +638,7 @@ rigor upgrade
595
638
  ```
596
639
 
597
640
  Until then it prints the current version and notes that upgrade is
598
- queued. Exits `0`.
641
+ queued. Exits `0`.
599
642
 
600
643
  ## Environment variables
601
644
 
@@ -609,6 +652,9 @@ operational knobs read the environment instead.
609
652
  | `RIGOR_RACTOR_WORKERS=N` | Worker count for parallel analysis. Sits between the CLI flag and the config key in precedence: `--workers=N` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > `0` (sequential). |
610
653
  | `RIGOR_POOL_BACKEND=ractor` | Opt back into the (off-by-default) Ractor worker pool instead of the active fork-based pool ([ADR-15](../adr/15-ractor-concurrency.md)). Only relevant with a non-zero worker count; the fork pool is the supported backend. |
611
654
  | `RIGOR_PLUGIN_ISOLATION=none\|process\|ruby_box` | How a plugin's direct calls into its target library are isolated. Default `process`. See [Using plugins § Isolation strategy](07-plugins.md). `RIGOR_BOX` is a legacy alias for `ruby_box`. |
655
+ | `RIGOR_STRICT_VALIDATION=1` | Force full-content cache validation for one run (the same as `cache.validation: digest`, and winning over it) — re-hash every file's content instead of trusting its stat metadata. Use it if a filesystem's timestamps or inode numbers cannot be trusted. See [Caching § How a file is checked for changes](12-caching.md#how-a-file-is-checked-for-changes). |
656
+ | `RIGOR_DISABLE_YJIT=1` | Opt out of Rigor's deferred YJIT enablement. Rigor turns YJIT on partway through a long `check` / `coverage` run so short runs never pay the JIT warm-up; this variable leaves it off entirely. Diagnostics and allocations are identical either way — the effect is wall-time only. |
657
+ | `RIGOR_YJIT_DEADLINE=<seconds>` | Advanced: tune how long a run must last before deferred YJIT enables (default `5.0`). Lower it if your runs are long and you want the JIT sooner; raise it to protect short runs. Ignored when `RIGOR_DISABLE_YJIT=1` is set or YJIT is unavailable. |
612
658
 
613
659
  Three further variables (`RIGOR_BUDGET_TRACE`,
614
660
  `RIGOR_HEAP_PROFILE`, `RIGOR_HEAP_TRACE`) enable developer-facing