rigortype 0.4.6 → 0.4.8

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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
data/lib/rigor/cli.rb CHANGED
@@ -10,6 +10,9 @@ require_relative "version"
10
10
  require_relative "analysis/diagnostic"
11
11
  require_relative "analysis/result"
12
12
  require_relative "cli/options"
13
+ require "did_you_mean"
14
+
15
+ require_relative "cli/catalogue"
13
16
  require_relative "cli/diagnostic_formats"
14
17
  require_relative "ci_detector"
15
18
  require_relative "analysis/check_rules/rule_ids"
@@ -30,8 +33,11 @@ module Rigor
30
33
  # actionable": `SystemStackError` from an ancestry walk that never terminates, `NoMemoryError` from a
31
34
  # runaway allocation. Neither is a `StandardError`, so every rescue between here and the raise site
32
35
  # declines them by design — the whole point of those rescues is to convert a per-file or per-plugin
33
- # failure into a diagnostic, and a process running out of stack is not per-file. Left uncaught they
34
- # abort with Ruby's own ~40-frame dump, which is non-zero (so a CI gate does catch it) but says
36
+ # failure into a diagnostic. Since #2455 the per-file phases (`Runner#analyze_file_body`, the worker session,
37
+ # the discovery loops) do contain a `SystemStackError` raised from Ruby code, as that file's own crash row;
38
+ # what still reaches here is one raised from C (`Prism.parse` of a source nested near Prism's cap, which has
39
+ # no backtrace and leaves no stack to recover on), one outside a per-file phase, and `NoMemoryError`. Left
40
+ # uncaught they abort with Ruby's own ~40-frame dump, which is non-zero (so a CI gate does catch it) but says
35
41
  # neither that Rigor is at fault nor that the report on disk is empty. `Interrupt`, `SignalException`
36
42
  # and `SystemExit` stay out: those are the user's answer, not a failure.
37
43
  FATAL_ERRORS = [SystemStackError, NoMemoryError].freeze
@@ -46,6 +52,17 @@ module Rigor
46
52
  # 2026-07-17 and nothing noticed. See `docs/internal-spec/config.md`.
47
53
  CONFIG_SCHEMA_URL = "https://github.com/rigortype/rigor/raw/master/schemas/rigor-config.schema.json"
48
54
 
55
+ # The comment `rigor init` writes under `plugins: []` when Gemfile.lock names a gem a bundled plugin models. The
56
+ # plugins are suggested, not enabled: ADR-96 Criterion 2 keeps a lockfile's presence from opting a project into a
57
+ # plugin's code, so the user uncomments the ones they want (#2536).
58
+ DETECTED_PLUGINS_HEADER = [
59
+ "# Detected from Gemfile.lock (suggested, not enabled). To load them, delete the `[]` above, uncomment the",
60
+ "# entries you want, then run `rigor plugins --strict`."
61
+ ].freeze
62
+
63
+ # The file whose presence marks a Rails application for `rigor init`'s `paths:` suggestion (#2560).
64
+ RAILS_APPLICATION_FILE = "config/application.rb"
65
+
49
66
  HANDLERS = {
50
67
  "check" => :run_check,
51
68
  "init" => :run_init,
@@ -79,6 +96,26 @@ module Rigor
79
96
  new(argv.dup, out: out, err: err).run
80
97
  end
81
98
 
99
+ # The parser `rigor init` parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
100
+ def self.init_option_parser(options = {})
101
+ OptionParser.new do |opts|
102
+ opts.banner = "Usage: rigor init [options]"
103
+ opts.on("--force", "Overwrite an existing configuration file") { options[:force] = true }
104
+ opts.on("--path=PATH", "Configuration file path") { |value| options[:path] = value }
105
+ opts.on("--no-plugins", "Write no commented plugin suggestions from Gemfile.lock") do
106
+ options[:plugins] = false
107
+ end
108
+ end
109
+ end
110
+
111
+ # The parser `rigor help` parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
112
+ def self.help_option_parser(options = {})
113
+ OptionParser.new do |opts|
114
+ opts.banner = "Usage: rigor help [--format=text|json] [<command>]"
115
+ Options.add_format(opts, options, "Output format: text (default) or json")
116
+ end
117
+ end
118
+
82
119
  def initialize(argv = ARGV.dup, out: $stdout, err: $stderr)
83
120
  @argv = argv
84
121
  @out = out
@@ -89,12 +126,14 @@ module Rigor
89
126
  command = @argv.shift
90
127
 
91
128
  case command
92
- when nil, "help", "-h", "--help"
129
+ when nil, "-h", "--help"
93
130
  @out.puts(help)
94
131
  0
132
+ when "help"
133
+ run_help
95
134
  when "version", "-v", "--version"
96
- @out.puts("rigor #{Rigor::VERSION}")
97
- 0
135
+ run_version
136
+
98
137
  else
99
138
  dispatch(command)
100
139
  end
@@ -139,6 +178,8 @@ module Rigor
139
178
  return arm_jit_deadline { send(handler) } if handler
140
179
 
141
180
  @err.puts("Unknown command: #{command}")
181
+ nearest = ::DidYouMean::SpellChecker.new(dictionary: Catalogue.names).correct(command).first
182
+ @err.puts("Did you mean `rigor #{nearest}`?") if nearest
142
183
  @err.puts(help)
143
184
  EXIT_USAGE
144
185
  end
@@ -156,6 +197,16 @@ module Rigor
156
197
  yield
157
198
  end
158
199
 
200
+ # `rigor version` and `-v`/`--version` print the version; `rigor version --help` prints the usage instead.
201
+ def run_version
202
+ if @argv.intersect?(%w[-h --help])
203
+ @out.puts("Usage: rigor version")
204
+ else
205
+ @out.puts("rigor #{Rigor::VERSION}")
206
+ end
207
+ 0
208
+ end
209
+
159
210
  def run_check
160
211
  require_relative "cli/check_command"
161
212
 
@@ -168,15 +219,11 @@ module Rigor
168
219
  # present; no implicit merge).
169
220
  options = {
170
221
  force: false,
171
- path: ".rigor.dist.yml"
222
+ path: ".rigor.dist.yml",
223
+ plugins: true
172
224
  }
173
225
 
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)
226
+ self.class.init_option_parser(options).parse!(@argv)
180
227
 
181
228
  path = options.fetch(:path)
182
229
  if File.exist?(path) && !options.fetch(:force)
@@ -184,24 +231,57 @@ module Rigor
184
231
  return 1
185
232
  end
186
233
 
187
- File.write(path, init_template(path))
234
+ plugins = options.fetch(:plugins) ? detected_plugins : []
235
+ paths_suggestion = detected_paths
236
+ begin
237
+ File.write(path, init_template(path, plugins: plugins, paths_suggestion: paths_suggestion))
238
+ rescue SystemCallError => e
239
+ # A destination whose directory is missing or not writable is a bad path argument, the same usage error
240
+ # `--config=PATH` gives for a file that does not exist (exit 64, docs/manual/02-cli-reference.md). The bare
241
+ # Errno::ENOENT backtrace read as an internal crash.
242
+ reason = e.message.sub(/ @ .*\z/m, "")
243
+ @err.puts("rigor init: cannot write #{path}: #{reason}")
244
+ return EXIT_USAGE
245
+ end
188
246
  @out.puts("Created #{path}")
189
- print_init_next_steps(path)
247
+ print_init_next_steps(path, plugins, paths_suggestion)
190
248
  0
191
249
  end
192
250
 
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.
251
+ # The bundled plugins whose target gems the project's `Gemfile.lock` declares: the read `rigor doctor` makes,
252
+ # through {PluginGapAdvisory}, so the two cannot disagree about what the project depends on. Empty without a
253
+ # lockfile, which leaves the template as it was before detection existed. The advisory is required lazily:
254
+ # it loads every bundled plugin's entry file, a cost a plain `rigor init` in a directory with no lockfile never
255
+ # pays.
256
+ def detected_plugins
257
+ require_relative "plugin_gap_advisory"
258
+
259
+ PluginGapAdvisory.gaps(project_root: Dir.pwd, plugins: []).map(&:plugin_gem)
260
+ end
261
+
262
+ # `rigor init`'s template lists the plugins Gemfile.lock detected, or ships empty `plugins:` so a fresh init has
263
+ # nothing to validate — but the moment the user adds any plugin entry, the activation-failure surfaces enumerated
264
+ # in `rigor plugins`'s docstring become real.
195
265
  # Point them at the verification command + the canonical readiness flow so silent failures (the cwd / Gemfile /
196
266
  # signature_paths mismatches that surfaced during the Mastodon trial) get caught the first time the user wires a
197
267
  # plugin, not the first time `rigor check` reports false positives that should have been covered.
198
- def print_init_next_steps(path)
268
+ def print_init_next_steps(path, plugins = [], paths_suggestion = nil)
199
269
  @out.puts ""
200
270
  @out.puts "Next steps:"
201
- @out.puts " 1. Edit #{path} — add the `plugins:` your project needs."
271
+ if plugins.empty?
272
+ @out.puts " 1. Edit #{path} — add the `plugins:` your project needs."
273
+ else
274
+ @out.puts " 1. Edit #{path} — to load a detected plugin, delete the `[]` on the `plugins:` line and"
275
+ @out.puts " uncomment the entries you want:"
276
+ plugins.each { |gem| @out.puts " #{gem}" }
277
+ end
202
278
  @out.puts " 2. Run `rigor plugins` to verify every configured plugin loads."
203
279
  @out.puts " (`--strict` exits 1 on failure; ideal CI gate.)"
204
280
  @out.puts " 3. Run `rigor check` to analyse your code."
281
+ return if paths_suggestion.nil?
282
+
283
+ @out.puts " `rigor check` analyses only `lib` until you edit `paths:` in #{path}. " \
284
+ "The suggestion is commented beneath it."
205
285
  end
206
286
 
207
287
  # Word-wraps `CheckRules::ALL_RULES` into a comma-separated, `#`-commented block for `init_template`, so the
@@ -230,8 +310,9 @@ module Rigor
230
310
 
231
311
  # Renders the starter `.rigor.yml` body. The template serialises `Configuration::DEFAULTS` (so the on-disk file
232
312
  # 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")
313
+ # most likely to want to edit. `plugins:` stays the empty list the defaults ship; detected gems appear beneath it as
314
+ # comments, so a project with no lockfile gets the template it always did.
315
+ def init_template(path = ".rigor.dist.yml", plugins: [], paths_suggestion: nil)
235
316
  <<~YAML
236
317
  # yaml-language-server: $schema=#{CONFIG_SCHEMA_URL}
237
318
  # Rigor configuration. The full key reference is
@@ -287,10 +368,49 @@ module Rigor
287
368
  # `target_ruby` is left commented: the default only picks the syntax Rigor parses. Set it to the lowest
288
369
  # Ruby your project runs on, and Rigor also reports what that Ruby deprecates.
289
370
  # target_ruby: #{Configuration::DEFAULTS.fetch('target_ruby').dump}
290
- #{YAML.dump(init_settings(path)).sub(/\A---\n/, '')}
371
+ #{settings_yaml(path, plugins, paths_suggestion)}
291
372
  YAML
292
373
  end
293
374
 
375
+ # The serialised settings. `plugins: []` stays valid YAML and schema-clean; when `Gemfile.lock` named a modelled
376
+ # gem, the suggestion block sits under it as comments, so nothing loads until the user uncomments a line. The
377
+ # `paths:` suggestion sits under the active `paths:` list the same way, and the active list stays `lib`.
378
+ def settings_yaml(path, plugins, paths_suggestion = nil)
379
+ yaml = YAML.dump(init_settings(path)).sub(/\A---\n/, "")
380
+ yaml = yaml.sub(/^plugins: \[\]\n/) { "plugins: []\n#{detected_plugins_block(plugins)}" } unless plugins.empty?
381
+ return yaml if paths_suggestion.nil?
382
+
383
+ yaml.sub(/^paths:\n(?:- .*\n)+/) { |active| active + paths_suggestion_block(paths_suggestion, path) }
384
+ end
385
+
386
+ # The commented `paths:` suggestion: one line naming what was detected and why, then the value to uncomment, written
387
+ # relative to the config file's directory like the active list (#1399).
388
+ def paths_suggestion_block(suggestion, path)
389
+ named = suggestion.fetch(:dirs).join(", ")
390
+ written = roots_relative_to(suggestion.fetch(:dirs), config_dir_of(path)).join(", ")
391
+ pronoun = suggestion.fetch(:dirs).one? ? "it" : "them"
392
+ "# #{suggestion.fetch(:reason)}: #{named}. To scan #{pronoun}, delete the `paths:` list above and " \
393
+ "uncomment the line below:\n# paths: [#{written}]\n"
394
+ end
395
+
396
+ # The source directories `rigor init` suggests for `paths:`, read from the project root (the cwd, like the plugin
397
+ # detection). A Rails application (config/application.rb) has app/ and lib/, each listed only if it exists. Any
398
+ # other layout gets no suggestion: the template's `lib` already covers it, and no other directory is guessed
399
+ # (#2560). Nil when there is nothing to add to today's `paths: [lib]`.
400
+ def detected_paths
401
+ return nil unless File.file?(RAILS_APPLICATION_FILE)
402
+
403
+ dirs = %w[app lib].select { |dir| File.directory?(dir) }
404
+ return nil if dirs.empty? || dirs == ["lib"]
405
+
406
+ { dirs: dirs, reason: "Detected a Rails app (#{RAILS_APPLICATION_FILE})" }
407
+ end
408
+
409
+ def detected_plugins_block(plugins)
410
+ lines = DETECTED_PLUGINS_HEADER + plugins.map { |gem| "# - #{gem}" }
411
+ lines.map { |line| "#{line}\n" }.join
412
+ end
413
+
294
414
  # The settings `rigor init` writes: the defaults, with `test_paths:` spelled out as the test roots found now, so
295
415
  # the project's config says where its tests are rather than leaving it to auto-detection. The roots are written
296
416
  # relative to the config file's directory, which is what {Configuration.load} resolves them against. `paths:`
@@ -320,7 +440,7 @@ module Rigor
320
440
 
321
441
  # Each root is resolved against the physical cwd (where `rigor` reads it from), then written relative to the config
322
442
  # 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.
443
+ # they are spelled from it (`lib`, `spec`).
324
444
  def roots_relative_to(roots, config_dir)
325
445
  roots.map { |root| Pathname(File.absolute_path(root)).relative_path_from(config_dir).to_s }
326
446
  end
@@ -445,11 +565,18 @@ module Rigor
445
565
  # Surfaced because a bare `rigor describe` is the intuitive guess (the onboarding field trial saw it tried and met
446
566
  # "Unknown command").
447
567
  def run_describe
568
+ return print_describe_usage if @argv.intersect?(%w[-h --help])
569
+
448
570
  require_relative "cli/skill_command"
449
571
 
450
572
  CLI::SkillCommand.new(argv: ["describe", *@argv], out: @out, err: @err).run
451
573
  end
452
574
 
575
+ def print_describe_usage
576
+ @out.puts("Usage: rigor describe [--deep]")
577
+ 0
578
+ end
579
+
453
580
  def run_docs
454
581
  require_relative "cli/docs_command"
455
582
 
@@ -480,41 +607,72 @@ module Rigor
480
607
  CLI::UpgradeCommand.new(argv: @argv, out: @out, err: @err).run
481
608
  end
482
609
 
610
+ # The text help is rendered from {Catalogue}, so the command list cannot drift from the dispatch table.
483
611
  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
612
+ Catalogue.help_text
613
+ end
614
+
615
+ # `rigor help [--format=text|json] [<command>]`. `help <command>` behaves as `rigor <command> --help`;
616
+ # `--format=json` prints the whole {Catalogue} document.
617
+ def run_help
618
+ return print_command_list if @argv.intersect?(%w[-h --help])
619
+
620
+ options = parse_help_options
621
+ topics = @argv
622
+ return run_help_json(topics) if options[:format] == "json"
623
+
624
+ topic = topics.first
625
+ return run_help_topic(topic, topics.drop(1)) if topic
626
+
627
+ print_command_list
628
+ end
629
+
630
+ def print_command_list
631
+ @out.puts(help)
632
+ 0
633
+ end
634
+
635
+ def parse_help_options
636
+ options = Hash.new("text")
637
+ # `order!` stops at the first positional, so the flags of the command being asked about are never parsed here.
638
+ self.class.help_option_parser(options).order!(@argv)
639
+ options
640
+ end
641
+
642
+ def run_help_json(topics)
643
+ if topics.any?
644
+ @err.puts("rigor help --format=json takes no command; it prints every command")
645
+ return EXIT_USAGE
646
+ end
647
+
648
+ @out.puts(JSON.pretty_generate(Catalogue.document))
649
+ 0
650
+ end
651
+
652
+ def run_help_topic(topic, rest)
653
+ # Flags after the command name (`help type-of --workers=2`) are not parsed; a second word is not a subcommand's
654
+ # help, so it is refused rather than answered with the parent's.
655
+ extra = rest.reject { |word| word.start_with?("-") }
656
+ unless extra.empty?
657
+ @err.puts("rigor help takes one command, got: #{[topic, *extra].join(' ')}")
658
+ return EXIT_USAGE
659
+ end
660
+
661
+ case topic
662
+ when "help"
663
+ @out.puts(self.class.help_option_parser.help)
664
+ 0
665
+ when "version"
666
+ @out.puts("Usage: rigor version")
667
+ 0
668
+ when *HANDLERS.keys
669
+ @argv = ["--help"]
670
+ dispatch(topic)
671
+ else
672
+ @err.puts("Unknown command: #{topic}")
673
+ @err.puts(help)
674
+ EXIT_USAGE
675
+ end
518
676
  end
519
677
  end
520
678
  end
@@ -3,7 +3,10 @@
3
3
  require "did_you_mean"
4
4
 
5
5
  require_relative "config_path"
6
+ require_relative "environment/bundle_sig_discovery"
7
+ require_relative "environment/rbs_collection_discovery"
6
8
  require_relative "flow_contribution/conflict"
9
+ require_relative "library_probe"
7
10
  require_relative "signature_path_audit"
8
11
  # ADR-87 WD4 — only the pure rule-id table (`ALL_RULES` / `RULE_FAMILIES`) is needed, so require the light
9
12
  # `rule_ids.rb` rather than the engine-pulling `check_rules.rb`; keeps `config_audit` (loaded by every check)
@@ -35,10 +38,10 @@ module Rigor
35
38
  # (`rbs_extended.*`, `rbs.*`, `dynamic.*`) is left alone, since the engine emits those ids
36
39
  # outside the catalogue.
37
40
  module ConfigAudit
38
- # One config-level finding. `kind` discriminates the source key (`:signature_path`,
41
+ # One config-level finding. `kind` discriminates the source key (`:unknown_key`, `:signature_path`,
39
42
  # `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
40
- # `:bundler_bundle_path`, `:bundler_lockfile`, `:rbs_collection_lockfile`); `fields` carries the kind-specific
41
- # structured data merged into {#to_h} for JSON consumers.
43
+ # `:bundler_bundle_path`, `:bundler_lockfile`, `:rbs_collection_lockfile`, `:rbs_collection_library`); `fields`
44
+ # carries the kind-specific structured data merged into {#to_h} for JSON consumers.
42
45
  Warning = Data.define(:kind, :message, :fields) do
43
46
  def to_h
44
47
  { "kind" => kind.to_s, "message" => message }.merge(fields)
@@ -51,7 +54,7 @@ module Rigor
51
54
  unknown_key_warnings(configuration) +
52
55
  signature_path_warnings(configuration) +
53
56
  bundled_plugin_signature_path_warnings(configuration) +
54
- library_warnings(configuration) +
57
+ library_warnings(configuration, project_root) +
55
58
  rule_token_warnings(configuration) +
56
59
  explicit_path_warnings(configuration, project_root)
57
60
  end
@@ -129,21 +132,32 @@ module Rigor
129
132
  end
130
133
  end
131
134
 
132
- # `libraries:` entries RBS does not recognise. Uses the same
133
- # `RBS::EnvironmentLoader#has_library?` guard the loader filters through
134
- # ({Environment::RbsLoader} `build_env_for`), so a flagged name is exactly one the loader
135
- # skipped. Fails soft: if RBS itself raises, no warning rather than a crash.
136
- def self.library_warnings(configuration)
137
- configured = Array(configuration.libraries)
138
- return [] if configured.empty?
139
-
140
- loader = ::RBS::EnvironmentLoader.new
141
- configured.reject { |lib| loader.has_library?(library: lib.to_s, version: nil) }.map do |lib|
142
- Warning.new(
143
- kind: :library,
144
- message: "libraries: #{lib.to_s.inspect} is not an available RBS library (no signatures loaded from it)",
145
- fields: { "name" => lib.to_s }
146
- )
135
+ # `libraries:` entries RBS does not recognise, and — issue #2946 — `source: { type: stdlib }` entries of
136
+ # the run's `rbs_collection.lock.yaml`, which load as libraries too ({Environment::BaseLibraries.resolve}). Uses the
137
+ # same `RBS::EnvironmentLoader#has_library?` answer the loader filters through
138
+ # ({Environment::RbsLoader} `library_loader`), so a flagged name is one the loader skipped. A
139
+ # {Environment::RequiredFeatures::VENDORED_DIRS} name (`prime`) under `libraries:` is not audited: the
140
+ # configuration turns it into a vendored feature token the loader never asks `has_library?` about. A
141
+ # lockfile entry of that name is not a token, so it is still audited. The `rbs` CLI warns on the same
142
+ # lockfile entry (`Collection::Sources::Stdlib#manifest_of`).
143
+ # Fails soft: if RBS itself raises, no warning rather than a crash.
144
+ def self.library_warnings(configuration, project_root = Dir.pwd)
145
+ lock = { lockfile_path: configuration.rbs_collection_lockfile, project_root: project_root,
146
+ auto_detect: configuration.rbs_collection_auto_detect }
147
+ lockfile = Environment::RbsCollectionDiscovery.stdlib_library_names(**lock)
148
+ # Issue #2971 — a `libraries:` name the collection lockfile provides loads from the collection, not as an
149
+ # RBS library, so it is not an unknown library.
150
+ provided = Environment::RbsCollectionDiscovery.collection_gem_names(**lock)
151
+ named = (LibraryProbe.auditable(configuration.libraries) - provided).map { |lib| [:library, "libraries:", lib] } +
152
+ lockfile.map { |name| [:rbs_collection_library, "rbs_collection.lock.yaml: stdlib entry", name] }
153
+ return [] if named.empty?
154
+
155
+ unavailable = LibraryProbe.predicate.then { |has| named.reject { |_, _, lib| has.call(lib) } }
156
+ # Issue #2987 — nor is a name whose own `sig/` the bundle loaded; read only once a name is unavailable.
157
+ bundled = unavailable.empty? ? [] : Environment::BundleSigDiscovery.loaded_names(configuration, project_root)
158
+ unavailable.reject { |kind, _, lib| kind == :library && bundled.include?(lib) }.map do |kind, source, lib|
159
+ Warning.new(kind: kind, fields: { "name" => lib },
160
+ message: "#{source} #{lib.inspect} is not an available RBS library (no signatures loaded from it)")
147
161
  end
148
162
  rescue StandardError
149
163
  []
@@ -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
@@ -86,6 +86,7 @@ module Rigor
86
86
  # a config edit the author may deliberately not want.
87
87
  "effect.annotations-unchecked" => :info,
88
88
  "static.value-use.void" => :off,
89
+ "plugin.api-surface" => :off,
89
90
  # Opt-in author assertion: you only see it if you wrote a
90
91
  # `conforms-to` directive, so it stays a :warning even in
91
92
  # lenient — it is never unsolicited noise.
@@ -131,6 +132,7 @@ module Rigor
131
132
  "suppression.empty" => :warning,
132
133
  "suppression.unknown-marker" => :warning,
133
134
  "static.value-use.void" => :off,
135
+ "plugin.api-surface" => :off,
134
136
  "effect.envelope-exceeded" => :warning,
135
137
  "effect.liskov-widened" => :warning,
136
138
  "effect.unknown-label" => :info,
@@ -173,6 +175,7 @@ module Rigor
173
175
  "suppression.unknown-marker" => :warning,
174
176
  # `:off` even under strict: the gate is `bleeding_edge:`, not the profile (ADR-50 WD1 / ADR-100).
175
177
  "static.value-use.void" => :off,
178
+ "plugin.api-surface" => :off,
176
179
  "effect.envelope-exceeded" => :error,
177
180
  "effect.liskov-widened" => :error,
178
181
  "effect.unknown-label" => :warning,
@@ -234,11 +237,17 @@ module Rigor
234
237
 
235
238
  private_class_method :row_override
236
239
 
240
+ # Rule ids a dot-free family key does not reach. `plugin.api-surface` is the one built-in id under `plugin.`,
241
+ # the prefix of plugin rows, so a `plugin` key must not turn the bleeding-edge rule on (#2822); `disable:
242
+ # [plugin]` does not reach it either. A key naming the id itself still does, in either direction.
243
+ FAMILY_FALLBACK_EXEMPT = ["plugin.api-surface"].freeze
244
+
237
245
  # A family name has no `.` in it, so only a dot-free key can match one. `resolve` runs for every
238
246
  # diagnostic in a run's stream, and most configurations carry no such key (usually no overrides at all),
239
247
  # so the split below is skipped for them rather than allocating a name that nothing can match.
240
248
  def family_override(rule, overrides)
241
249
  return nil unless overrides.any? { |key, _| !key.to_s.include?(".") }
250
+ return nil if FAMILY_FALLBACK_EXEMPT.include?(rule)
242
251
 
243
252
  family = rule.split(".").first
244
253
  return nil if family.nil?