rigortype 0.4.5 → 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 (452) 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 +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -4,6 +4,7 @@ require "json"
4
4
  require "optionparser"
5
5
 
6
6
  require_relative "renderable"
7
+ require_relative "json_document"
7
8
 
8
9
  module Rigor
9
10
  class CLI
@@ -100,7 +101,7 @@ module Rigor
100
101
  end
101
102
 
102
103
  def render_json(report)
103
- @out.puts(JSON.pretty_generate(json_payload(report)))
104
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(json_payload(report))))
104
105
  end
105
106
 
106
107
  def json_payload(report)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optionparser"
4
+ require_relative "json_document"
4
5
  require "json"
5
6
 
6
7
  require_relative "../configuration"
@@ -67,6 +68,23 @@ module Rigor
67
68
  0
68
69
  end
69
70
 
71
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
72
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
73
+ def option_parser(options = {})
74
+ OptionParser.new do |opts|
75
+ opts.banner = USAGE
76
+ Options.add_config(opts, options)
77
+ Options.add_format(opts, options, "Output format: text (default) or json")
78
+ opts.on("--entry-point=GLOB", "Treat declarations in files matching GLOB as roots (repeatable)") do |v|
79
+ options[:entry_points] << v
80
+ end
81
+ opts.on("--limit=N", Integer, "Print at most N candidates (default: all)") { |v| options[:limit] = v }
82
+ # WD5 — refuse rather than silently absorb. A reachability answer is sound only over a full run, and a
83
+ # user who asked for the fast path must not silently get the slow one.
84
+ opts.on("--incremental", "(unsupported)") { options[:incremental] = true }
85
+ end
86
+ end
87
+
70
88
  private
71
89
 
72
90
  # A scan's role and the spelling it records depend on the root and the working directory (#1760): a role
@@ -120,20 +138,8 @@ module Rigor
120
138
 
121
139
  def parse_options
122
140
  options = { config: nil, format: "text", entry_points: [], limit: nil }
123
- parser = OptionParser.new do |opts|
124
- opts.banner = USAGE
125
- Options.add_config(opts, options)
126
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
127
- opts.on("--entry-point=GLOB", "Treat declarations in files matching GLOB as roots (repeatable)") do |v|
128
- options[:entry_points] << v
129
- end
130
- opts.on("--limit=N", Integer, "Print at most N candidates (default: all)") { |v| options[:limit] = v }
131
- # WD5 — refuse rather than silently absorb. A reachability answer is sound only over a full run, and a
132
- # user who asked for the fast path must not silently get the slow one.
133
- opts.on("--incremental", "(unsupported)") { options[:incremental] = true }
134
- end
135
- parser.parse!(@argv)
136
- return usage_error(parser) if options[:incremental]
141
+ option_parser(options).parse!(@argv)
142
+ return usage_error if options[:incremental]
137
143
 
138
144
  options
139
145
  rescue OptionParser::ParseError => e
@@ -141,7 +147,7 @@ module Rigor
141
147
  :usage_error
142
148
  end
143
149
 
144
- def usage_error(_parser)
150
+ def usage_error
145
151
  @err.puts("rigor unused does not support --incremental: reachability is only sound over a whole-project " \
146
152
  "run, so an incremental pass would report constants as unused merely because the files that " \
147
153
  "reference them were served from cache. Re-run without --incremental.")
@@ -379,6 +385,9 @@ module Rigor
379
385
  required_features: ProjectEnvironment.required_features(configuration),
380
386
  **ProjectEnvironment.dependency_discovery_options(configuration))
381
387
  ->(fqn) { !env.singleton_for_name(fqn).nil? }
388
+ rescue Environment::CoreRootError
389
+ # Issue #2517 — answering "unknown" for every name would report against no core at all.
390
+ raise
382
391
  rescue StandardError
383
392
  ->(_fqn) { false }
384
393
  end
@@ -413,7 +422,7 @@ module Rigor
413
422
  end
414
423
 
415
424
  def json(report, options, supply)
416
- JSON.pretty_generate(
425
+ payload = {
417
426
  declared: report.declared, reachable: report.reachable, roots: report.roots, edges: report.edges,
418
427
  namespaces: report.namespaces,
419
428
  plugin_roots: supply.supplied, plugin_roots_unmatched: supply.unmatched,
@@ -421,7 +430,8 @@ module Rigor
421
430
  undecidable: limited(report.undecidable, options).map do |u|
422
431
  { name: u.fqn, path: u.path, line: u.line, reason: u.reason }
423
432
  end
424
- )
433
+ }
434
+ JSON.pretty_generate(JsonDocument.stamp(payload))
425
435
  end
426
436
 
427
437
  def rows_json(rows, options)
@@ -15,6 +15,11 @@ module Rigor
15
15
 
16
16
  # @return CLI exit status.
17
17
  def run
18
+ if @argv.intersect?(%w[-h --help])
19
+ @out.puts(USAGE)
20
+ return 0
21
+ end
22
+
18
23
  @out.puts("rigor upgrade: No migration target available yet (ADR-50 WD7, queued).")
19
24
  @out.puts("Current version: #{Rigor::VERSION}")
20
25
  0
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.
@@ -156,9 +156,8 @@ module Rigor
156
156
  disable = Array(configuration.disabled_rules).filter_map do |token|
157
157
  disable_token_warning(token.to_s, plugin_ids)
158
158
  end
159
- overrides = configuration.severity_overrides.keys.filter_map do |key|
160
- override_key_warning(key.to_s, plugin_ids)
161
- end
159
+ keys = configuration.severity_overrides.keys.map(&:to_s)
160
+ overrides = keys.filter_map { |key| override_key_warning(key, plugin_ids, keys) }
162
161
  disable + overrides
163
162
  end
164
163
 
@@ -172,14 +171,26 @@ module Rigor
172
171
  # `plugin.`-qualified key naming an unconfigured plugin, and any key outside the built-in vocabulary that is
173
172
  # not a qualified plugin id (a plugin's bare rule included), is inert. A reserved run-level row is the home of
174
173
  # this map, so naming one is never flagged; `all` is not a family wildcard here, so it is.
175
- def self.override_key_warning(key, plugin_ids)
176
- reason = override_key_problem(key, plugin_ids)
174
+ def self.override_key_warning(key, plugin_ids, present = [])
175
+ reason = legacy_alias_problem(key, present) || override_key_problem(key, plugin_ids)
177
176
  return nil if reason.nil?
178
177
 
179
178
  Warning.new(kind: :severity_override, message: "severity_overrides: #{key.inspect} #{reason}",
180
179
  fields: { "token" => key })
181
180
  end
182
181
 
182
+ # #2005 — `disable:` expands a legacy alias to its canonical id, but the severity resolver matches canonical ids
183
+ # only, so an alias key reaches no row. Name the canonical id rather than accept the key. `present` holds the
184
+ # keys the map already has: when the canonical id is one of them, the advice to write it is already followed.
185
+ def self.legacy_alias_problem(key, present)
186
+ canonical = Analysis::CheckRules::LEGACY_RULE_ALIASES[key]
187
+ return nil if canonical.nil?
188
+
189
+ reason = "is a legacy alias of `#{canonical}`; the override has no effect"
190
+ present.include?(canonical) ? reason : "#{reason}, so write `#{canonical}`"
191
+ end
192
+ private_class_method :legacy_alias_problem
193
+
183
194
  def self.override_key_problem(key, plugin_ids)
184
195
  return "is not a recognized rule id; the override has no effect" if key == "all"
185
196
  return nil unless Array(Analysis::CheckRules.resolve_rule_token(key)).empty?
@@ -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