rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -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
 
@@ -943,6 +948,7 @@ module Rigor
943
948
  @err.puts(" recursion-unroll-fuel hits: #{counts[Inference::BudgetTrace::RECURSION_UNROLL_FUEL]}")
944
949
  @err.puts(" recursion-fixpoint-cap hits: #{counts[Inference::BudgetTrace::RECURSION_FIXPOINT_CAP]}")
945
950
  @err.puts(" block-writeback-cap hits: #{counts[Inference::BudgetTrace::BLOCK_WRITEBACK_CAP]}")
951
+ @err.puts(" callee-chain-cap hits: #{counts[Inference::BudgetTrace::CALLEE_CHAIN_CAP]}")
946
952
  write_budget_distributions
947
953
  write_memo_profile(counts)
948
954
  end
@@ -1153,7 +1159,7 @@ module Rigor
1153
1159
  payload["fail_on"] = fail_on.to_s
1154
1160
  payload["coverage"] = coverage_payload(coverage) if coverage
1155
1161
  payload["config_warnings"] = config_warnings.map(&:to_h) unless config_warnings.empty?
1156
- @out.puts(JSON.pretty_generate(payload))
1162
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(payload)))
1157
1163
  when "text"
1158
1164
  write_text_result(result)
1159
1165
  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
@@ -68,7 +68,7 @@ module Rigor
68
68
  configuration = Configuration.load(options.fetch(:config))
69
69
  paths = resolve_paths(configuration)
70
70
  return CLI::EXIT_USAGE if paths.nil?
71
- return usage_error if paths.empty?
71
+ return no_ruby_files_error(configuration) if paths.empty?
72
72
 
73
73
  return run_protection(paths, options, configuration) if options[:protection]
74
74
 
@@ -77,27 +77,31 @@ module Rigor
77
77
  determine_exit(report, options)
78
78
  end
79
79
 
80
+ # The parser this command parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
81
+ def option_parser(options = {})
82
+ OptionParser.new { |opts| define_options(opts, options) }
83
+ end
84
+
80
85
  private
81
86
 
82
- # Like `rigor check`, fall back to the configured `paths:` when no path is given on the command line
83
- # (`coverage` previously required an explicit path — operational friction the two commands should not
84
- # differ on). The mutation mode keeps its own git-changed-files default and returns before this.
87
+ # Like `rigor check`, fall back to the configured `paths:` when no path is given. The mutation mode keeps its own
88
+ # git-changed-files default and returns before this.
85
89
  def resolve_paths(configuration)
86
90
  args = @argv.empty? ? configuration.paths : @argv
87
- collect_paths(args, command_name: "coverage")
91
+ collect_paths_reporting(args)
88
92
  end
89
93
 
90
94
  def parse_options
91
95
  options = { format: "text", threshold: nil, config: nil, protection: false, mutation: false,
92
96
  with_tests: false, test_command: DEFAULT_TEST_COMMAND, include_dynamic: false,
93
97
  limit: nil, seed: 1, workers: nil, no_cache: false }
94
- OptionParser.new { |opts| define_options(opts, options) }.parse!(@argv)
98
+ option_parser(options).parse!(@argv)
95
99
  options
96
100
  end
97
101
 
98
102
  def define_options(opts, options)
99
103
  opts.banner = USAGE
100
- opts.on("--format=FORMAT", "Output format: text or json") { |v| options[:format] = v }
104
+ Options.add_format(opts, options, "Output format: text or json")
101
105
  Options.add_config(opts, options)
102
106
  opts.on("--protection", "Report type-protection coverage (ADR-63 Tier 1) instead of type precision") do
103
107
  options[:protection] = true
@@ -203,7 +207,7 @@ module Rigor
203
207
  # `singleton(Account)` instead of `Dynamic`. Without this, a
204
208
  # single-file scan cannot see a class it does not itself declare,
205
209
  # so every cross-file class-constant dispatch was miscounted as
206
- # unprotected (the model-constant undercount found 2026-07-04).
210
+ # unprotected.
207
211
  # - `param_inferred_types` (ADR-67 WD3) — an inferred-parameter
208
212
  # receiver (`node.loc` where `node` is a `def compile(node)`
209
213
  # parameter) counts as protected when its call sites resolve to
@@ -253,6 +257,15 @@ module Rigor
253
257
  CLI::EXIT_USAGE
254
258
  end
255
259
 
260
+ # #2206 — paths were named, or configured, but none holds a Ruby file. Said as such, not as "no path given".
261
+ def no_ruby_files_error(configuration)
262
+ return usage_error if @argv.empty? && Array(configuration.paths).empty?
263
+
264
+ @err.puts("coverage: no Ruby files under the given paths")
265
+ @err.puts(USAGE)
266
+ CLI::EXIT_USAGE
267
+ end
268
+
256
269
  def scan_paths(paths, configuration)
257
270
  CoverageScan.precision_report(files: paths, configuration: configuration)
258
271
  end
@@ -24,7 +24,6 @@ module Rigor
24
24
  DISCOVERY_SEEDED_MUTATION_SITES = "discovery-seeded-mutation-sites"
25
25
 
26
26
  # ADR-50 § WD2 — the bleeding-edge feature id gating the Tier-2 dependent-closure kill oracle (#254).
27
- # Same reason as above for naming it here rather than inlining the string.
28
27
  DEPENDENT_CLOSURE_KILL_ORACLE = "dependent-closure-kill-oracle"
29
28
 
30
29
  # #264 — the "loud" threshold for a rescued-harness-failure count. Below it, a rescued mutant reads as
@@ -52,8 +51,7 @@ module Rigor
52
51
  MeasurementIntegrityWarning.emit(report, err: @err, floor: HARNESS_ERROR_WARN_FLOOR)
53
52
  end
54
53
 
55
- # The cross-file knowledge Tier 2 measures with — the #253 gate, and the ONLY place in this feature that
56
- # knows a feature id exists.
54
+ # The cross-file knowledge Tier 2 measures with, gated on the #253 feature.
57
55
  #
58
56
  # Returns nil (today's behaviour: an unseeded per-file view on both halves of the measurement) unless the
59
57
  # project has adopted `discovery-seeded-mutation-sites`. With it adopted, returns the table set
@@ -81,7 +79,7 @@ module Rigor
81
79
  seed.empty? ? nil : seed
82
80
  end
83
81
 
84
- # The kill oracle Tier 2 measures with — the #254 gate, and the only place that feature id is read.
82
+ # The kill oracle Tier 2 measures with, gated on the #254 feature.
85
83
  #
86
84
  # Returns nil (today's behaviour: {Protection::DiagnosticOracle}, which re-analyses the mutated file
87
85
  # alone) unless the project has adopted `dependent-closure-kill-oracle`. With it adopted, returns the
@@ -139,7 +137,7 @@ module Rigor
139
137
  # warm loop), then re-analyses each target file's mutants against its clean baseline. Defaults to the git-changed
140
138
  # `.rb` files; explicit paths override (and enable the whole-project opt-in, which is minutes).
141
139
  def run_mutation_protection(options)
142
- explicit = collect_paths(@argv, command_name: "coverage")
140
+ explicit = collect_paths_reporting(@argv)
143
141
  return CLI::EXIT_USAGE if explicit.nil?
144
142
 
145
143
  target_files = explicit.empty? ? changed_ruby_files : explicit
@@ -204,8 +202,8 @@ module Rigor
204
202
  determine_protection_exit(report, options)
205
203
  end
206
204
 
207
- # An explicit `--workers=N` on the fused path cannot be honoured, so say so rather than repeat the bug
208
- # this slice fixed (a flag accepted and silently dropped). Only the explicit flag warns — a project-wide
205
+ # An explicit `--workers=N` on the fused path cannot be honoured, so say so rather than drop it silently. Only the
206
+ # explicit flag warns — a project-wide
209
207
  # `parallel.workers:` or `RIGOR_RACTOR_WORKERS` default is not a request about *this* run.
210
208
  def warn_workers_ignored_under_tests(options)
211
209
  return unless options[:workers].to_i > 1
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require_relative "renderable"
5
+ require_relative "json_document"
5
6
 
6
7
  module Rigor
7
8
  class CLI
@@ -93,7 +94,7 @@ module Rigor
93
94
  end
94
95
 
95
96
  def render_json(report)
96
- @out.puts(JSON.pretty_generate(json_payload(report)))
97
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(json_payload(report), mode: "precision")))
97
98
  end
98
99
 
99
100
  def json_payload(report)
@@ -23,11 +23,9 @@ module Rigor
23
23
 
24
24
  # @param files — explicit `.rb` file paths to scan.
25
25
  #
26
- # Issue #513 — the environment is PLUGIN-AWARE (`LanguageServer::ProjectContext`, the same builder
27
- # `--protection` uses), not the bare RBS environment this path carried until 2026-09-01. Without the
28
- # plugin registry every plugin-typed receiver read `Dynamic` and the precision ratio understated the
29
- # engine by ~1.2 points on a Rails target — the defect the protection path documented and fixed for
30
- # itself on 2026-07-04, never applied here.
26
+ # Issue #513 — the environment is plugin-aware (`LanguageServer::ProjectContext`, the same builder
27
+ # `--protection` uses). A bare RBS environment read every plugin-typed receiver as `Dynamic` and understated
28
+ # the precision ratio by ~1.2 points on a Rails target.
31
29
  def precision_report(files:, configuration:)
32
30
  scope = discovery_seeded_scope(
33
31
  files: files,
@@ -5,11 +5,12 @@ require_relative "options"
5
5
 
6
6
  require "json"
7
7
  require "optionparser"
8
+ require_relative "json_document"
8
9
 
9
10
  module Rigor
10
11
  class CLI
11
- # Executes `rigor diff <baseline.json> [paths...]`. Compares the current `rigor check` diagnostics against a saved
12
- # baseline JSON (the output of a previous `rigor check --format=json` run) and prints the delta:
12
+ # Executes `rigor diff <rigor-check.json> [paths...]`. Compares the current `rigor check` diagnostics against a
13
+ # saved `rigor check --format=json` result and prints the delta:
13
14
  #
14
15
  # - **new** — diagnostics in the current run that were not
15
16
  # in the baseline (typically a regression introduced in
@@ -21,11 +22,11 @@ module Rigor
21
22
  # diagnostic to a new line surfaces as one fixed + one new pair, which lines up with the user's mental model of "you
22
23
  # changed the line, the analyzer's position changed too."
23
24
  #
24
- # CI usage: commit a `rigor.baseline.json` produced once with `rigor check --format=json > rigor.baseline.json`,
25
- # then run `rigor diff rigor.baseline.json` in CI. Exit code is `1` when any new diagnostic appears, `0` otherwise —
25
+ # CI usage: commit a `rigor-check.json` produced once with `rigor check --format=json > rigor-check.json`, then
26
+ # run `rigor diff rigor-check.json` in CI. Exit code is `1` when any new diagnostic appears, `0` otherwise —
26
27
  # so adding new errors fails CI but legacy errors recorded in the baseline don't.
27
28
  class DiffCommand < Command
28
- USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
29
+ USAGE = "Usage: rigor diff [options] <rigor-check.json> [paths...]"
29
30
 
30
31
  # @return CLI exit status.
31
32
  def run
@@ -51,22 +52,26 @@ module Rigor
51
52
  diff[:new].any? ? 1 : 0
52
53
  end
53
54
 
54
- private
55
-
56
- def parse_options
57
- options = { format: "text", current_path: nil, config: nil }
55
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
56
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
57
+ def option_parser(options = {})
58
58
  OptionParser.new do |opt|
59
59
  opt.banner = USAGE
60
- opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
61
- options[:format] = fmt
62
- end
60
+ Options.add_format(opt, options, "Output format (text | json). Default: text.")
63
61
  opt.on("--current=PATH", "Compare to the saved current JSON instead of running `rigor check`.") do |path|
64
62
  options[:current_path] = path
65
63
  end
66
64
  opt.on("--config=PATH", "Path to .rigor.yml. Forwarded to the implicit `rigor check` run.") do |path|
67
65
  options[:config] = Options.config_path(path)
68
66
  end
69
- end.parse!(@argv)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def parse_options
73
+ options = { format: "text", current_path: nil, config: nil }
74
+ option_parser(options).parse!(@argv)
70
75
  options
71
76
  end
72
77
 
@@ -121,13 +126,13 @@ module Rigor
121
126
  end
122
127
 
123
128
  def write_diff_json(diff, baseline_path, baseline_count, current_count)
124
- @out.puts(JSON.pretty_generate(
125
- "baseline" => baseline_path,
126
- "baseline_count" => baseline_count,
127
- "current_count" => current_count,
128
- "new" => diff[:new],
129
- "fixed" => diff[:fixed]
130
- ))
129
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp({
130
+ "baseline" => baseline_path,
131
+ "baseline_count" => baseline_count,
132
+ "current_count" => current_count,
133
+ "new" => diff[:new],
134
+ "fixed" => diff[:fixed]
135
+ })))
131
136
  end
132
137
 
133
138
  def write_diff_text(diff, baseline_path, baseline_count, current_count)
@@ -203,8 +203,7 @@ module Rigor
203
203
  relative: relative,
204
204
  path: path,
205
205
  category: category,
206
- # Always-unique addresses: the `docs/`-relative path and the prefixed basename. `handbook/03-narrowing` and
207
- # `03-narrowing`.
206
+ # Always-unique addresses: the `docs/`-relative path and the prefixed basename.
208
207
  exact_aliases: [relative, base].uniq,
209
208
  # The prefix-stripped short name (`narrowing`); ambiguous when two categories carry the same chapter slug
210
209
  # (`plugins`).
@@ -17,6 +17,7 @@ require_relative "../type/combinator"
17
17
  require_relative "check_invocation"
18
18
  require_relative "command"
19
19
  require_relative "options"
20
+ require_relative "json_document"
20
21
 
21
22
  module Rigor
22
23
  class CLI
@@ -86,23 +87,22 @@ module Rigor
86
87
  CLI::EXIT_USAGE
87
88
  end
88
89
 
89
- private
90
-
91
- def parse_options
92
- options = { config: nil, format: "text" }
90
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
91
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
92
+ def option_parser(options = {})
93
93
  OptionParser.new do |opts|
94
94
  opts.banner = USAGE
95
95
  Options.add_config(opts, options)
96
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
97
- end.parse!(@argv)
98
- validate!(options)
99
- options
96
+ Options.add_format(opts, options, "Output format: text (default) or json")
97
+ end
100
98
  end
101
99
 
102
- def validate!(options)
103
- return if %w[text json].include?(options.fetch(:format))
100
+ private
104
101
 
105
- raise OptionParser::InvalidArgument, "unsupported format: #{options.fetch(:format)}"
102
+ def parse_options
103
+ options = { config: nil, format: "text" }
104
+ option_parser(options).parse!(@argv)
105
+ options
106
106
  end
107
107
 
108
108
  # ------------------------------------------------------------------
@@ -437,7 +437,7 @@ module Rigor
437
437
  }
438
438
  end
439
439
  }
440
- @out.puts(JSON.pretty_generate(payload))
440
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(payload)))
441
441
  end
442
442
 
443
443
  def report_text(findings)