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
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "optparse"
4
4
 
5
+ require "did_you_mean"
6
+
5
7
  require_relative "../analysis/baseline"
6
8
  require_relative "options"
7
9
  require_relative "../analysis/runner"
@@ -24,7 +26,9 @@ module Rigor
24
26
  # rigor baseline prune
25
27
  class BaselineCommand < Command # rubocop:disable Metrics/ClassLength
26
28
  EXIT_USAGE = 64
29
+ USAGE = "Usage: rigor baseline <subcommand> [options]"
27
30
  DEFAULT_BASELINE_PATH = ".rigor-baseline.yml"
31
+ MAX_UNBASELINEABLE_FILES = 10
28
32
 
29
33
  SUBCOMMANDS = %w[generate regenerate dump drift prune].freeze
30
34
 
@@ -41,6 +45,8 @@ module Rigor
41
45
  when "prune" then run_prune
42
46
  else
43
47
  @err.puts("Unknown baseline subcommand: #{subcommand.inspect}")
48
+ nearest = ::DidYouMean::SpellChecker.new(dictionary: SUBCOMMANDS).correct(subcommand.to_s).first
49
+ @err.puts("Did you mean `rigor baseline #{nearest}`?") if nearest
44
50
  @err.puts(help)
45
51
  EXIT_USAGE
46
52
  end
@@ -49,10 +55,78 @@ module Rigor
49
55
  EXIT_USAGE
50
56
  end
51
57
 
58
+ # The parser of `subcommand`, built without parsing anything (the catalogue reads it, ADR-126 WD3).
59
+ def option_parser(subcommand, options = {})
60
+ case subcommand
61
+ when "generate", "regenerate" then generate_parser(subcommand, options)
62
+ when "dump" then dump_parser(options)
63
+ when "drift" then drift_parser(options)
64
+ when "prune" then prune_parser(options)
65
+ else raise ArgumentError, "unknown baseline subcommand: #{subcommand.inspect}"
66
+ end
67
+ end
68
+
52
69
  private
53
70
 
71
+ def generate_parser(subcommand, options)
72
+ OptionParser.new do |opts|
73
+ opts.banner = "Usage: rigor baseline #{subcommand} [options]"
74
+ Options.add_config(opts, options)
75
+ opts.on("--output=PATH", "Write baseline to PATH (default: #{DEFAULT_BASELINE_PATH})") do |v|
76
+ options[:output] = v
77
+ end
78
+ opts.on("--match-mode=MODE", %i[rule message],
79
+ "Row form: rule (default) or message") do |v|
80
+ options[:match_mode] = v
81
+ end
82
+ # `regenerate` always overwrites — no `--force` to offer.
83
+ if subcommand == "generate"
84
+ opts.on("--force", "Overwrite an existing baseline file") { options[:force] = true }
85
+ end
86
+ end
87
+ end
88
+
89
+ def dump_parser(options)
90
+ OptionParser.new do |opts|
91
+ opts.banner = "Usage: rigor baseline dump [options]"
92
+ opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
93
+ options[:baseline] = v
94
+ end
95
+ Options.add_format(opts, options, "Output format: text (default) or json")
96
+ opts.on("--rule=RULE", "Filter rows by exact rule id") { |v| options[:rule] = v }
97
+ opts.on("--file=GLOB", "Filter rows by File.fnmatch? glob") { |v| options[:file] = v }
98
+ end
99
+ end
100
+
101
+ def drift_parser(options)
102
+ OptionParser.new do |opts|
103
+ opts.banner = "Usage: rigor baseline drift [options]"
104
+ Options.add_config(opts, options)
105
+ opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
106
+ options[:baseline] = v
107
+ end
108
+ opts.on("--only=STATUS", %i[within over cleared reducible],
109
+ "Show only buckets with the given status (within|over|cleared|reducible)") do |v|
110
+ options[:only] = v
111
+ end
112
+ end
113
+ end
114
+
115
+ def prune_parser(options)
116
+ OptionParser.new do |opts|
117
+ opts.banner = "Usage: rigor baseline prune [options]"
118
+ Options.add_config(opts, options)
119
+ opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
120
+ options[:baseline] = v
121
+ end
122
+ opts.on("--dry-run", "Show what would be dropped without writing the file") { options[:dry_run] = true }
123
+ end
124
+ end
125
+
54
126
  def run_generate
55
127
  options = parse_generate_options
128
+ return positional_paths_error("generate") unless @argv.empty?
129
+
56
130
  path = options.fetch(:output)
57
131
 
58
132
  if File.exist?(path) && !options.fetch(:force)
@@ -69,9 +143,32 @@ module Rigor
69
143
  # so `rigor baseline regenerate` reads cleanly as "make the baseline match reality again".
70
144
  def run_regenerate
71
145
  options = parse_generate_options(subcommand: "regenerate")
146
+ return positional_paths_error("regenerate") unless @argv.empty?
147
+
72
148
  write_baseline(options, verb: "regenerated baseline")
73
149
  end
74
150
 
151
+ # Issues #2466 and #2577 — the baseline covers the configured `paths:`, never the positional arguments: a
152
+ # baseline built over a subset would silently drop the entries of every other file. `drift` and `prune`
153
+ # analyse the configured `paths:` and `dump` prints the whole file, so none of the five reads a path argument.
154
+ # Reject them, as `rigor effects update` does (`effects_snapshot_command.rb:54`), rather than honour them.
155
+ def positional_paths_error(subcommand)
156
+ @err.puts("rigor baseline #{subcommand}: unexpected argument(s) #{@argv.map(&:inspect).join(' ')}; " \
157
+ "#{positional_paths_hint(subcommand)}")
158
+ EXIT_USAGE
159
+ end
160
+
161
+ def positional_paths_hint(subcommand)
162
+ case subcommand
163
+ when "dump"
164
+ "dump reads the baseline file, so pass none: use --file=GLOB to filter rows or --baseline=PATH to pick a file"
165
+ when "drift", "prune"
166
+ "#{subcommand} audits the configured `paths:`, so pass none"
167
+ else
168
+ "the baseline is generated from the configured `paths:`, so pass none"
169
+ end
170
+ end
171
+
75
172
  def write_baseline(options, verb:)
76
173
  path = options.fetch(:output)
77
174
  configuration = Configuration.load(options.fetch(:config))
@@ -81,11 +178,13 @@ module Rigor
81
178
  project_root: Dir.pwd)
82
179
  File.write(path, baseline.to_yaml)
83
180
 
181
+ covered = baseline.buckets.sum(&:count)
84
182
  @err.puts(
85
183
  "rigor: #{verb} #{path} " \
86
- "(#{baseline.size} bucket(s) covering #{diagnostics.size} diagnostic(s); " \
184
+ "(#{baseline.size} bucket(s) covering #{covered} diagnostic(s); " \
87
185
  "match-mode: #{options.fetch(:match_mode)})"
88
186
  )
187
+ report_unbaselineable(Analysis::Baseline.unkeyable(diagnostics))
89
188
  if configuration.baseline_path.nil?
90
189
  # Name the config file that actually governs this project (`.rigor.dist.yml` when no local override
91
190
  # exists), so the hint points at the file the user must edit.
@@ -98,6 +197,38 @@ module Rigor
98
197
  0
99
198
  end
100
199
 
200
+ # Diagnostics the baseline cannot key never enter the file, so the next `rigor check` surfaces them. Say so,
201
+ # with how many, why and where; the exit status stays 0 (the file was written as asked).
202
+ def report_unbaselineable(unkeyable)
203
+ return if unkeyable.empty?
204
+
205
+ no_rule, no_path = unkeyable.partition { |d| d.qualified_rule.nil? }
206
+ files = unbaselineable_files(no_rule)
207
+ parts = []
208
+ unless no_rule.empty?
209
+ parts << "#{no_rule.size} diagnostic(s) have no rule (parse, path or internal errors) " \
210
+ "in #{files.size} file(s)"
211
+ end
212
+ parts << "#{no_path.size} diagnostic(s) have no path" unless no_path.empty?
213
+ listing = files.empty? ? "" : ":"
214
+ @err.puts("rigor: #{parts.join('; ')} and cannot be baselined; " \
215
+ "fix them, or remove or exclude the files#{listing}")
216
+ files.first(MAX_UNBASELINEABLE_FILES).each { |f| @err.puts(" #{f}") }
217
+ extra = files.size - MAX_UNBASELINEABLE_FILES
218
+ @err.puts(" ... and #{extra} more file(s)") if extra.positive?
219
+ end
220
+
221
+ def unbaselineable_files(diagnostics)
222
+ root = Pathname.new(Dir.pwd)
223
+ diagnostics.filter_map(&:path).map { |p| relative_to(root, p) }.uniq.sort
224
+ end
225
+
226
+ def relative_to(root, path)
227
+ Pathname.new(path).relative_path_from(root).to_s
228
+ rescue ArgumentError
229
+ path
230
+ end
231
+
101
232
  def parse_generate_options(subcommand: "generate")
102
233
  options = {
103
234
  config: nil,
@@ -105,22 +236,7 @@ module Rigor
105
236
  match_mode: :rule,
106
237
  force: false
107
238
  }
108
- parser = OptionParser.new do |opts|
109
- opts.banner = "Usage: rigor baseline #{subcommand} [options]"
110
- Options.add_config(opts, options)
111
- opts.on("--output=PATH", "Write baseline to PATH (default: #{DEFAULT_BASELINE_PATH})") do |v|
112
- options[:output] = v
113
- end
114
- opts.on("--match-mode=MODE", %i[rule message],
115
- "Row form: rule (default) or message") do |v|
116
- options[:match_mode] = v
117
- end
118
- # `regenerate` always overwrites — no `--force` to offer.
119
- if subcommand == "generate"
120
- opts.on("--force", "Overwrite an existing baseline file") { options[:force] = true }
121
- end
122
- end
123
- parser.parse!(@argv)
239
+ generate_parser(subcommand, options).parse!(@argv)
124
240
  options
125
241
  end
126
242
 
@@ -139,8 +255,6 @@ module Rigor
139
255
  runner.run(configuration_for_generation.paths).diagnostics
140
256
  end
141
257
 
142
- # The loaded configuration with only `baseline` disabled (#1777): any other key can change the
143
- # diagnostic set, and the baseline must record the set `rigor check` will then report.
144
258
  def override_configuration_baseline_off(configuration)
145
259
  configuration.without_baseline
146
260
  end
@@ -149,31 +263,22 @@ module Rigor
149
263
 
150
264
  def run_dump
151
265
  options = parse_dump_options
266
+ return positional_paths_error("dump") unless @argv.empty?
267
+
152
268
  baseline = load_baseline_or_exit(options.fetch(:baseline))
153
269
  return EXIT_USAGE if baseline == :error
154
270
 
155
271
  rows = filter_dump_rows(baseline.buckets, options)
156
272
  case options.fetch(:format)
157
- when :json then @out.puts(JSON.pretty_generate(dump_to_json(rows)))
273
+ when "json" then @out.puts(JSON.pretty_generate(dump_to_json(rows)))
158
274
  else dump_text(rows, options)
159
275
  end
160
276
  0
161
277
  end
162
278
 
163
279
  def parse_dump_options
164
- options = { baseline: DEFAULT_BASELINE_PATH, format: :text, rule: nil, file: nil }
165
- parser = OptionParser.new do |opts|
166
- opts.banner = "Usage: rigor baseline dump [options]"
167
- opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
168
- options[:baseline] = v
169
- end
170
- opts.on("--format=FORMAT", %i[text json], "Output format: text (default) or json") do |v|
171
- options[:format] = v
172
- end
173
- opts.on("--rule=RULE", "Filter rows by exact rule id") { |v| options[:rule] = v }
174
- opts.on("--file=GLOB", "Filter rows by File.fnmatch? glob") { |v| options[:file] = v }
175
- end
176
- parser.parse!(@argv)
280
+ options = { baseline: DEFAULT_BASELINE_PATH, format: "text", rule: nil, file: nil }
281
+ dump_parser(options).parse!(@argv)
177
282
  options
178
283
  end
179
284
 
@@ -226,6 +331,8 @@ module Rigor
226
331
 
227
332
  def run_drift
228
333
  options = parse_drift_options
334
+ return positional_paths_error("drift") unless @argv.empty?
335
+
229
336
  baseline = load_baseline_or_exit(options.fetch(:baseline))
230
337
  return EXIT_USAGE if baseline == :error
231
338
 
@@ -254,18 +361,7 @@ module Rigor
254
361
  baseline: DEFAULT_BASELINE_PATH,
255
362
  only: nil
256
363
  }
257
- parser = OptionParser.new do |opts|
258
- opts.banner = "Usage: rigor baseline drift [options]"
259
- Options.add_config(opts, options)
260
- opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
261
- options[:baseline] = v
262
- end
263
- opts.on("--only=STATUS", %i[within over cleared reducible],
264
- "Show only buckets with the given status (within|over|cleared|reducible)") do |v|
265
- options[:only] = v
266
- end
267
- end
268
- parser.parse!(@argv)
364
+ drift_parser(options).parse!(@argv)
269
365
  options
270
366
  end
271
367
 
@@ -300,6 +396,8 @@ module Rigor
300
396
 
301
397
  def run_prune
302
398
  options = parse_prune_options
399
+ return positional_paths_error("prune") unless @argv.empty?
400
+
303
401
  baseline = load_baseline_or_exit(options.fetch(:baseline))
304
402
  return EXIT_USAGE if baseline == :error
305
403
 
@@ -328,15 +426,7 @@ module Rigor
328
426
  baseline: DEFAULT_BASELINE_PATH,
329
427
  dry_run: false
330
428
  }
331
- parser = OptionParser.new do |opts|
332
- opts.banner = "Usage: rigor baseline prune [options]"
333
- Options.add_config(opts, options)
334
- opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
335
- options[:baseline] = v
336
- end
337
- opts.on("--dry-run", "Show what would be dropped without writing the file") { options[:dry_run] = true }
338
- end
339
- parser.parse!(@argv)
429
+ prune_parser(options).parse!(@argv)
340
430
  options
341
431
  end
342
432
 
@@ -362,7 +452,7 @@ module Rigor
362
452
 
363
453
  def help
364
454
  <<~HELP
365
- Usage: rigor baseline <subcommand> [options]
455
+ #{USAGE}
366
456
 
367
457
  Subcommands:
368
458
  generate Write a fresh baseline file from a `rigor check` run.
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ require_relative "options"
6
+ require_relative "../version"
7
+ require_relative "json_document"
8
+
9
+ module Rigor
10
+ class CLI
11
+ # The machine-readable description of the CLI surface (ADR-126 WD3): one entry per command, with the one-line
12
+ # summary, usage, subcommands, flags, accepted formats and the MCP tool that mirrors it.
13
+ #
14
+ # Nothing here is a second hand-written list of flags. The commands that parse with an `OptionParser` expose the
15
+ # parser they build (`#option_parser`, built without parsing anything) and the catalogue reads its switches; the
16
+ # commands that read `argv` by hand declare theirs in {DECLARED}. The only prose kept here is the one-line
17
+ # {SUMMARIES}, which `rigor help` prints, so the text help and `rigor help --format=json` cannot disagree about
18
+ # which commands exist. `spec/rigor/cli/catalogue_spec.rb` gates the names against `CLI::HANDLERS`.
19
+ module Catalogue
20
+ SCHEMA_VERSION = JsonDocument::SCHEMA_VERSION
21
+
22
+ # `help` and `version` are dispatched by `CLI#run` itself, outside `CLI::HANDLERS`.
23
+ BUILTINS = %w[help version].freeze
24
+
25
+ # The `rigor help` listing, in print order.
26
+ SUMMARIES = {
27
+ "check" => "Analyze Ruby source files",
28
+ "init" => "Create a starter .rigor.yml",
29
+ "annotate" => "Print FILE with each line's last-expression type",
30
+ "type-of" => "Print inferred types at FILE:LINE[:COL] positions",
31
+ "lens" => "Map a file's declarations with their types and provenance (experimental)",
32
+ "trace" => "Replay how the engine typed FILE as a terminal animation",
33
+ "type-scan" => "Report Scope#type_of coverage across PATHs",
34
+ "effects" => "Report each method's effect labels, and the committed effect snapshot " \
35
+ "(opt-in; effects update/check/diff/explain)",
36
+ "explain" => "Print the description of one or all CheckRules",
37
+ "diff" => "Compare current diagnostics to a saved `rigor check --format=json` result",
38
+ "baseline" => "Manage the baseline file (baseline generate/regenerate/dump/drift/prune)",
39
+ "sig-gen" => "Emit RBS skeletons inferred from .rb sources",
40
+ "lsp" => "Run the Rigor Language Server (LSP) over stdio",
41
+ "mcp" => "Run the Rigor MCP server over stdio",
42
+ "triage" => "Summarise diagnostics: distribution, hotspots, hints",
43
+ "coverage" => "Report type-precision coverage (precise vs Dynamic ratio)",
44
+ "unused" => "Report unreferenced classes, modules and constants as removal candidates",
45
+ "plugins" => "Report activation status of every configured plugin",
46
+ "plugin" => "Browse bundled plugin source as worked examples (plugin <name>, plugin --list)",
47
+ "playground" => "Start the browser playground (requires rigor-playground gem)",
48
+ "describe" => "Recommend the next skill for this project (alias for `skill describe`)",
49
+ "skill" => "Recommend the next skill + list/print bundled Agent Skills (skill describe, skill <name>)",
50
+ "docs" => "Print the bundled docs offline (docs <name>, docs --list)",
51
+ "show-bleedingedge" => "Show the bleeding-edge overlay + what your config adopts",
52
+ "doctor" => "Classify setup problems vs clean run with routed next actions",
53
+ "upgrade" => "Migration command skeleton (queued)",
54
+ "version" => "Print the Rigor version",
55
+ "help" => "Print this help, or a command's help (help <command>, help --format=json)"
56
+ }.freeze
57
+
58
+ # The commands whose `argv` is read by hand, so there is no parser to read: their switches, subcommands and
59
+ # formats are stated here. `playground` is delegated to the optional `rigor-playground` gem and is listed without
60
+ # flags.
61
+ DECLARED = {
62
+ "describe" => {
63
+ usage: "Usage: rigor describe [--deep]",
64
+ flags: [
65
+ { name: "--deep", arg: nil, desc: "Run `rigor check` first and route the recommendation on its result" }
66
+ ]
67
+ },
68
+ "docs" => {
69
+ usage: "Usage: rigor docs [<name>] [--path <name>] [--list [<category>]]",
70
+ flags: [
71
+ { name: "--path", arg: "NAME", desc: "Print the absolute path of a doc" },
72
+ { name: "--list", arg: "CATEGORY", optional: true, desc: "List bundled docs (optionally one category)" }
73
+ ]
74
+ },
75
+ "playground" => { usage: "Usage: rigor playground [options]" },
76
+ "version" => { usage: "Usage: rigor version" }
77
+ }.freeze
78
+
79
+ # The command files that build an `OptionParser`; the command class is the file's name in camel case.
80
+ PARSER_FILES = %w[
81
+ check annotate type_of lens trace type_scan effects explain diff sig_gen lsp mcp baseline triage coverage unused
82
+ plugins show_bleedingedge doctor skill plugin
83
+ ].to_h { |file| [file.tr("_", "-"), "#{file}_command"] }.freeze
84
+
85
+ # The commands that have no parser of their own at the top, so their usage is their `USAGE` constant.
86
+ USAGE_FILES = { "baseline" => "baseline_command", "upgrade" => "upgrade_command" }.freeze
87
+
88
+ Ractor.make_shareable(SUMMARIES)
89
+ Ractor.make_shareable(USAGE_FILES)
90
+ Ractor.make_shareable(DECLARED)
91
+ Ractor.make_shareable(PARSER_FILES)
92
+
93
+ module_function
94
+
95
+ # Every command name the CLI dispatches, in `rigor help` order.
96
+ def names
97
+ SUMMARIES.keys - BUILTINS + BUILTINS
98
+ end
99
+
100
+ # The `rigor help` text, rendered from {SUMMARIES} so its list is the catalogue's.
101
+ def help_text
102
+ width = names.map(&:length).max + 2
103
+ lines = names.map { |name| " #{name.ljust(width)}#{SUMMARIES.fetch(name)}" }
104
+ <<~HELP
105
+ Usage: rigor <command> [options]
106
+
107
+ Commands:
108
+ #{lines.join("\n")}
109
+
110
+ Run `rigor help <command>` (or `rigor <command> --help`) for a command's options.
111
+ Run `rigor describe` for the next step on this project, or `rigor help --format=json` for the full surface.
112
+ HELP
113
+ end
114
+
115
+ # All entries, as plain hashes ready for JSON.
116
+ def entries
117
+ names.map { |name| entry(name) }
118
+ end
119
+
120
+ # The document `rigor help --format=json` prints.
121
+ def document
122
+ { "schema_version" => SCHEMA_VERSION, "version" => Rigor::VERSION, "commands" => entries }
123
+ end
124
+
125
+ def entry(name)
126
+ declared = DECLARED.fetch(name, {})
127
+ parsed = parsed_entry(name)
128
+ {
129
+ "name" => name,
130
+ "summary" => SUMMARIES.fetch(name),
131
+ "usage" => declared[:usage] || parsed[:usage] || command_usage(name),
132
+ "subcommands" => parsed[:subcommands] || declared_subcommands(declared),
133
+ "flags" => parsed[:flags] || declared_flags(declared),
134
+ "formats" => parsed[:formats] || [],
135
+ "mcp_tool" => mcp_tool(name)
136
+ }
137
+ end
138
+
139
+ # The `OptionParser` view of a command: its own flags and formats at the top, and under `subcommands` the same
140
+ # shape for each subcommand's parser. A command with no parser of its own (`baseline`) has empty top-level
141
+ # `flags` and `formats`; none of them is a union over its subcommands.
142
+ def parsed_entry(name)
143
+ pairs = parser_pairs(name)
144
+ return {} if pairs.nil?
145
+
146
+ own = pairs.find { |sub, _| sub.nil? }&.last
147
+ subs = pairs.reject { |sub, _| sub.nil? }
148
+ top = own ? describe_parser(own) : { flags: [], formats: [] }
149
+ return top if subs.empty?
150
+
151
+ top.merge(subcommands: subs.map do |sub, parser|
152
+ { "name" => sub }.merge(stringify_keys(describe_parser(parser)))
153
+ end)
154
+ end
155
+
156
+ # The `[subcommand, parser]` pairs of a command (no name for its own parser), or nil when it has no parser.
157
+ def parser_pairs(name)
158
+ case name
159
+ when "init" then [[nil, CLI.init_option_parser]]
160
+ when "help" then [[nil, CLI.help_option_parser]]
161
+ else command_parser_pairs(name)
162
+ end
163
+ end
164
+
165
+ def command_parser_pairs(name)
166
+ file = PARSER_FILES[name]
167
+ return nil unless file
168
+
169
+ require_relative file
170
+ klass = CLI.const_get(file.split("_").map(&:capitalize).join)
171
+ case name
172
+ when "effects" then effects_parser_pairs(klass)
173
+ when "skill"
174
+ [[nil, klass.new(argv: []).option_parser],
175
+ ["describe", klass.describe_option_parser("Usage: rigor skill describe [--deep]")]]
176
+ when "baseline"
177
+ command = klass.new(argv: [])
178
+ klass::SUBCOMMANDS.map { |sub| [sub, command.option_parser(sub)] }
179
+ else [[nil, klass.new(argv: []).option_parser]]
180
+ end
181
+ end
182
+
183
+ def effects_parser_pairs(klass)
184
+ [[nil, klass.new(argv: []).option_parser]] +
185
+ EffectsSnapshotCommand::VERBS.map do |verb|
186
+ [verb, EffectsSnapshotCommand.new(argv: [], verb: verb).option_parser]
187
+ end
188
+ end
189
+
190
+ # The usage line of a command with no parser to take a banner from, read from the command's own `USAGE`.
191
+ def command_usage(name)
192
+ file = USAGE_FILES[name]
193
+ return nil unless file
194
+
195
+ require_relative file
196
+ CLI.const_get(file.split("_").map(&:capitalize).join)::USAGE
197
+ end
198
+
199
+ def declared_flags(declared)
200
+ (declared[:flags] || []).map do |flag|
201
+ { "name" => flag.fetch(:name), "arg" => flag.fetch(:arg), "optional" => flag.fetch(:optional, false),
202
+ "desc" => flag.fetch(:desc) }
203
+ end
204
+ end
205
+
206
+ def declared_subcommands(declared)
207
+ (declared[:subcommands] || []).map do |sub|
208
+ { "name" => sub.fetch(:name), "usage" => sub.fetch(:usage), "flags" => [], "formats" => [] }
209
+ end
210
+ end
211
+
212
+ # Reads usage, flags and formats from the switches of `parser`.
213
+ def describe_parser(parser)
214
+ flags = parser.top.list.flat_map { |switch| flags_of(switch) }
215
+ format_switch = parser.top.list.any? { |switch| switch.long.first == "--format" }
216
+ { usage: parser.banner.to_s.lines.first&.strip, flags: flags,
217
+ formats: format_switch ? Options.accepted_formats(parser) || [] : [] }
218
+ end
219
+
220
+ # A switch's literal flags: `--[no-]color` is the two flags `--color` and `--no-color`.
221
+ def flags_of(switch)
222
+ long = switch.long.first
223
+ return [] unless long
224
+
225
+ arg, optional = arg_of(switch.arg)
226
+ desc = switch.desc.join(" ")
227
+ flag_names(long).map { |flag| { "name" => flag, "arg" => arg, "optional" => optional, "desc" => desc } }
228
+ end
229
+
230
+ # `[arg, optional]`: `=[LIST]` is the argument `LIST`, optional.
231
+ def arg_of(raw)
232
+ arg = raw&.sub(/\A[= ]/, "")
233
+ optional = !arg.nil? && arg.start_with?("[") && arg.end_with?("]")
234
+ [optional ? arg[1..-2] : arg, optional]
235
+ end
236
+
237
+ def flag_names(long)
238
+ return [long] unless long.start_with?("--[no-]")
239
+
240
+ bare = long.delete_prefix("--[no-]")
241
+ ["--#{bare}", "--no-#{bare}"]
242
+ end
243
+
244
+ # `rigor_check` mirrors `check`, `rigor_sig_gen` mirrors `sig-gen`: a tool is named for its command.
245
+ def mcp_tool(name)
246
+ require_relative "../mcp/server"
247
+ tool = "rigor_#{name.tr('-', '_')}"
248
+ tool if MCP::Server::TOOLS.any? { |candidate| candidate.fetch(:name) == tool }
249
+ end
250
+
251
+ def stringify_keys(hash)
252
+ hash.transform_keys(&:to_s)
253
+ end
254
+ end
255
+ end
256
+ end