rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -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,
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "command"
4
+ require_relative "options"
4
5
 
5
6
  require "json"
6
7
  require "optionparser"
8
+ require_relative "json_document"
7
9
 
8
10
  module Rigor
9
11
  class CLI
10
- # Executes `rigor diff <baseline.json> [paths...]`. Compares the current `rigor check` diagnostics against a saved
11
- # 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:
12
14
  #
13
15
  # - **new** — diagnostics in the current run that were not
14
16
  # in the baseline (typically a regression introduced in
@@ -20,11 +22,11 @@ module Rigor
20
22
  # diagnostic to a new line surfaces as one fixed + one new pair, which lines up with the user's mental model of "you
21
23
  # changed the line, the analyzer's position changed too."
22
24
  #
23
- # CI usage: commit a `rigor.baseline.json` produced once with `rigor check --format=json > rigor.baseline.json`,
24
- # 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 —
25
27
  # so adding new errors fails CI but legacy errors recorded in the baseline don't.
26
28
  class DiffCommand < Command
27
- USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
29
+ USAGE = "Usage: rigor diff [options] <rigor-check.json> [paths...]"
28
30
 
29
31
  # @return CLI exit status.
30
32
  def run
@@ -50,22 +52,26 @@ module Rigor
50
52
  diff[:new].any? ? 1 : 0
51
53
  end
52
54
 
53
- private
54
-
55
- def parse_options
56
- 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 = {})
57
58
  OptionParser.new do |opt|
58
59
  opt.banner = USAGE
59
- opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
60
- options[:format] = fmt
61
- end
60
+ Options.add_format(opt, options, "Output format (text | json). Default: text.")
62
61
  opt.on("--current=PATH", "Compare to the saved current JSON instead of running `rigor check`.") do |path|
63
62
  options[:current_path] = path
64
63
  end
65
64
  opt.on("--config=PATH", "Path to .rigor.yml. Forwarded to the implicit `rigor check` run.") do |path|
66
- options[:config] = path
65
+ options[:config] = Options.config_path(path)
67
66
  end
68
- 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)
69
75
  options
70
76
  end
71
77
 
@@ -120,13 +126,13 @@ module Rigor
120
126
  end
121
127
 
122
128
  def write_diff_json(diff, baseline_path, baseline_count, current_count)
123
- @out.puts(JSON.pretty_generate(
124
- "baseline" => baseline_path,
125
- "baseline_count" => baseline_count,
126
- "current_count" => current_count,
127
- "new" => diff[:new],
128
- "fixed" => diff[:fixed]
129
- ))
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
+ })))
130
136
  end
131
137
 
132
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)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "did_you_mean"
3
4
  require "optionparser"
4
5
 
5
6
  require_relative "../configuration"
@@ -50,6 +51,38 @@ module Rigor
50
51
  run_report
51
52
  end
52
53
 
54
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
55
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
56
+ def option_parser(options = {})
57
+ OptionParser.new do |opts|
58
+ opts.banner = USAGE
59
+ Options.add_config(opts, options)
60
+ Options.add_format(opts, options, "Output format: text (default) or json")
61
+ opts.on("--full", "List every method, including the ones with nothing to say") do
62
+ options[:full] = true
63
+ end
64
+ opts.on("--label=LABEL", "Only methods carrying LABEL (or a label under it), in either lane") do |value|
65
+ options[:label].concat(value.split(",").map(&:strip).reject(&:empty?))
66
+ end
67
+ opts.on("--pure", "Only methods proven to do nothing beyond mutate.local — the %a{pure} set") do
68
+ options[:pure] = true
69
+ end
70
+ opts.on("--limit=N", Integer, "Print at most N methods") { |value| options[:limit] = value }
71
+ opts.on("--why", "Expand each method's unresolved reasons and declared-lane sources") do
72
+ options[:why] = true
73
+ end
74
+ opts.on("--list-labels", "Print the effect vocabulary this project can name, and exit") do
75
+ options[:list_labels] = true
76
+ end
77
+ # Accepted here and deliberately inert, exactly as it is on `update`: the report is an
78
+ # observation, and observations are undischarged. Only a JUDGMENT reads `effects.tolerated:` —
79
+ # `rigor effects check` / `diff`, and `rigor check`'s envelope contract.
80
+ opts.on("--no-tolerated-effects", "Judge as if effects.tolerated: were empty (inert on the report)") do
81
+ options[:no_tolerated] = true
82
+ end
83
+ end
84
+ end
85
+
53
86
  private
54
87
 
55
88
  def run_verb(verb)
@@ -91,7 +124,7 @@ module Rigor
91
124
 
92
125
  def run_report
93
126
  options = parse_options
94
- return usage_error("unsupported format: #{options.fetch(:format)}") unless FORMATS.include?(options[:format])
127
+ return CLI::EXIT_USAGE if misspelled_verb?
95
128
 
96
129
  configuration = Configuration.load(options.fetch(:config)).with_effects_enabled
97
130
  return list_labels(configuration) if options.fetch(:list_labels)
@@ -107,6 +140,20 @@ module Rigor
107
140
  0
108
141
  end
109
142
 
143
+ # A first word that is no path but is one edit from a snapshot verb (`rigor effects updat`) is a misspelled verb,
144
+ # not a scope: the report would otherwise run to completion and say only that no unit lives in `updat`.
145
+ def misspelled_verb?
146
+ word = @argv.first
147
+ return false if word.nil? || word.match?(%r{[/.]}) || File.exist?(word)
148
+
149
+ nearest = ::DidYouMean::SpellChecker.new(dictionary: EffectsSnapshotCommand::VERBS).correct(word).first
150
+ return false unless nearest
151
+
152
+ @err.puts("rigor: unknown effects subcommand: #{word}")
153
+ @err.puts("Did you mean `rigor effects #{nearest}`?")
154
+ true
155
+ end
156
+
110
157
  # A path argument is a **view**, and the note says so (#439).
111
158
  #
112
159
  # It used to narrow the analysed set, and effect labels are transitive over whatever was analysed —
@@ -131,48 +178,13 @@ module Rigor
131
178
  "label is the one the whole-project run reports")
132
179
  end
133
180
 
134
- FORMATS = %w[text json].freeze
135
- private_constant :FORMATS
136
-
137
181
  def parse_options
138
182
  options = { config: nil, format: "text", full: false, no_tolerated: false, label: [], pure: false,
139
183
  limit: nil, why: false, list_labels: false }
140
- OptionParser.new do |opts|
141
- opts.banner = USAGE
142
- Options.add_config(opts, options)
143
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
144
- opts.on("--full", "List every method, including the ones with nothing to say") do
145
- options[:full] = true
146
- end
147
- opts.on("--label=LABEL", "Only methods carrying LABEL (or a label under it), in either lane") do |value|
148
- options[:label].concat(value.split(",").map(&:strip).reject(&:empty?))
149
- end
150
- opts.on("--pure", "Only methods proven to do nothing beyond mutate.local — the %a{pure} set") do
151
- options[:pure] = true
152
- end
153
- opts.on("--limit=N", Integer, "Print at most N methods") { |value| options[:limit] = value }
154
- opts.on("--why", "Expand each method's unresolved reasons and declared-lane sources") do
155
- options[:why] = true
156
- end
157
- opts.on("--list-labels", "Print the effect vocabulary this project can name, and exit") do
158
- options[:list_labels] = true
159
- end
160
- # Accepted here and deliberately inert, exactly as it is on `update`: the report is an
161
- # observation, and observations are undischarged. Only a JUDGMENT reads `effects.tolerated:` —
162
- # `rigor effects check` / `diff`, and `rigor check`'s envelope contract.
163
- opts.on("--no-tolerated-effects", "Judge as if effects.tolerated: were empty (inert on the report)") do
164
- options[:no_tolerated] = true
165
- end
166
- end.parse!(@argv)
184
+ option_parser(options).parse!(@argv)
167
185
  options
168
186
  end
169
187
 
170
- def usage_error(message)
171
- @err.puts(message)
172
- @err.puts(USAGE)
173
- CLI::EXIT_USAGE
174
- end
175
-
176
188
  # Sequential and cache-backed, through the same ADR-45 whole-run result cache `rigor check` uses:
177
189
  # the diagnostics entry serves the run and the #382 effects sidecar serves the collections, leaving
178
190
  # only the fixpoint. Sequential is not a cache decision — the run-result cache declines pool mode —
@@ -222,9 +234,7 @@ module Rigor
222
234
 
223
235
  # The analysed set is the configured `paths:` **plus** whatever the arguments name — never the
224
236
  # arguments alone (#439). An effect summary is transitive over whatever was analysed, so analysing
225
- # less does not filter the report, it lowers every answer in it: `rigor effects
226
- # app/controllers/issues_controller.rb` used to report `IssuesController#create: [] …?` where the
227
- # whole-project run reported four labels and a declared lane, with nothing marking the difference.
237
+ # less does not filter the report, it lowers every answer in it.
228
238
  #
229
239
  # The union rather than the configured paths alone, so that pointing the command at a tree the
230
240
  # configuration does not cover — which is what every `rigor effects PATH` invocation from outside a
@@ -6,6 +6,7 @@ require_relative "../effects/definition_lines"
6
6
  require_relative "../effects/snapshot"
7
7
  require_relative "../effects/snapshot_diff"
8
8
  require_relative "renderable"
9
+ require_relative "json_document"
9
10
 
10
11
  module Rigor
11
12
  class CLI
@@ -121,7 +122,7 @@ module Rigor
121
122
  # method that no longer exists was not defined by it. Such a row renders exactly as it did before
122
123
  # this suffix existed.
123
124
  #
124
- # @return `[relative path, line]` per file, the line nil when that
125
+ # @return a relative path and line per file, the line nil when that
125
126
  # file spells the key with no `def` — an accessor, or a definition the nesting cannot name.
126
127
  def sources_of(symbol)
127
128
  Array(@sources[symbol]).map do |path|
@@ -175,22 +176,22 @@ module Rigor
175
176
  end
176
177
 
177
178
  def render_json(diff)
178
- @out.puts(JSON.pretty_generate(
179
- "fresh" => diff.fresh?,
180
- "events" => diff.events.map { |event| event_json(event) },
181
- "footer" => {
182
- "added_symbols" => diff.footer[:added_symbols],
183
- "removed_symbols" => diff.footer[:removed_symbols],
184
- "suppressed" => diff.footer[:suppressed]
185
- },
186
- "regeneration" => diff.regeneration?,
187
- "header" => {
188
- "snapshot" => @path,
189
- "gate" => diff.gate.to_s,
190
- "recorded" => diff.recorded&.header,
191
- "current" => diff.current.header
192
- }
193
- ))
179
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp({
180
+ "fresh" => diff.fresh?,
181
+ "events" => diff.events.map { |event| event_json(event) },
182
+ "footer" => {
183
+ "added_symbols" => diff.footer[:added_symbols],
184
+ "removed_symbols" => diff.footer[:removed_symbols],
185
+ "suppressed" => diff.footer[:suppressed]
186
+ },
187
+ "regeneration" => diff.regeneration?,
188
+ "header" => {
189
+ "snapshot" => @path,
190
+ "gate" => diff.gate.to_s,
191
+ "recorded" => diff.recorded&.header,
192
+ "current" => diff.current.header
193
+ }
194
+ })))
194
195
  end
195
196
  end
196
197
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  require_relative "renderable"
6
+ require_relative "json_document"
6
7
 
7
8
  module Rigor
8
9
  class CLI
@@ -53,18 +54,18 @@ module Rigor
53
54
  end
54
55
 
55
56
  def render_json(rows)
56
- @out.puts(JSON.pretty_generate(
57
- "paths" => rows.map do |row|
58
- {
59
- "table" => row.table,
60
- "symbol" => row.symbol,
61
- "label" => row.label,
62
- "path" => row.path,
63
- "origin" => row.origin,
64
- "causes" => row.causes
65
- }
66
- end
67
- ))
57
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp({
58
+ "paths" => rows.map do |row|
59
+ {
60
+ "table" => row.table,
61
+ "symbol" => row.symbol,
62
+ "label" => row.label,
63
+ "path" => row.path,
64
+ "origin" => row.origin,
65
+ "causes" => row.causes
66
+ }
67
+ end
68
+ })))
68
69
  end
69
70
  end
70
71
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  require_relative "renderable"
6
+ require_relative "json_document"
6
7
 
7
8
  module Rigor
8
9
  class CLI
@@ -85,7 +86,7 @@ module Rigor
85
86
  end
86
87
  }
87
88
  payload["totals"] = report.totals.to_h.transform_keys(&:to_s) if report.totals
88
- @out.puts(JSON.pretty_generate(payload))
89
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(payload)))
89
90
  end
90
91
  end
91
92
  end
@@ -38,9 +38,6 @@ module Rigor
38
38
  class EffectsSnapshotCommand < Command
39
39
  VERBS = %w[update check diff explain].freeze
40
40
 
41
- FORMATS = %w[text json].freeze
42
- private_constant :FORMATS
43
-
44
41
  # The verbs that read a committed record, and so accept `--baseline PATH` —
45
42
  # `--baseline <(git show origin/main:.rigor-effects.yml)` is the bot form.
46
43
  COMPARING_VERBS = %w[check diff explain].freeze
@@ -54,8 +51,6 @@ module Rigor
54
51
  # @return 0 fresh / written, 1 on drift under `check`, 64 on a usage error.
55
52
  def run
56
53
  options = parse_options
57
- return usage_error("unsupported format: #{options.fetch(:format)}") unless
58
- FORMATS.include?(options[:format])
59
54
  return usage_error("rigor effects #{@verb} takes no paths; it records the configured paths:") unless
60
55
  @argv.empty?
61
56
 
@@ -64,6 +59,22 @@ module Rigor
64
59
  usage_error(e.message)
65
60
  end
66
61
 
62
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
63
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
64
+ def option_parser(options = {})
65
+ OptionParser.new do |opts|
66
+ opts.banner = "Usage: rigor effects #{@verb} [options]"
67
+ Options.add_config(opts, options)
68
+ add_common_options(opts, options)
69
+ add_comparison_options(opts, options) if COMPARING_VERBS.include?(@verb)
70
+ if @verb == "explain"
71
+ opts.on("--symbol=KEY", "Explain this unit instead of the changed ones") do |value|
72
+ options[:symbol] = value
73
+ end
74
+ end
75
+ end
76
+ end
77
+
67
78
  private
68
79
 
69
80
  def execute(options)
@@ -273,17 +284,7 @@ module Rigor
273
284
  def parse_options
274
285
  options = { config: nil, format: "text", full: false, baseline: nil, symbol: nil,
275
286
  strict_tolerated: false, no_tolerated: false }
276
- OptionParser.new do |opts|
277
- opts.banner = "Usage: rigor effects #{@verb} [options]"
278
- Options.add_config(opts, options)
279
- add_common_options(opts, options)
280
- add_comparison_options(opts, options) if COMPARING_VERBS.include?(@verb)
281
- if @verb == "explain"
282
- opts.on("--symbol=KEY", "Explain this unit instead of the changed ones") do |value|
283
- options[:symbol] = value
284
- end
285
- end
286
- end.parse!(@argv)
287
+ option_parser(options).parse!(@argv)
287
288
  options
288
289
  end
289
290
 
@@ -292,7 +293,7 @@ module Rigor
292
293
  # byte-identical files. A flag that changed the record would make the file a function of policy and
293
294
  # a policy change indistinguishable from a code change.
294
295
  def add_common_options(opts, options)
295
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
296
+ Options.add_format(opts, options, "Output format: text (default) or json")
296
297
  opts.on("--full", "Record every method, including the omitted trivial and synthesised ones") do
297
298
  options[:full] = true
298
299
  end