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
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "json_document"
4
5
  require "optionparser"
5
6
 
6
7
  require_relative "../analysis/rule_catalog"
@@ -46,22 +47,28 @@ module Rigor
46
47
  0
47
48
  end
48
49
 
50
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
51
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
52
+ def option_parser(options = {})
53
+ OptionParser.new do |opt|
54
+ opt.banner = USAGE
55
+ Options.add_format(opt, options, "Output format (text | json). Default: text.")
56
+ end
57
+ end
58
+
49
59
  private
50
60
 
51
61
  def parse_options
52
62
  options = { format: "text" }
53
- OptionParser.new do |opt|
54
- opt.banner = USAGE
55
- opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
56
- options[:format] = fmt
57
- end
58
- end.parse!(@argv)
63
+ option_parser(options).parse!(@argv)
59
64
  options
60
65
  end
61
66
 
62
67
  def render_index(format)
63
68
  case format
64
- when "json" then @out.puts(JSON.pretty_generate(Analysis::RuleCatalog.all.map(&:to_h)))
69
+ when "json"
70
+ @out.puts(JSON.pretty_generate(Analysis::RuleCatalog.all.map(&:to_h)))
71
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:explain_rules))
65
72
  else render_index_text
66
73
  end
67
74
  end
@@ -87,6 +94,7 @@ module Rigor
87
94
  def render_skip_reason(entry, format)
88
95
  if format == "json"
89
96
  @out.puts(JSON.pretty_generate(entry.to_h))
97
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:explain_skip_reasons))
90
98
  return
91
99
  end
92
100
 
@@ -106,7 +114,9 @@ module Rigor
106
114
 
107
115
  def render_entries(entries, format)
108
116
  case format
109
- when "json" then @out.puts(JSON.pretty_generate(entries.map(&:to_h)))
117
+ when "json"
118
+ @out.puts(JSON.pretty_generate(entries.map(&:to_h)))
119
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:explain_rules))
110
120
  else
111
121
  entries.each_with_index do |entry, index|
112
122
  @out.puts("") if index.positive?
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "json_document"
4
5
 
5
6
  module Rigor
6
7
  class CLI
@@ -23,7 +24,7 @@ module Rigor
23
24
  private
24
25
 
25
26
  def render_json(report)
26
- @out.puts(JSON.pretty_generate(report.to_h))
27
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(report.to_h)))
27
28
  end
28
29
 
29
30
  def render_text(report)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class CLI
5
+ # The version marker every `--format=json` document Rigor defines carries (ADR-126 WD2, ADR-50 WD1).
6
+ #
7
+ # `schema_version` is the first top-level key and counts per document (per `mode` where a flag selects a
8
+ # different document). Adding a key keeps the version; removing or renaming one is a major-only change at
9
+ # v1.0.0. `lens` (`schema`/`revision`), `baseline dump` (`version`) and `check`'s external formats keep
10
+ # their own markers and do not pass through here.
11
+ module JsonDocument
12
+ SCHEMA_VERSION = 1
13
+
14
+ # @param payload — the document's top-level object (string or symbol keys).
15
+ # @param mode — names the document when a flag selects among several; nil when the command has one.
16
+ # @return a new Hash whose first key is `schema_version` (then `mode`, when given).
17
+ def self.stamp(payload, mode: nil)
18
+ head = { "schema_version" => SCHEMA_VERSION }
19
+ head["mode"] = mode if mode
20
+ head.merge(payload)
21
+ end
22
+
23
+ # The one-line stderr notice for `type-of`, `explain` and `trace`, whose top-level JSON shape changes in
24
+ # the next minor (ADR-126 WD4). stdout stays byte-identical until then. The MCP server appends the same
25
+ # text as its own content block, recognised by {NOTICE_PATTERN}.
26
+ def self.shape_notice(command, replacement)
27
+ "rigor: note — `rigor #{command} --format=json` will print #{replacement} from the next minor " \
28
+ "release (currently a different top-level shape); update JSON consumers now. See ADR-126."
29
+ end
30
+
31
+ NOTICE_PATTERN = /^rigor: note — `rigor [a-z-]+ --format=json` will print .*$/
32
+
33
+ SHAPE_NOTICES = {
34
+ type_of: shape_notice("type-of", '{ "schema_version": 1, "results": [...] }').freeze,
35
+ trace: shape_notice("trace", '{ "schema_version": 1, "events": [...] }').freeze,
36
+ explain_rules: shape_notice("explain", '{ "schema_version": 1, "rules": [...] }').freeze,
37
+ explain_skip_reasons: shape_notice("explain", '{ "schema_version": 1, "skip_reasons": [...] }').freeze
38
+ }.freeze
39
+ end
40
+ end
41
+ end
@@ -15,7 +15,6 @@ module Rigor
15
15
  # computes them (ADR-113; output contract docs/internal-spec/lens.md). Experimental until `lens/v1`.
16
16
  class LensCommand < Command
17
17
  USAGE = "Usage: rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE..."
18
- FORMATS = %w[text json].freeze
19
18
 
20
19
  def run
21
20
  options = parse_options
@@ -35,17 +34,22 @@ module Rigor
35
34
  outcome.targets.any? { |target| target.status == "ok" } ? 0 : 1
36
35
  end
37
36
 
38
- private
39
-
40
- def parse_options
41
- options = { format: "text", config: nil, workers: nil }
37
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
38
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
39
+ def option_parser(options = {})
42
40
  OptionParser.new do |opts|
43
41
  opts.banner = USAGE
44
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
42
+ Options.add_format(opts, options, "Output format: text (default) or json")
45
43
  opts.on("--workers=N", Integer, "Worker processes for a cold run") { |value| options[:workers] = value }
46
44
  Options.add_config(opts, options)
47
- end.parse!(@argv)
48
- return usage_error("--format must be text or json") unless FORMATS.include?(options[:format])
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def parse_options
51
+ options = { format: "text", config: nil, workers: nil }
52
+ option_parser(options).parse!(@argv)
49
53
  return usage_error("no FILE given") if @argv.empty?
50
54
 
51
55
  options