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
@@ -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
@@ -17,8 +17,9 @@ require_relative "command"
17
17
  require_relative "options"
18
18
  require_relative "diagnostic_formats"
19
19
  require_relative "../ci_detector"
20
+ require_relative "json_document"
20
21
  # ADR-87 WD4 — `coverage_scan` and `check_runner_factory` both pull the inference engine, so they are required
21
- # lazily (in `compute_coverage` / `build_check_runner`) rather than at load time: an ordinary check whose
22
+ # lazily (in `invoke_check` / `compute_coverage`) rather than at load time: an ordinary check whose
22
23
  # result the run-cache probe serves must reach the hit verdict without `rigor/inference` in $LOADED_FEATURES.
23
24
 
24
25
  module Rigor
@@ -92,6 +93,97 @@ module Rigor
92
93
  exit_code
93
94
  end
94
95
 
96
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
97
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
98
+ def option_parser(options = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
99
+ OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
100
+ opts.banner = "Usage: rigor check [options] [paths]"
101
+ Options.add_config(opts, options)
102
+ Options.add_format(opts, options,
103
+ "Output format: text, json, sarif, github, gitlab, checkstyle, junit, teamcity",
104
+ formats: %w[text json] + DiagnosticFormats::FORMATS)
105
+ opts.on("--explain", "Surface fail-soft fallback events as :info diagnostics") { options[:explain] = true }
106
+ opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
107
+ opts.on("--coverage",
108
+ "Add a type-precision coverage block (an extra precision pass over the analyzed files)") do
109
+ options[:coverage] = true
110
+ end
111
+ opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
112
+ opts.on("--no-cache",
113
+ "Disable the persistent cache for this run (with --incremental: no snapshot is read or written)") do
114
+ options[:no_cache] = true
115
+ end
116
+ opts.on("--[no-]stats",
117
+ "Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
118
+ options[:stats] = value
119
+ end
120
+ opts.on("--workers=N", Integer,
121
+ "Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
122
+ options[:workers] = value
123
+ end
124
+ Options.add_editor_mode(opts, options)
125
+ opts.on("--baseline=PATH",
126
+ "load baseline from PATH (overrides .rigor.yml `baseline:`)") do |value|
127
+ options[:baseline] = value
128
+ end
129
+ opts.on("--no-baseline",
130
+ "ignore any configured baseline for this run") do
131
+ options[:baseline] = false
132
+ end
133
+ opts.on("--baseline-strict",
134
+ "fail the run on any baseline drift (CI gate)") do
135
+ options[:baseline_strict] = true
136
+ end
137
+ opts.on("--fail-on=SEVERITY",
138
+ "exit non-zero on a diagnostic at or above SEVERITY: error (default), warning, or info") do |value|
139
+ # Validated post-parse (`valid_fail_on_option?`, called from `run`) rather than here — see its
140
+ # comment for why an unrecognised value is not raised from inside this block.
141
+ options[:fail_on] = value.to_s.downcase.to_sym
142
+ end
143
+ opts.on("--treat-all-as-inline-rbs",
144
+ "force-load rigor-rbs-inline with require_magic_comment: false") do
145
+ options[:treat_all_as_inline_rbs] = true
146
+ end
147
+ opts.on("--verify-incremental",
148
+ "assert incremental analysis matches a full run, then exit") do
149
+ options[:verify_incremental] = true
150
+ end
151
+ opts.on("--verify-incremental-edits=N", Integer,
152
+ "with --verify-incremental, also replay edits (empty, class/module skeleton, up to 2 " \
153
+ "one-statement drops) on N files") do |value|
154
+ if value.negative?
155
+ raise OptionParser::InvalidArgument, "--verify-incremental-edits=#{value} (must be 0 or more)"
156
+ end
157
+
158
+ options[:verify_incremental] = true
159
+ options[:verify_incremental_edits] = value
160
+ end
161
+ opts.on("--incremental",
162
+ "re-analyze only files changed since the last run (cross-process cache)") do
163
+ options[:incremental] = true
164
+ end
165
+ opts.on("--no-ci-detect",
166
+ "do not auto-emit CI-native output when a CI environment is detected") do
167
+ options[:ci_detect] = false
168
+ end
169
+ # ADR-50 § WD2 — `=[LIST]` (not ` [LIST]`) so a bare `--bleeding-edge` never swallows a following positional
170
+ # path: `rigor check --bleeding-edge lib` adopts the whole overlay and checks `lib`.
171
+ opts.on("--bleeding-edge=[LIST]",
172
+ "adopt the bleeding-edge overlay for this run " \
173
+ "(all features, or a comma-separated feature-id list)") do |value|
174
+ options[:bleeding_edge] = value.nil? || value.split(",").map(&:strip).reject(&:empty?)
175
+ end
176
+ opts.on("--no-bleeding-edge",
177
+ "ignore any configured bleeding_edge: selection for this run") do
178
+ options[:bleeding_edge] = false
179
+ end
180
+ opts.on("--no-tolerated-effects",
181
+ "check effect envelopes as if effects.tolerated: were empty") do
182
+ options[:no_tolerated_effects] = true
183
+ end
184
+ end
185
+ end
186
+
95
187
  private
96
188
 
97
189
  # True when `diagnostics` (already baseline-filtered and severity-resolved — the same list
@@ -219,10 +311,9 @@ module Rigor
219
311
  exit_code
220
312
  end
221
313
 
222
- # ADR-46 — the two incremental-analysis check modes both fully handle the run and return an exit code (so `run`
223
- # short-circuits); returns nil for an ordinary check.
224
- # A nil return means "not a special mode" — `run` continues with the ordinary check. Editor mode option B
225
- # also returns nil when it declines (no reusable snapshot), so the run falls back to single-file scope.
314
+ # ADR-46 — the two incremental-analysis check modes handle the run and return its exit code, so `run`
315
+ # short-circuits. Returns nil for an ordinary check, and for editor mode option B when it declines (no reusable
316
+ # snapshot), so the run falls back to single-file scope.
226
317
  def dispatch_special_check_mode(configuration, options, cache_root, buffer)
227
318
  if options.fetch(:verify_incremental)
228
319
  # The gate compares an incremental recheck against a full-run oracle, and a buffer makes the two
@@ -283,8 +374,10 @@ module Rigor
283
374
 
284
375
  # Issue #1967 — the partition check above re-analyses an unchanged tree, so every served row is still right and a
285
376
  # missing invalidation edge goes unseen. Here each of `--verify-incremental-edits` sampled files is edited through
286
- # an editor buffer (never on disk) by each of {Analysis::EditReplay.edits} — emptied, then reduced to its
287
- # `class` / `module` skeleton — and the warm recheck of the baseline ({IncrementalSession#replay_edit}) must equal
377
+ # an editor buffer (never on disk) by each of {Analysis::EditReplay.edits} — emptied, reduced to its
378
+ # `class` / `module` skeleton, and (#2277) with one body statement dropped, at most
379
+ # {Analysis::EditReplay::MAX_DROPS} times — and the warm recheck of the baseline
380
+ # ({IncrementalSession#replay_edit}) must equal
288
381
  # a full analysis of the edited tree: a dependent the recheck failed to re-analyse is served from the baseline and
289
382
  # differs. A run in which every replay declined compared nothing, so it fails rather than report OK.
290
383
  def verify_edit_replays(session, configuration, paths, options)
@@ -346,8 +439,8 @@ module Rigor
346
439
  return 1
347
440
  end
348
441
 
349
- line = "rigor: --verify-incremental OK — #{replayed} edits replayed (files emptied and reduced to their " \
350
- "class/module skeleton), every recheck matches full"
442
+ line = "rigor: --verify-incremental OK — #{replayed} edits replayed (files emptied, reduced to their " \
443
+ "class/module skeleton, and with a body statement dropped), every recheck matches full"
351
444
  line = "#{line}; #{declined} declined (the recheck would have been a full analysis)" if declined.positive?
352
445
  side_output(format).puts(line)
353
446
  0
@@ -386,9 +479,7 @@ module Rigor
386
479
  return run_editor_mode_option_b(session, snapshot, fingerprint, configuration, options) if buffer
387
480
 
388
481
  diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
389
- # The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
390
- # `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
391
- # built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
482
+ # The banner's file count comes from the session's analyzed set, so no probe Runner is built just to size it.
392
483
  # A warm banner reports the recheck's own sets (#1526), which only the session knows.
393
484
  report_incremental_run(warm, session.analyzed_files.size, recheck: session.last_recheck)
394
485
  emit_incremental_fact_surface_notes(session)
@@ -688,7 +779,7 @@ module Rigor
688
779
  )
689
780
  end
690
781
 
691
- def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
782
+ def parse_check_options
692
783
  options = {
693
784
  config: nil,
694
785
  format: "text",
@@ -698,7 +789,7 @@ module Rigor
698
789
  no_cache: false,
699
790
  stats: true,
700
791
  # ADR-15 Phase 4c — when nil, falls back to `RIGOR_RACTOR_WORKERS` then `.rigor.yml` `parallel.workers:` then
701
- # 0 (sequential). See `resolve_workers` for the precedence chain.
792
+ # 0 (sequential). See `CheckRunnerFactory.resolve_workers` for the precedence chain.
702
793
  workers: nil,
703
794
  tmp_file: nil,
704
795
  instead_of: nil,
@@ -747,93 +838,7 @@ module Rigor
747
838
  # one-line coverage summary.
748
839
  coverage: false
749
840
  }
750
- parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
751
- opts.banner = "Usage: rigor check [options] [paths]"
752
- Options.add_config(opts, options)
753
- opts.on("--format=FORMAT",
754
- "Output format: text, json, sarif, github, gitlab, checkstyle, junit, teamcity") do |value|
755
- options[:format] = value
756
- end
757
- opts.on("--explain", "Surface fail-soft fallback events as :info diagnostics") { options[:explain] = true }
758
- opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
759
- opts.on("--coverage",
760
- "Add a type-precision coverage block (an extra precision pass over the analyzed files)") do
761
- options[:coverage] = true
762
- end
763
- opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
764
- opts.on("--no-cache",
765
- "Disable the persistent cache for this run (with --incremental: no snapshot is read or written)") do
766
- options[:no_cache] = true
767
- end
768
- opts.on("--[no-]stats",
769
- "Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
770
- options[:stats] = value
771
- end
772
- opts.on("--workers=N", Integer,
773
- "Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
774
- options[:workers] = value
775
- end
776
- Options.add_editor_mode(opts, options)
777
- opts.on("--baseline=PATH",
778
- "load baseline from PATH (overrides .rigor.yml `baseline:`)") do |value|
779
- options[:baseline] = value
780
- end
781
- opts.on("--no-baseline",
782
- "ignore any configured baseline for this run") do
783
- options[:baseline] = false
784
- end
785
- opts.on("--baseline-strict",
786
- "fail the run on any baseline drift (CI gate)") do
787
- options[:baseline_strict] = true
788
- end
789
- opts.on("--fail-on=SEVERITY",
790
- "exit non-zero on a diagnostic at or above SEVERITY: error (default), warning, or info") do |value|
791
- # Validated post-parse (`valid_fail_on_option?`, called from `run`) rather than here — see its
792
- # comment for why an unrecognised value is not raised from inside this block.
793
- options[:fail_on] = value.to_s.downcase.to_sym
794
- end
795
- opts.on("--treat-all-as-inline-rbs",
796
- "force-load rigor-rbs-inline with require_magic_comment: false") do
797
- options[:treat_all_as_inline_rbs] = true
798
- end
799
- opts.on("--verify-incremental",
800
- "assert incremental analysis matches a full run, then exit") do
801
- options[:verify_incremental] = true
802
- end
803
- opts.on("--verify-incremental-edits=N", Integer,
804
- "with --verify-incremental, also replay 2 edits (empty, class/module skeleton) on N files") do |value|
805
- if value.negative?
806
- raise OptionParser::InvalidArgument, "--verify-incremental-edits=#{value} (must be 0 or more)"
807
- end
808
-
809
- options[:verify_incremental] = true
810
- options[:verify_incremental_edits] = value
811
- end
812
- opts.on("--incremental",
813
- "re-analyze only files changed since the last run (cross-process cache)") do
814
- options[:incremental] = true
815
- end
816
- opts.on("--no-ci-detect",
817
- "do not auto-emit CI-native output when a CI environment is detected") do
818
- options[:ci_detect] = false
819
- end
820
- # ADR-50 § WD2 — `=[LIST]` (not ` [LIST]`) so a bare `--bleeding-edge` never swallows a following positional
821
- # path: `rigor check --bleeding-edge lib` adopts the whole overlay and checks `lib`.
822
- opts.on("--bleeding-edge=[LIST]",
823
- "adopt the bleeding-edge overlay for this run " \
824
- "(all features, or a comma-separated feature-id list)") do |value|
825
- options[:bleeding_edge] = value.nil? || value.split(",").map(&:strip).reject(&:empty?)
826
- end
827
- opts.on("--no-bleeding-edge",
828
- "ignore any configured bleeding_edge: selection for this run") do
829
- options[:bleeding_edge] = false
830
- end
831
- opts.on("--no-tolerated-effects",
832
- "check effect envelopes as if effects.tolerated: were empty") do
833
- options[:no_tolerated_effects] = true
834
- end
835
- end
836
- parser.parse!(@argv)
841
+ option_parser(options).parse!(@argv)
837
842
  options
838
843
  end
839
844
 
@@ -861,7 +866,8 @@ module Rigor
861
866
  return Configuration.load(options.fetch(:config)) unless options.fetch(:treat_all_as_inline_rbs)
862
867
 
863
868
  path = options.fetch(:config) || Configuration.discover
864
- data = path && File.exist?(path) ? Configuration.load_with_includes(path) : {}
869
+ config_file = path && File.exist?(path) ? path : nil
870
+ data = config_file ? Configuration.load_with_includes(config_file) : {}
865
871
  # ADR-103 WD15 — captured before `DEFAULTS.merge` below folds "no `effects:` key" and "`effects:
866
872
  # false`" into the same value; see `Configuration.load`'s identical capture.
867
873
  effects_key_present = data.key?("effects")
@@ -869,7 +875,8 @@ module Rigor
869
875
  data = data.dup
870
876
  data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
871
877
  Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present, target_ruby_key_present)
872
- .with_exclude_base(path && File.exist?(path) ? File.dirname(path) : nil)
878
+ .with_exclude_base(config_file && File.dirname(config_file))
879
+ .with_config_path(config_file)
873
880
  end
874
881
 
875
882
  # ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge` CLI selection over the configured
@@ -943,6 +950,7 @@ module Rigor
943
950
  @err.puts(" recursion-unroll-fuel hits: #{counts[Inference::BudgetTrace::RECURSION_UNROLL_FUEL]}")
944
951
  @err.puts(" recursion-fixpoint-cap hits: #{counts[Inference::BudgetTrace::RECURSION_FIXPOINT_CAP]}")
945
952
  @err.puts(" block-writeback-cap hits: #{counts[Inference::BudgetTrace::BLOCK_WRITEBACK_CAP]}")
953
+ @err.puts(" callee-chain-cap hits: #{counts[Inference::BudgetTrace::CALLEE_CHAIN_CAP]}")
946
954
  write_budget_distributions
947
955
  write_memo_profile(counts)
948
956
  end
@@ -1153,7 +1161,7 @@ module Rigor
1153
1161
  payload["fail_on"] = fail_on.to_s
1154
1162
  payload["coverage"] = coverage_payload(coverage) if coverage
1155
1163
  payload["config_warnings"] = config_warnings.map(&:to_h) unless config_warnings.empty?
1156
- @out.puts(JSON.pretty_generate(payload))
1164
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(payload)))
1157
1165
  when "text"
1158
1166
  write_text_result(result)
1159
1167
  write_coverage_summary(coverage) if coverage
@@ -7,8 +7,8 @@ module Rigor
7
7
  class CLI
8
8
  # Shared factory for building an {Analysis::Runner} from CLI option hashes + a loaded {Configuration}.
9
9
  #
10
- # Extracted from {CheckCommand} so that `rigor doctor` and future `describe --deep` can reuse the exact same
11
- # runner-setup logic (cache, workers, buffer, explain) without diverging from the primary check path (ADR-77 WD3).
10
+ # Extracted from {CheckCommand} so that other commands (`rigor doctor`, `rigor skill describe --deep`) reuse the
11
+ # same runner setup (cache, workers, buffer, explain) instead of diverging from the `rigor check` path (ADR-77 WD3).
12
12
  module CheckRunnerFactory
13
13
  module_function
14
14
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../rooted_glob"
4
+ require_relative "../analysis/path_expansion"
4
5
 
5
6
  module Rigor
6
7
  class CLI
@@ -11,7 +12,7 @@ module Rigor
11
12
  # `$stderr`, most not at all). Centralising it here gives one consistent shape and lets a test instantiate a command
12
13
  # with just `argv:` (the streams default so a spec can pass a `StringIO` for one and ignore the other).
13
14
  #
14
- # Subclasses read the `@argv` / `@out` / `@err` ivars directly, as they did before.
15
+ # Subclasses read the `@argv` / `@out` / `@err` ivars directly.
15
16
  class Command
16
17
  def initialize(argv:, out: $stdout, err: $stderr)
17
18
  @argv = argv
@@ -38,6 +39,19 @@ module Rigor
38
39
  end
39
40
  paths.uniq
40
41
  end
42
+
43
+ # #2206 — the `.rb` paths `args` names, with a path that is neither a directory nor a `.rb` file reported the way
44
+ # `rigor check` reports it ({Analysis::PathExpansion.diagnostics}): a warning beside files that will be read, an
45
+ # error when nothing will be. The diagnostics go to stderr. Returns nil when one is an error, so the caller exits.
46
+ def collect_paths_reporting(args)
47
+ bad = Analysis::PathExpansion.unusable(args)
48
+ files = Analysis::PathExpansion.ruby_files(args, [])
49
+ diagnostics = Analysis::PathExpansion.diagnostics(bad, any_files: files.any?)
50
+ diagnostics.each { |diagnostic| @err.puts(diagnostic.to_s) }
51
+ return nil if diagnostics.any?(&:error?)
52
+
53
+ files.uniq
54
+ end
41
55
  end
42
56
  end
43
57
  end