rigortype 0.4.6 → 0.4.7

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 (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
data/lib/rigor/cli.rb CHANGED
@@ -10,6 +10,9 @@ require_relative "version"
10
10
  require_relative "analysis/diagnostic"
11
11
  require_relative "analysis/result"
12
12
  require_relative "cli/options"
13
+ require "did_you_mean"
14
+
15
+ require_relative "cli/catalogue"
13
16
  require_relative "cli/diagnostic_formats"
14
17
  require_relative "ci_detector"
15
18
  require_relative "analysis/check_rules/rule_ids"
@@ -46,6 +49,17 @@ module Rigor
46
49
  # 2026-07-17 and nothing noticed. See `docs/internal-spec/config.md`.
47
50
  CONFIG_SCHEMA_URL = "https://github.com/rigortype/rigor/raw/master/schemas/rigor-config.schema.json"
48
51
 
52
+ # The comment `rigor init` writes under `plugins: []` when Gemfile.lock names a gem a bundled plugin models. The
53
+ # plugins are suggested, not enabled: ADR-96 Criterion 2 keeps a lockfile's presence from opting a project into a
54
+ # plugin's code, so the user uncomments the ones they want (#2536).
55
+ DETECTED_PLUGINS_HEADER = [
56
+ "# Detected from Gemfile.lock (suggested, not enabled). To load them, delete the `[]` above, uncomment the",
57
+ "# entries you want, then run `rigor plugins --strict`."
58
+ ].freeze
59
+
60
+ # The file whose presence marks a Rails application for `rigor init`'s `paths:` suggestion (#2560).
61
+ RAILS_APPLICATION_FILE = "config/application.rb"
62
+
49
63
  HANDLERS = {
50
64
  "check" => :run_check,
51
65
  "init" => :run_init,
@@ -79,6 +93,26 @@ module Rigor
79
93
  new(argv.dup, out: out, err: err).run
80
94
  end
81
95
 
96
+ # The parser `rigor init` parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
97
+ def self.init_option_parser(options = {})
98
+ OptionParser.new do |opts|
99
+ opts.banner = "Usage: rigor init [options]"
100
+ opts.on("--force", "Overwrite an existing configuration file") { options[:force] = true }
101
+ opts.on("--path=PATH", "Configuration file path") { |value| options[:path] = value }
102
+ opts.on("--no-plugins", "Write no commented plugin suggestions from Gemfile.lock") do
103
+ options[:plugins] = false
104
+ end
105
+ end
106
+ end
107
+
108
+ # The parser `rigor help` parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
109
+ def self.help_option_parser(options = {})
110
+ OptionParser.new do |opts|
111
+ opts.banner = "Usage: rigor help [--format=text|json] [<command>]"
112
+ Options.add_format(opts, options, "Output format: text (default) or json")
113
+ end
114
+ end
115
+
82
116
  def initialize(argv = ARGV.dup, out: $stdout, err: $stderr)
83
117
  @argv = argv
84
118
  @out = out
@@ -89,12 +123,14 @@ module Rigor
89
123
  command = @argv.shift
90
124
 
91
125
  case command
92
- when nil, "help", "-h", "--help"
126
+ when nil, "-h", "--help"
93
127
  @out.puts(help)
94
128
  0
129
+ when "help"
130
+ run_help
95
131
  when "version", "-v", "--version"
96
- @out.puts("rigor #{Rigor::VERSION}")
97
- 0
132
+ run_version
133
+
98
134
  else
99
135
  dispatch(command)
100
136
  end
@@ -139,6 +175,8 @@ module Rigor
139
175
  return arm_jit_deadline { send(handler) } if handler
140
176
 
141
177
  @err.puts("Unknown command: #{command}")
178
+ nearest = ::DidYouMean::SpellChecker.new(dictionary: Catalogue.names).correct(command).first
179
+ @err.puts("Did you mean `rigor #{nearest}`?") if nearest
142
180
  @err.puts(help)
143
181
  EXIT_USAGE
144
182
  end
@@ -156,6 +194,16 @@ module Rigor
156
194
  yield
157
195
  end
158
196
 
197
+ # `rigor version` and `-v`/`--version` print the version; `rigor version --help` prints the usage instead.
198
+ def run_version
199
+ if @argv.intersect?(%w[-h --help])
200
+ @out.puts("Usage: rigor version")
201
+ else
202
+ @out.puts("rigor #{Rigor::VERSION}")
203
+ end
204
+ 0
205
+ end
206
+
159
207
  def run_check
160
208
  require_relative "cli/check_command"
161
209
 
@@ -168,15 +216,11 @@ module Rigor
168
216
  # present; no implicit merge).
169
217
  options = {
170
218
  force: false,
171
- path: ".rigor.dist.yml"
219
+ path: ".rigor.dist.yml",
220
+ plugins: true
172
221
  }
173
222
 
174
- parser = OptionParser.new do |opts|
175
- opts.banner = "Usage: rigor init [options]"
176
- opts.on("--force", "Overwrite an existing configuration file") { options[:force] = true }
177
- opts.on("--path=PATH", "Configuration file path") { |value| options[:path] = value }
178
- end
179
- parser.parse!(@argv)
223
+ self.class.init_option_parser(options).parse!(@argv)
180
224
 
181
225
  path = options.fetch(:path)
182
226
  if File.exist?(path) && !options.fetch(:force)
@@ -184,24 +228,57 @@ module Rigor
184
228
  return 1
185
229
  end
186
230
 
187
- File.write(path, init_template(path))
231
+ plugins = options.fetch(:plugins) ? detected_plugins : []
232
+ paths_suggestion = detected_paths
233
+ begin
234
+ File.write(path, init_template(path, plugins: plugins, paths_suggestion: paths_suggestion))
235
+ rescue SystemCallError => e
236
+ # A destination whose directory is missing or not writable is a bad path argument, the same usage error
237
+ # `--config=PATH` gives for a file that does not exist (exit 64, docs/manual/02-cli-reference.md). The bare
238
+ # Errno::ENOENT backtrace read as an internal crash.
239
+ reason = e.message.sub(/ @ .*\z/m, "")
240
+ @err.puts("rigor init: cannot write #{path}: #{reason}")
241
+ return EXIT_USAGE
242
+ end
188
243
  @out.puts("Created #{path}")
189
- print_init_next_steps(path)
244
+ print_init_next_steps(path, plugins, paths_suggestion)
190
245
  0
191
246
  end
192
247
 
193
- # `rigor init`'s template ships empty `plugins:` so a fresh init has nothing to validate — but the moment the user
194
- # adds any plugin entry, the activation-failure surfaces enumerated in `rigor plugins`'s docstring become real.
248
+ # The bundled plugins whose target gems the project's `Gemfile.lock` declares: the read `rigor doctor` makes,
249
+ # through {PluginGapAdvisory}, so the two cannot disagree about what the project depends on. Empty without a
250
+ # lockfile, which leaves the template as it was before detection existed. The advisory is required lazily:
251
+ # it loads every bundled plugin's entry file, a cost a plain `rigor init` in a directory with no lockfile never
252
+ # pays.
253
+ def detected_plugins
254
+ require_relative "plugin_gap_advisory"
255
+
256
+ PluginGapAdvisory.gaps(project_root: Dir.pwd, plugins: []).map(&:plugin_gem)
257
+ end
258
+
259
+ # `rigor init`'s template lists the plugins Gemfile.lock detected, or ships empty `plugins:` so a fresh init has
260
+ # nothing to validate — but the moment the user adds any plugin entry, the activation-failure surfaces enumerated
261
+ # in `rigor plugins`'s docstring become real.
195
262
  # Point them at the verification command + the canonical readiness flow so silent failures (the cwd / Gemfile /
196
263
  # signature_paths mismatches that surfaced during the Mastodon trial) get caught the first time the user wires a
197
264
  # plugin, not the first time `rigor check` reports false positives that should have been covered.
198
- def print_init_next_steps(path)
265
+ def print_init_next_steps(path, plugins = [], paths_suggestion = nil)
199
266
  @out.puts ""
200
267
  @out.puts "Next steps:"
201
- @out.puts " 1. Edit #{path} — add the `plugins:` your project needs."
268
+ if plugins.empty?
269
+ @out.puts " 1. Edit #{path} — add the `plugins:` your project needs."
270
+ else
271
+ @out.puts " 1. Edit #{path} — to load a detected plugin, delete the `[]` on the `plugins:` line and"
272
+ @out.puts " uncomment the entries you want:"
273
+ plugins.each { |gem| @out.puts " #{gem}" }
274
+ end
202
275
  @out.puts " 2. Run `rigor plugins` to verify every configured plugin loads."
203
276
  @out.puts " (`--strict` exits 1 on failure; ideal CI gate.)"
204
277
  @out.puts " 3. Run `rigor check` to analyse your code."
278
+ return if paths_suggestion.nil?
279
+
280
+ @out.puts " `rigor check` analyses only `lib` until you edit `paths:` in #{path}. " \
281
+ "The suggestion is commented beneath it."
205
282
  end
206
283
 
207
284
  # Word-wraps `CheckRules::ALL_RULES` into a comma-separated, `#`-commented block for `init_template`, so the
@@ -230,8 +307,9 @@ module Rigor
230
307
 
231
308
  # Renders the starter `.rigor.yml` body. The template serialises `Configuration::DEFAULTS` (so the on-disk file
232
309
  # round-trips through `Configuration.load`) and prepends a short header that points the user at the keys they are
233
- # most likely to want to edit.
234
- def init_template(path = ".rigor.dist.yml")
310
+ # most likely to want to edit. `plugins:` stays the empty list the defaults ship; detected gems appear beneath it as
311
+ # comments, so a project with no lockfile gets the template it always did.
312
+ def init_template(path = ".rigor.dist.yml", plugins: [], paths_suggestion: nil)
235
313
  <<~YAML
236
314
  # yaml-language-server: $schema=#{CONFIG_SCHEMA_URL}
237
315
  # Rigor configuration. The full key reference is
@@ -287,10 +365,49 @@ module Rigor
287
365
  # `target_ruby` is left commented: the default only picks the syntax Rigor parses. Set it to the lowest
288
366
  # Ruby your project runs on, and Rigor also reports what that Ruby deprecates.
289
367
  # target_ruby: #{Configuration::DEFAULTS.fetch('target_ruby').dump}
290
- #{YAML.dump(init_settings(path)).sub(/\A---\n/, '')}
368
+ #{settings_yaml(path, plugins, paths_suggestion)}
291
369
  YAML
292
370
  end
293
371
 
372
+ # The serialised settings. `plugins: []` stays valid YAML and schema-clean; when `Gemfile.lock` named a modelled
373
+ # gem, the suggestion block sits under it as comments, so nothing loads until the user uncomments a line. The
374
+ # `paths:` suggestion sits under the active `paths:` list the same way, and the active list stays `lib`.
375
+ def settings_yaml(path, plugins, paths_suggestion = nil)
376
+ yaml = YAML.dump(init_settings(path)).sub(/\A---\n/, "")
377
+ yaml = yaml.sub(/^plugins: \[\]\n/) { "plugins: []\n#{detected_plugins_block(plugins)}" } unless plugins.empty?
378
+ return yaml if paths_suggestion.nil?
379
+
380
+ yaml.sub(/^paths:\n(?:- .*\n)+/) { |active| active + paths_suggestion_block(paths_suggestion, path) }
381
+ end
382
+
383
+ # The commented `paths:` suggestion: one line naming what was detected and why, then the value to uncomment, written
384
+ # relative to the config file's directory like the active list (#1399).
385
+ def paths_suggestion_block(suggestion, path)
386
+ named = suggestion.fetch(:dirs).join(", ")
387
+ written = roots_relative_to(suggestion.fetch(:dirs), config_dir_of(path)).join(", ")
388
+ pronoun = suggestion.fetch(:dirs).one? ? "it" : "them"
389
+ "# #{suggestion.fetch(:reason)}: #{named}. To scan #{pronoun}, delete the `paths:` list above and " \
390
+ "uncomment the line below:\n# paths: [#{written}]\n"
391
+ end
392
+
393
+ # The source directories `rigor init` suggests for `paths:`, read from the project root (the cwd, like the plugin
394
+ # detection). A Rails application (config/application.rb) has app/ and lib/, each listed only if it exists. Any
395
+ # other layout gets no suggestion: the template's `lib` already covers it, and no other directory is guessed
396
+ # (#2560). Nil when there is nothing to add to today's `paths: [lib]`.
397
+ def detected_paths
398
+ return nil unless File.file?(RAILS_APPLICATION_FILE)
399
+
400
+ dirs = %w[app lib].select { |dir| File.directory?(dir) }
401
+ return nil if dirs.empty? || dirs == ["lib"]
402
+
403
+ { dirs: dirs, reason: "Detected a Rails app (#{RAILS_APPLICATION_FILE})" }
404
+ end
405
+
406
+ def detected_plugins_block(plugins)
407
+ lines = DETECTED_PLUGINS_HEADER + plugins.map { |gem| "# - #{gem}" }
408
+ lines.map { |line| "#{line}\n" }.join
409
+ end
410
+
294
411
  # The settings `rigor init` writes: the defaults, with `test_paths:` spelled out as the test roots found now, so
295
412
  # the project's config says where its tests are rather than leaving it to auto-detection. The roots are written
296
413
  # relative to the config file's directory, which is what {Configuration.load} resolves them against. `paths:`
@@ -320,7 +437,7 @@ module Rigor
320
437
 
321
438
  # Each root is resolved against the physical cwd (where `rigor` reads it from), then written relative to the config
322
439
  # dir. With the config at the project root, both sides are the same physical directory, so the roots come out as
323
- # they are spelled from it (`lib`, `spec`), which is master's output for the default config.
440
+ # they are spelled from it (`lib`, `spec`).
324
441
  def roots_relative_to(roots, config_dir)
325
442
  roots.map { |root| Pathname(File.absolute_path(root)).relative_path_from(config_dir).to_s }
326
443
  end
@@ -445,11 +562,18 @@ module Rigor
445
562
  # Surfaced because a bare `rigor describe` is the intuitive guess (the onboarding field trial saw it tried and met
446
563
  # "Unknown command").
447
564
  def run_describe
565
+ return print_describe_usage if @argv.intersect?(%w[-h --help])
566
+
448
567
  require_relative "cli/skill_command"
449
568
 
450
569
  CLI::SkillCommand.new(argv: ["describe", *@argv], out: @out, err: @err).run
451
570
  end
452
571
 
572
+ def print_describe_usage
573
+ @out.puts("Usage: rigor describe [--deep]")
574
+ 0
575
+ end
576
+
453
577
  def run_docs
454
578
  require_relative "cli/docs_command"
455
579
 
@@ -480,41 +604,72 @@ module Rigor
480
604
  CLI::UpgradeCommand.new(argv: @argv, out: @out, err: @err).run
481
605
  end
482
606
 
607
+ # The text help is rendered from {Catalogue}, so the command list cannot drift from the dispatch table.
483
608
  def help
484
- <<~HELP
485
- Usage: rigor <command> [options]
486
-
487
- Commands:
488
- check Analyze Ruby source files
489
- init Create a starter .rigor.yml
490
- annotate Print FILE with each line's last-expression type
491
- type-of Print inferred types at FILE:LINE[:COL] positions
492
- lens Map a file's declarations with their types and provenance (experimental)
493
- trace Replay how the engine typed FILE as a terminal animation
494
- type-scan Report Scope#type_of coverage across PATHs
495
- effects Report each method's effect labels, and the committed effect snapshot
496
- (opt-in; effects update/check/diff/explain)
497
- explain Print the description of one or all CheckRules
498
- diff Compare current diagnostics to a saved baseline JSON
499
- baseline Manage the baseline file (baseline generate/regenerate/dump/drift/prune)
500
- sig-gen Emit RBS skeletons inferred from .rb sources
501
- lsp Run the Rigor Language Server (LSP) over stdio
502
- mcp Run the Rigor MCP server over stdio
503
- triage Summarise diagnostics: distribution, hotspots, hints
504
- coverage Report type-precision coverage (precise vs Dynamic ratio)
505
- unused Report unreferenced classes, modules and constants as removal candidates
506
- plugins Report activation status of every configured plugin
507
- plugin Browse bundled plugin source as worked examples (list/path/print/root)
508
- playground Start the browser playground (requires rigor-playground gem)
509
- describe Recommend the next skill for this project (alias for `skill describe`)
510
- skill Recommend the next skill + list/print bundled Agent Skills (skill describe, skill <name>)
511
- docs Print the bundled docs offline (docs <name>, docs --list)
512
- show-bleedingedge Show the bleeding-edge overlay + what your config adopts
513
- doctor Classify setup problems vs clean run with routed next actions
514
- upgrade Migration command skeleton (queued)
515
- version Print the Rigor version
516
- help Print this help
517
- HELP
609
+ Catalogue.help_text
610
+ end
611
+
612
+ # `rigor help [--format=text|json] [<command>]`. `help <command>` behaves as `rigor <command> --help`;
613
+ # `--format=json` prints the whole {Catalogue} document.
614
+ def run_help
615
+ return print_command_list if @argv.intersect?(%w[-h --help])
616
+
617
+ options = parse_help_options
618
+ topics = @argv
619
+ return run_help_json(topics) if options[:format] == "json"
620
+
621
+ topic = topics.first
622
+ return run_help_topic(topic, topics.drop(1)) if topic
623
+
624
+ print_command_list
625
+ end
626
+
627
+ def print_command_list
628
+ @out.puts(help)
629
+ 0
630
+ end
631
+
632
+ def parse_help_options
633
+ options = Hash.new("text")
634
+ # `order!` stops at the first positional, so the flags of the command being asked about are never parsed here.
635
+ self.class.help_option_parser(options).order!(@argv)
636
+ options
637
+ end
638
+
639
+ def run_help_json(topics)
640
+ if topics.any?
641
+ @err.puts("rigor help --format=json takes no command; it prints every command")
642
+ return EXIT_USAGE
643
+ end
644
+
645
+ @out.puts(JSON.pretty_generate(Catalogue.document))
646
+ 0
647
+ end
648
+
649
+ def run_help_topic(topic, rest)
650
+ # Flags after the command name (`help type-of --workers=2`) are not parsed; a second word is not a subcommand's
651
+ # help, so it is refused rather than answered with the parent's.
652
+ extra = rest.reject { |word| word.start_with?("-") }
653
+ unless extra.empty?
654
+ @err.puts("rigor help takes one command, got: #{[topic, *extra].join(' ')}")
655
+ return EXIT_USAGE
656
+ end
657
+
658
+ case topic
659
+ when "help"
660
+ @out.puts(self.class.help_option_parser.help)
661
+ 0
662
+ when "version"
663
+ @out.puts("Usage: rigor version")
664
+ 0
665
+ when *HANDLERS.keys
666
+ @argv = ["--help"]
667
+ dispatch(topic)
668
+ else
669
+ @err.puts("Unknown command: #{topic}")
670
+ @err.puts(help)
671
+ EXIT_USAGE
672
+ end
518
673
  end
519
674
  end
520
675
  end
@@ -35,7 +35,7 @@ module Rigor
35
35
  # (`rbs_extended.*`, `rbs.*`, `dynamic.*`) is left alone, since the engine emits those ids
36
36
  # outside the catalogue.
37
37
  module ConfigAudit
38
- # One config-level finding. `kind` discriminates the source key (`:signature_path`,
38
+ # One config-level finding. `kind` discriminates the source key (`:unknown_key`, `:signature_path`,
39
39
  # `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
40
40
  # `:bundler_bundle_path`, `:bundler_lockfile`, `:rbs_collection_lockfile`); `fields` carries the kind-specific
41
41
  # structured data merged into {#to_h} for JSON consumers.
@@ -15,12 +15,12 @@ module Rigor
15
15
  # rules `:warning`.
16
16
  # - `strict`: Every rule is `:error`. CI-friendly.
17
17
  #
18
- # The profile resolution order:
18
+ # The resolution order, first match wins:
19
19
  #
20
- # 1. Profile-specific entry for the canonical rule id.
21
- # 2. The diagnostic's own authored severity (the rule's default).
22
- # 3. `:error` (catch-all so an unrecognised rule still emits visibly — the public-API drift spec catches
23
- # the bookkeeping gap separately).
20
+ # 1. An explicit `severity_overrides:` entry for the row (`resolve`'s `overrides:`).
21
+ # 2. A bleeding-edge severity override.
22
+ # 3. The profile's entry for the canonical rule id.
23
+ # 4. The diagnostic's own authored severity (the rule's default); an unrecognised rule keeps it.
24
24
  module SeverityProfile
25
25
  VALID_PROFILES = %i[lenient balanced strict].freeze
26
26
  VALID_SEVERITIES = %i[error warning info off].freeze
@@ -62,6 +62,10 @@ module Rigor
62
62
  "disable" => [],
63
63
  "libraries" => [],
64
64
  "signature_paths" => nil,
65
+ # Issue #2517 — a core signature directory loaded IN PLACE OF the `rbs` gem's `core/` (a provider such as
66
+ # rbs-shifter writes one for an older CRuby). `nil` (the default) loads the gem's core. Unlike
67
+ # `signature_paths:`, which only adds, this replaces: the default core is not loaded beside it.
68
+ "core_root" => nil,
65
69
  # The project's test roots: the directories (or files) holding its tests. `nil` (the default) auto-detects
66
70
  # whichever of `spec/` and `test/` exist, the same way `signature_paths: nil` auto-detects `sig/`; `[]`
67
71
  # declares that the project has none. `rigor sig-gen --params=observed` harvests call-site observations
@@ -77,10 +81,9 @@ module Rigor
77
81
  "inline_declared" => "write"
78
82
  },
79
83
  # ADR-17 — project-side monkey-patch pre-evaluation. Empty by default; users opt in by listing explicit
80
- # files that the analyzer walks before per-file inference so patched-method declarations are visible
81
- # across the project (e.g. `lib/core_ext/string_extensions.rb`). Slice 1 plumbing only — listed files
82
- # are validated at config-load time (`pre-eval.file-not-found` on a missing path), but the dispatcher
83
- # tier consuming the registry lands in slice 2.
84
+ # files (or globs) that the analyzer walks before per-file inference so patched-method declarations are
85
+ # visible across the project (e.g. `lib/core_ext/string_extensions.rb`). A missing literal path surfaces
86
+ # as `pre-eval.file-not-found`.
84
87
  "pre_eval" => [],
85
88
  # ADR-22 — baseline file path. nil (default) means no baseline is loaded; the `false` literal is
86
89
  # treated as the explicit-disable form for `.rigor.yml`-side override of an upstream `.rigor.dist.yml`
@@ -229,6 +232,11 @@ module Rigor
229
232
  PATH_KEYS = %w[paths signature_paths test_paths pre_eval].freeze
230
233
  private_constant :PATH_KEYS
231
234
 
235
+ # The single-path sibling of {PATH_KEYS}: resolved the same way, but kept a String rather than wrapped in an
236
+ # Array.
237
+ SCALAR_PATH_KEYS = %w[core_root].freeze
238
+ private_constant :SCALAR_PATH_KEYS
239
+
232
240
  # Top-level keys this implementation DECLARES but never reads — see `docs/internal-spec/config.md`
233
241
  # § "Reserved namespaces" and ADR-99.
234
242
  #
@@ -271,7 +279,8 @@ module Rigor
271
279
 
272
280
  attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :cache_max_bytes,
273
281
  :cache_validation, :disabled_rules,
274
- :libraries, :signature_paths, :test_paths, :fold_platform_specific_paths, :parameter_inference,
282
+ :libraries, :signature_paths, :core_root, :test_paths, :fold_platform_specific_paths,
283
+ :parameter_inference,
275
284
  :plugins_isolation,
276
285
  :plugins_io_network, :plugins_io_allowed_paths,
277
286
  :plugins_io_allowed_url_hosts,
@@ -490,6 +499,7 @@ module Rigor
490
499
 
491
500
  out = data.dup
492
501
  PATH_KEYS.each { |key| resolve_path_key!(out, key, base_dir) }
502
+ SCALAR_PATH_KEYS.each { |key| resolve_scalar_path_key!(out, key, base_dir) }
493
503
  resolve_plugins_io_paths!(out, base_dir)
494
504
  out
495
505
  end
@@ -500,6 +510,12 @@ module Rigor
500
510
  out[key] = Array(out[key]).map { |p| ConfigPath.absolute(p.to_s, base_dir) }
501
511
  end
502
512
 
513
+ def self.resolve_scalar_path_key!(out, key, base_dir)
514
+ return unless out.key?(key) && out[key].is_a?(String)
515
+
516
+ out[key] = ConfigPath.absolute(out[key], base_dir)
517
+ end
518
+
503
519
  def self.resolve_plugins_io_paths!(out, base_dir)
504
520
  plugins_io = out["plugins_io"]
505
521
  return unless plugins_io.is_a?(Hash) && plugins_io["allowed_paths"]
@@ -582,6 +598,7 @@ module Rigor
582
598
  @libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
583
599
  sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
584
600
  @signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
601
+ @core_root = coerce_core_root(data.fetch("core_root", DEFAULTS.fetch("core_root")))
585
602
  test_paths = data.fetch("test_paths", DEFAULTS.fetch("test_paths"))
586
603
  @test_paths = test_paths.nil? ? nil : Array(test_paths).map(&:to_s).freeze
587
604
  @sig_gen_inline_declared = coerce_sig_gen_inline_declared(data.fetch("sig_gen", nil))
@@ -595,18 +612,13 @@ module Rigor
595
612
  # ADR-67 WD6a — resolve to a strict Boolean so a truthy non-`true` value (e.g. a stray String) does not
596
613
  # silently enable the gate; only the literal `true` activates the check-walk collector pre-pass.
597
614
  @parameter_inference = data.fetch("parameter_inference", DEFAULTS.fetch("parameter_inference")) == true
598
- # Resolved before `@effects` — ADR-103 WD15's "effects-on-by-default" reads {#bleeding_edge_active?},
599
- # which reads `@bleeding_edge_active_ids`.
600
- @bleeding_edge = coerce_bleeding_edge(
601
- data.fetch("bleeding_edge", DEFAULTS.fetch("bleeding_edge"))
602
- )
603
- @bleeding_edge_severity_overrides = BleedingEdge.severity_overrides_for(@bleeding_edge)
604
- @bleeding_edge_active_ids = BleedingEdge.active_ids_for(@bleeding_edge)
605
615
  # ADR-103 WD13 — presence, not truthiness: `effects:` written with no body parses as nil and still
606
- # means "on", so the key's presence in the loaded data is what {#effects_enabled?} reads.
607
- @effects = coerce_effects(data, effects_key_present: effects_key_present)
608
- coerce_effects_snapshot(@effects)
609
- coerce_effects_policy(@effects)
616
+ # means "on", so the key's presence in the loaded data is what {#effects_enabled?} reads. The raw
617
+ # input is kept so {#with_bleeding_edge} re-derives the effects block from the same source.
618
+ @effects_key_present = effects_key_present
619
+ @effects_written = Ractor.make_shareable(data.fetch("effects", false))
620
+ derive_bleeding_edge(data.fetch("bleeding_edge", DEFAULTS.fetch("bleeding_edge")))
621
+ derive_effects
610
622
  @cache_path = cache.fetch("path").to_s
611
623
  raw_max = cache.fetch("max_bytes")
612
624
  @cache_max_bytes = raw_max.nil? ? nil : Integer(raw_max)
@@ -672,6 +684,7 @@ module Rigor
672
684
  "disable" => disabled_rules,
673
685
  "libraries" => libraries,
674
686
  "signature_paths" => signature_paths,
687
+ "core_root" => core_root,
675
688
  # `test_paths:` is deliberately absent, as `effects:` is: `to_h` feeds the run cache key and the
676
689
  # incremental-snapshot fingerprint, and the test roots change no diagnostic, so editing them must not
677
690
  # invalidate either. `sig_gen:` is absent for the same reason: only `rigor sig-gen` reads it.
@@ -752,15 +765,14 @@ module Rigor
752
765
  # It is a frozen `dup` with the bleeding-edge ivars re-set: `dup` returns an unfrozen shallow copy
753
766
  # (every other ivar is the receiver's deeply-frozen value, safe to share read-only), the replacements
754
767
  # are themselves deeply frozen, and the re-`freeze` keeps the result `Ractor.shareable?` for the worker
755
- # path. **Every ivar `#initialize` derives from `@bleeding_edge` has to be re-derived here** — the
756
- # selector, the severity map, and the active-id set the {#bleeding_edge_active?} predicate reads.
768
+ # path. **Every ivar `#initialize` derives from the bleeding-edge selector has to be re-derived here** — it
769
+ # calls the same {#derive_bleeding_edge} and {#derive_effects} that `#initialize` does, so the selector,
770
+ # the severity map, the active-id set and the `effects:` default (ADR-103 WD15's "effects-on-by-default")
771
+ # all follow the new value, and a derived ivar added to `#initialize` is picked up without a second edit.
757
772
  def with_bleeding_edge(value)
758
- selector = coerce_bleeding_edge(value)
759
773
  copy = dup
760
- copy.instance_variable_set(:@bleeding_edge, selector)
761
- copy.instance_variable_set(:@bleeding_edge_severity_overrides,
762
- BleedingEdge.severity_overrides_for(selector))
763
- copy.instance_variable_set(:@bleeding_edge_active_ids, BleedingEdge.active_ids_for(selector))
774
+ copy.send(:derive_bleeding_edge, value)
775
+ copy.send(:derive_effects)
764
776
  copy.freeze
765
777
  end
766
778
 
@@ -794,19 +806,38 @@ module Rigor
794
806
  #
795
807
  # Same `dup` + re-`freeze` shape as {#with_bleeding_edge}: every other ivar is the receiver's deeply
796
808
  # frozen value, safe to share read-only, and the result stays `Ractor.shareable?` for the worker path.
809
+ # The implicit block is recorded as a written empty one, so a later {#with_bleeding_edge} re-derives
810
+ # it instead of falling back to the absent-key default and dropping it.
797
811
  def with_effects_enabled
798
812
  return self if effects_enabled?
799
813
 
800
814
  copy = dup
801
- copy.instance_variable_set(:@effects, {}.freeze)
802
- # The implicit block defaults like a written one. `rigor effects` emits no diagnostic either way,
803
- # but the two configurations must not differ in a field a later reader could branch on.
804
- copy.instance_variable_set(:@effects_check, true)
815
+ copy.instance_variable_set(:@effects_key_present, true)
816
+ copy.instance_variable_set(:@effects_written, {}.freeze)
817
+ copy.send(:derive_effects)
805
818
  copy.freeze
806
819
  end
807
820
 
808
821
  private
809
822
 
823
+ # ADR-50 § WD2 / ADR-103 WD15 — derives the bleeding-edge selector and the two ivars the
824
+ # {#bleeding_edge_active?} predicate and the severity resolution read. `#initialize` and
825
+ # {#with_bleeding_edge} both call this, so there is one derivation to keep in step.
826
+ def derive_bleeding_edge(value)
827
+ @bleeding_edge = coerce_bleeding_edge(value)
828
+ @bleeding_edge_severity_overrides = BleedingEdge.severity_overrides_for(@bleeding_edge)
829
+ @bleeding_edge_active_ids = BleedingEdge.active_ids_for(@bleeding_edge)
830
+ end
831
+
832
+ # ADR-103 WD13 / WD15 — derives the `effects:` block and its snapshot and policy ivars from the raw
833
+ # `effects:` input the instance kept (`@effects_written`, `@effects_key_present`). The effects-on-by-default
834
+ # default reads the active-id set, so {#derive_bleeding_edge} must run first.
835
+ def derive_effects
836
+ @effects = coerce_effects(@effects_written, effects_key_present: @effects_key_present)
837
+ coerce_effects_snapshot(@effects)
838
+ coerce_effects_policy(@effects)
839
+ end
840
+
810
841
  # ADR-103 WD13 — the `effects:` block, or nil when the key is absent or explicitly `false`. A
811
842
  # present-but-empty key (`effects:` alone, which YAML parses as nil) and `effects: {}` are the same
812
843
  # thing: collection on, every sub-key defaulted. A non-Hash body is normalised to `{}` rather than
@@ -818,8 +849,7 @@ module Rigor
818
849
  # as `effects: {}` when the "effects-on-by-default" bleeding-edge feature is adopted. An explicit
819
850
  # `effects: false` — `effects_key_present == true` — always stays off regardless of the feature, and a
820
851
  # written block always wins outright: the feature can only fill an *absence*.
821
- def coerce_effects(data, effects_key_present:)
822
- value = data.fetch("effects", false)
852
+ def coerce_effects(value, effects_key_present:)
823
853
  return {}.freeze if value == false && !effects_key_present && effects_forced_on_by_bleeding_edge?
824
854
  return nil if value == false
825
855
 
@@ -1054,7 +1084,7 @@ module Rigor
1054
1084
  end
1055
1085
  end
1056
1086
 
1057
- # `target_ruby` is passed to `Prism.parse_file(path, version:)` at the analyser's three parse sites
1087
+ # `target_ruby` is passed as Prism's `version:` option at the analyser's parse sites
1058
1088
  # (`Analysis::Runner`, `CLI::TypeOfCommand`, `CLI::TypeScanCommand`) so projects that target an older
1059
1089
  # Ruby get parse errors for syntax their target doesn't support. Format validation here is loose —
1060
1090
  # accepts any `<major>.<minor>` or `<major>.<minor>.<patch>` form, plus the literal `"latest"`. Prism
@@ -1097,6 +1127,18 @@ module Rigor
1097
1127
  # ADR-22 WD2 (b) — `baseline: <path>` activates the file; `baseline: false` is the explicit-disable form
1098
1128
  # (useful in `.rigor.yml` to override an upstream `.rigor.dist.yml` that names a baseline). `nil`
1099
1129
  # (default / absent key) is also "no baseline".
1130
+ # Issue #2517 — the directory's existence and contents are checked where the environment is built
1131
+ # ({Environment::CoreRoot.validate!}), not here: `rigor init`, `rigor doctor` and the language server's reload
1132
+ # load a configuration without building an environment, and must still be able to read one naming a core that
1133
+ # is not generated yet.
1134
+ def coerce_core_root(value)
1135
+ return nil if value.nil?
1136
+ raise ConfigurationError, "core_root must be a directory path, got #{value.inspect}" unless value.is_a?(String)
1137
+ raise ConfigurationError, "core_root must not be empty" if value.strip.empty?
1138
+
1139
+ value.dup.freeze
1140
+ end
1141
+
1100
1142
  def coerce_baseline_path(value)
1101
1143
  return nil if value.nil? || value == false
1102
1144
 
@@ -58,11 +58,9 @@ module Rigor
58
58
  node.is_a?(Prism::SingletonClassNode) || (node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode))
59
59
  end
60
60
 
61
- # A class body.
62
61
  CLASS_BODY = of(:class, false)
63
62
  # A `class << self` body: `self` and the definee are both the singleton class.
64
63
  SINGLETON_CLASS_BODY = of(:singleton_class, true)
65
- # The body of an instance method of the class.
66
64
  INSTANCE_METHOD_BODY = of(:instance, false)
67
65
  # The top level, where `self` is `main`, an instance of `Object`, and a `def` defines `Object#m`.
68
66
  TOP_LEVEL = INSTANCE_METHOD_BODY
@@ -8,7 +8,7 @@ module Rigor
8
8
  # An author-declared upper bound on one method's effect labels (ADR-103 WD1; normative in
9
9
  # `docs/type-specification/effect-labels.md` § Effect envelopes).
10
10
  #
11
- # Four facts and one seam:
11
+ # Its facts:
12
12
  #
13
13
  # - {#owner_key} — the method key the envelope binds (`Class#m` / `Class.m`). A class-level
14
14
  # envelope is read once and {#rebind}ed onto each method of that class discovery knows.
@@ -69,7 +69,7 @@ module Rigor
69
69
  VALIDATION_TRIGGERS = %w[valid? invalid? validate!].freeze
70
70
  SINGLETON_SAVE_TRIGGERS = %w[create create!].freeze
71
71
 
72
- # `initialize` is not a mailer action, and neither is a method a plugin would want double-counted.
72
+ # `initialize` is not a mailer action.
73
73
  NON_ACTION_METHODS = %w[initialize].to_set.freeze
74
74
 
75
75
  module_function