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,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "optparse"
4
+
3
5
  require_relative "command"
4
6
  require_relative "../rooted_glob"
5
7
 
@@ -7,7 +9,7 @@ module Rigor
7
9
  class CLI
8
10
  # `rigor plugin` (singular) — discover and read the plugin source bundled with the `rigortype` gem.
9
11
  #
10
- # Rigor ships ~30 production plugins under `plugins/` and a set of tutorial plugins under `examples/`. When Rigor is
12
+ # Rigor ships production plugins under `plugins/` and tutorial plugins under `examples/`. When Rigor is
11
13
  # installed via `mise` / `gem install` the gem checkout is on disk, so a plugin author (or an AI coding agent
12
14
  # following the `rigor-plugin-author` skill) can read a real, working plugin as a worked example — instead of
13
15
  # guessing the `Rigor::Plugin::Base` surface from prose. This command outputs the absolute paths so they can be
@@ -17,75 +19,122 @@ module Rigor
17
19
  # configured in *your* `.rigor.yml`. This command (singular) browses the plugins bundled in the *toolchain*.
18
20
  # Mnemonic: "plugins" = my config; "plugin" = the catalogue I can learn from.
19
21
  #
20
- # Subcommands:
22
+ # Grammar (mirrors `rigor docs` and `rigor skill`, ADR-126 WD4): the positional slot is always a plugin *name*;
23
+ # alternative outputs are flags, so a plugin can never be shadowed by a verb.
21
24
  #
22
- # - `rigor plugin list` — every bundled plugin + example,
23
- # name + absolute directory path.
24
- # - `rigor plugin path <name>` — one-line absolute path to the
25
- # plugin's directory (Read-tool input).
26
- # - `rigor plugin print <name>` — a header (dir / lib / sig / README
27
- # paths) followed by the plugin's main
25
+ # - `rigor plugin` — every bundled plugin + example, name + absolute directory path (the default).
26
+ # - `rigor plugin <name>` — a header (dir / lib / sig / README paths) followed by the plugin's main
28
27
  # `lib/<name>.rb` source body.
29
- # - `rigor plugin root` — the rigortype gem root and its key
30
- # subdirectories (lib/, plugins/,
31
- # examples/, skills/, sig/), so an
32
- # author can read the public plugin
33
- # API (`lib/rigor/plugin.rb`) directly.
28
+ # - `rigor plugin --path <name>` — one-line absolute path to the plugin's directory (Read-tool input).
29
+ # - `rigor plugin --list` — the same listing as no argument.
30
+ # - `rigor plugin --root` — the rigortype gem root and its key subdirectories (lib/, plugins/, examples/,
31
+ # skills/, sig/), so an author can read the public plugin API
32
+ # (`lib/rigor/plugin.rb`) directly.
34
33
  #
35
- # `rigor plugin` with no subcommand is an alias for `list`.
34
+ # The verbs `list`, `path <name>`, `print <name>` and `root` are the pre-ADR-126 spelling. They still work for one
35
+ # minor, print the same stdout and return the same status, and add one deprecation notice on stderr naming the
36
+ # replacement.
36
37
  #
37
38
  # **Docker / cross-filesystem note.** Every path printed is resolved at runtime from this file's location, so it is
38
39
  # correct *on the filesystem where `rigor` runs*. If you run `rigor` inside a container but read files from the host
39
40
  # (or vice versa), the paths will not resolve — read them from the same environment that ran the command (`rigor
40
- # plugin print` inlines the body for exactly this case: it works with no file-reading tool at all).
41
+ # plugin <name>` inlines the body for exactly this case: it works with no file-reading tool at all).
41
42
  class PluginCommand < Command
42
43
  USAGE = <<~USAGE
43
- Usage: rigor plugin <subcommand> [args]
44
+ Usage: rigor plugin [<name>] [--path <name>] [--list] [--root]
44
45
 
45
46
  Browse the plugins bundled in the rigortype toolchain (worked
46
47
  examples for authoring your own). For the activation status of
47
48
  the plugins in your .rigor.yml, use `rigor plugins` (plural).
48
49
 
49
- Subcommands:
50
- list List bundled + example plugins (default)
51
- path <name> Print the absolute directory path of <name>
52
- print <name> Print <name>'s main lib source, with a header
53
- root Print the gem root + key subdirectories
50
+ rigor plugin List bundled + example plugins
51
+ rigor plugin <name> Print <name>'s main lib source, with a header
52
+ rigor plugin --path <name> Print the absolute directory path of <name>
53
+ rigor plugin --list List bundled + example plugins
54
+ rigor plugin --root Print the gem root + key subdirectories
54
55
 
55
56
  Examples:
56
- rigor plugin list
57
- rigor plugin path rigor-activerecord
58
- rigor plugin print rigor-activesupport-core-ext
59
- rigor plugin root
57
+ rigor plugin
58
+ rigor plugin rigor-activesupport-core-ext
59
+ rigor plugin --path rigor-activerecord
60
+ rigor plugin --root
60
61
  USAGE
61
62
 
63
+ # The pre-ADR-126 verbs, each with the spelling that replaces it.
64
+ DEPRECATED_VERBS = {
65
+ "list" => "rigor plugin --list",
66
+ "path" => "rigor plugin --path <name>",
67
+ "print" => "rigor plugin <name>",
68
+ "root" => "rigor plugin --root"
69
+ }.freeze
70
+
62
71
  # The bundled plugins/examples/source live at `<gem_root>/...`. From `lib/rigor/cli/plugin_command.rb` the gem
63
72
  # root is three directories up (matching SkillCommand::SKILLS_ROOT).
64
73
  GEM_ROOT = File.expand_path("../../..", __dir__).freeze
65
74
  PLUGINS_ROOT = File.join(GEM_ROOT, "plugins").freeze
66
75
  EXAMPLES_ROOT = File.join(GEM_ROOT, "examples").freeze
67
76
 
77
+ # The parser this command parses with, built without parsing anything: the catalogue reads its flags from it
78
+ # (ADR-126 WD3). `options` is only written by the switches' blocks.
79
+ def option_parser(options = {})
80
+ OptionParser.new do |opts|
81
+ opts.banner = USAGE.lines.first.chomp
82
+ opts.on("--path NAME", "Print the absolute directory path of a plugin") { |value| options[:path] = value }
83
+ opts.on("--list", "List bundled + example plugins") { options[:list] = true }
84
+ opts.on("--root", "Print the gem root + key subdirectories") { options[:root] = true }
85
+ end
86
+ end
87
+
68
88
  # @return CLI exit status.
69
89
  def run
70
- subcommand = @argv.shift || "list"
71
-
72
- case subcommand
73
- when "list" then run_list
74
- when "path" then run_path
75
- when "print" then run_print
76
- when "root" then run_root
77
- when "-h", "--help", "help"
90
+ if @argv.intersect?(%w[-h --help])
78
91
  @out.puts(USAGE)
79
- 0
80
- else
81
- @err.puts("Unknown subcommand: #{subcommand}")
82
- @err.puts(USAGE)
83
- Rigor::CLI::EXIT_USAGE
92
+ return 0
84
93
  end
94
+
95
+ verb = @argv.first
96
+ return run_deprecated_verb(@argv.shift) if DEPRECATED_VERBS.key?(verb)
97
+ return run_help if verb == "help"
98
+
99
+ run_grammar
85
100
  end
86
101
 
87
102
  private
88
103
 
104
+ def run_help
105
+ @out.puts(USAGE)
106
+ 0
107
+ end
108
+
109
+ def run_grammar
110
+ options = {}
111
+ begin
112
+ option_parser(options).parse!(@argv)
113
+ rescue OptionParser::ParseError => e
114
+ return usage_error(e.message)
115
+ end
116
+ return run_path(options[:path]) if options.key?(:path)
117
+ return run_root if options[:root]
118
+ return run_list if options[:list]
119
+
120
+ name = @argv.shift
121
+ name.nil? ? run_list : run_print(name)
122
+ end
123
+
124
+ def run_deprecated_verb(verb)
125
+ @err.puts("rigor: `rigor plugin #{verb}` is deprecated and will be removed in the next minor release; " \
126
+ "use `#{DEPRECATED_VERBS.fetch(verb)}`.")
127
+ # The verbs' stdout stays byte-identical to what they printed before the grammar changed, so its prose still
128
+ # names the verb spelling.
129
+ @legacy = true
130
+ case verb
131
+ when "list" then run_list
132
+ when "path" then run_path(@argv.shift)
133
+ when "print" then run_print(@argv.shift)
134
+ else run_root
135
+ end
136
+ end
137
+
89
138
  def run_list
90
139
  plugins = discover(PLUGINS_ROOT)
91
140
  examples = discover(EXAMPLES_ROOT)
@@ -113,8 +162,7 @@ module Rigor
113
162
  end
114
163
  end
115
164
 
116
- def run_path
117
- name = @argv.shift
165
+ def run_path(name)
118
166
  return usage_error("`path` requires a plugin name") if name.nil?
119
167
 
120
168
  plugin = find(name)
@@ -124,8 +172,7 @@ module Rigor
124
172
  0
125
173
  end
126
174
 
127
- def run_print
128
- name = @argv.shift
175
+ def run_print(name)
129
176
  return usage_error("`print` requires a plugin name") if name.nil?
130
177
 
131
178
  plugin = find(name)
@@ -159,7 +206,7 @@ module Rigor
159
206
  0
160
207
  end
161
208
 
162
- # The header that precedes the plugin body when an author runs `rigor plugin print <name>`. `# `-prefixed so the
209
+ # The header that precedes the plugin body when an author runs `rigor plugin <name>`. `# `-prefixed so the
163
210
  # combined output stays readable; the Ruby body below it is unchanged.
164
211
  def render_print_header(plugin)
165
212
  dir = plugin.fetch(:path)
@@ -176,7 +223,7 @@ module Rigor
176
223
  # The main source body is below; read the other files from the
177
224
  # paths above. To suppress `call.undefined-method` for methods a
178
225
  # DSL generates, study how an RBS-bundle plugin ships `sig/` (see
179
- # `rigor plugin print rigor-activesupport-core-ext`).
226
+ # `#{@legacy ? 'rigor plugin print' : 'rigor plugin'} rigor-activesupport-core-ext`).
180
227
  HEADER
181
228
  end
182
229
 
@@ -211,12 +258,12 @@ module Rigor
211
258
  def docker_note
212
259
  "\nNote: paths are local to where `rigor` runs. If you run rigor in a " \
213
260
  "container,\nread these files from the same filesystem (use " \
214
- "`rigor plugin print` to inline a body)."
261
+ "`#{@legacy ? 'rigor plugin print' : 'rigor plugin <name>'}` to inline a body)."
215
262
  end
216
263
 
217
264
  def name_error(name)
218
265
  @err.puts("Unknown plugin: #{name}")
219
- @err.puts("Run `rigor plugin list` to see the bundled plugins.")
266
+ @err.puts("Run `rigor plugin --list` to see the bundled plugins.")
220
267
  1
221
268
  end
222
269
 
@@ -30,7 +30,7 @@ module Rigor
30
30
  # - every manifest-declared extension surface
31
31
  # (`open_receivers:` / `owns_receivers:` / `produces:` /
32
32
  # `consumes:` / `block_as_methods:` / `heredoc_templates:` /
33
- # `trait_registries:` /
33
+ # `trait_registries:` / `per_invocation_classes:` /
34
34
  # `type_node_resolvers:` / `hkt_registrations:` /
35
35
  # `hkt_definitions:` / `protocol_contracts:` /
36
36
  # `source_rbs_synthesizer:`);
@@ -80,6 +80,20 @@ module Rigor
80
80
  0
81
81
  end
82
82
 
83
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
84
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
85
+ def option_parser(options = {})
86
+ OptionParser.new do |opts|
87
+ opts.banner = USAGE
88
+ Options.add_config(opts, options)
89
+ Options.add_format(opts, options, "Output format: text (default) or json")
90
+ opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
91
+ opts.on("--capabilities", "Emit the per-plugin extension-protocol catalogue (ADR-37)") do
92
+ options[:capabilities] = true
93
+ end
94
+ end
95
+ end
96
+
83
97
  private
84
98
 
85
99
  # Picks the renderer view. `--capabilities` switches to the focused extension-protocol catalogue (ADR-37 §
@@ -97,25 +111,10 @@ module Rigor
97
111
 
98
112
  def parse_options
99
113
  options = { config: nil, format: "text", strict: false, capabilities: false }
100
- OptionParser.new do |opts|
101
- opts.banner = USAGE
102
- Options.add_config(opts, options)
103
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
104
- opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
105
- opts.on("--capabilities", "Emit the per-plugin extension-protocol catalogue (ADR-37)") do
106
- options[:capabilities] = true
107
- end
108
- end.parse!(@argv)
109
- validate!(options)
114
+ option_parser(options).parse!(@argv)
110
115
  options
111
116
  end
112
117
 
113
- def validate!(options)
114
- return if %w[text json].include?(options.fetch(:format))
115
-
116
- raise OptionParser::InvalidArgument, "unsupported format: #{options.fetch(:format)}"
117
- end
118
-
119
118
  # Runs the plugin loader against the project configuration and returns a row per declared plugin entry. Each row
120
119
  # is a plain Hash with the fields enumerated in the class docstring so the renderer (text + JSON) reads from a
121
120
  # single shape.
@@ -204,7 +203,7 @@ module Rigor
204
203
  .merge(load_error: nil)
205
204
  end
206
205
 
207
- # ADR-37 narrow extension protocols. Unlike the 10 declarative manifest fields, these are class-level DSLs
206
+ # ADR-37 narrow extension protocols. Unlike the declarative manifest fields, these are class-level DSLs
208
207
  # (`node_rule` / `dynamic_return` / `narrowing_facts`), so they are read off the plugin class rather than the
209
208
  # manifest. The gate values — node types, receiver class names, specified method names — are the greppable,
210
209
  # enumerable surface the capability catalogue exposes.
@@ -1,11 +1,12 @@
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
7
8
  # Renderer for `rigor plugins`. Produces a human-readable text table and a JSON representation from the same row
8
- # shape (the Hash documented on {Rigor::CLI::PluginsCommand#loaded_row}).
9
+ # shape (the Hash {Rigor::CLI::PluginsCommand#build_rows} returns).
9
10
  #
10
11
  # The two formats carry the same content; JSON is meant for tooling (SKILLs, CI, editor integrations) while text is
11
12
  # for interactive inspection. Rows are printed in the order the loader resolved them.
@@ -44,7 +45,7 @@ module Rigor
44
45
  "available" => @inflection.fetch(:available)
45
46
  }
46
47
  end
47
- JSON.pretty_generate(payload)
48
+ JSON.pretty_generate(JsonDocument.stamp(payload, mode: "status"))
48
49
  end
49
50
 
50
51
  # ADR-37 § "Machine-readable capability catalogue" — the focused per-plugin extension-protocol dump. Only loaded
@@ -53,10 +54,13 @@ module Rigor
53
54
  # type-specifier methods, and produced / consumed facts.
54
55
  def capabilities_json
55
56
  JSON.pretty_generate(
56
- {
57
- "configuration" => @configuration_path,
58
- "capabilities" => loaded_rows.map { |row| capabilities_json_for(row) }
59
- }
57
+ JsonDocument.stamp(
58
+ {
59
+ "configuration" => @configuration_path,
60
+ "capabilities" => loaded_rows.map { |row| capabilities_json_for(row) }
61
+ },
62
+ mode: "capabilities"
63
+ )
60
64
  )
61
65
  end
62
66
 
@@ -31,8 +31,8 @@ module Rigor
31
31
  # @return one entry per path.
32
32
  def run(paths:, scanner:, environment:, configuration:, workers:)
33
33
  # Force the full RBS load on the parent so children copy-on-write inherit a warm environment rather
34
- # than each rebuilding it after the fork (mirrors the check fork pool's parent-side prewarm). A
35
- # no-op on the sequential path but cheap.
34
+ # than each rebuilding it after the fork (mirrors the check fork pool's parent-side prewarm).
35
+ # Skipped when the run is sequential.
36
36
  environment.rbs_loader&.prewarm if Inference::ForkMap.parallel?([workers, paths.size].min)
37
37
 
38
38
  payloads = Inference::ForkMap.call(items: paths, workers: workers) do |slice|
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  require_relative "../inference/dynamic_origin"
6
+ require_relative "json_document"
6
7
 
7
8
  module Rigor
8
9
  class CLI
@@ -32,7 +33,7 @@ module Rigor
32
33
  private
33
34
 
34
35
  def render_json(report)
35
- @out.puts(JSON.pretty_generate(report.to_h))
36
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(report.to_h, mode: "protection")))
36
37
  end
37
38
 
38
39
  def render_text(report)
@@ -7,6 +7,7 @@ require_relative "../bleeding_edge"
7
7
  require_relative "../configuration"
8
8
  require_relative "command"
9
9
  require_relative "options"
10
+ require_relative "json_document"
10
11
 
11
12
  module Rigor
12
13
  class CLI
@@ -35,19 +36,23 @@ module Rigor
35
36
  0
36
37
  end
37
38
 
38
- private
39
-
40
- def parse_options
41
- options = { format: "text", config: nil }
39
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
40
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
41
+ def option_parser(options = {})
42
42
  OptionParser.new do |opt|
43
43
  opt.banner = USAGE
44
- opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
45
- options[:format] = fmt
46
- end
44
+ Options.add_format(opt, options, "Output format (text | json). Default: text.")
47
45
  opt.on("--config=PATH", "Path to a .rigor.yml (default: auto-discovery).") do |path|
48
46
  options[:config] = Options.config_path(path)
49
47
  end
50
- end.parse!(@argv)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def parse_options
54
+ options = { format: "text", config: nil }
55
+ option_parser(options).parse!(@argv)
51
56
  options
52
57
  end
53
58
 
@@ -60,13 +65,14 @@ module Rigor
60
65
 
61
66
  def render_json(configuration)
62
67
  selector = configuration.bleeding_edge
63
- @out.puts(JSON.pretty_generate(
64
- "overlay" => BleedingEdge.features.map(&:to_h),
65
- "selector" => configuration.to_h.fetch("bleeding_edge"),
66
- "active" => BleedingEdge.active_features(selector).map(&:id),
67
- "unknown_selected" => BleedingEdge.unknown_selected_ids(selector),
68
- "graduated" => BleedingEdge::GRADUATED
69
- ))
68
+ payload = {
69
+ "overlay" => BleedingEdge.features.map(&:to_h),
70
+ "selector" => configuration.to_h.fetch("bleeding_edge"),
71
+ "active" => BleedingEdge.active_features(selector).map(&:id),
72
+ "unknown_selected" => BleedingEdge.unknown_selected_ids(selector),
73
+ "graduated" => BleedingEdge::GRADUATED
74
+ }
75
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(payload)))
70
76
  end
71
77
 
72
78
  def render_text(configuration)
@@ -38,7 +38,11 @@ module Rigor
38
38
 
39
39
  VALID_MODES = %w[print diff write check].freeze
40
40
  VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
41
- VALID_FORMATS = %w[text json].freeze
41
+
42
+ # The per-target outcomes `--write` refuses: the file is left untouched and the run exits 1. `--check` counts
43
+ # them apart from the targets it would update, since `--write` does not fix a refused file.
44
+ TARGET_REFUSALS = %i[skipped_invalid_rbs skipped_invalid_encoding skipped_unparseable].freeze
45
+ private_constant :TARGET_REFUSALS
42
46
 
43
47
  # The skip reasons {#report_skipped} counts. The four left out each have a report of their own
44
48
  # ({#report_unrenderable}, {#report_unresolvable_superclasses}, {#report_inline_declared}, and the refusal
@@ -61,17 +65,18 @@ module Rigor
61
65
 
62
66
  observation_roots = observation_roots(configuration, options)
63
67
  observations = collect_observations(configuration, options, observation_roots, paths)
68
+ mode = options.fetch(:mode).to_sym
64
69
  generator = SigGen::Generator.new(configuration: configuration, paths: paths,
65
70
  observations: observations,
66
71
  include_private: options.fetch(:include_private),
67
72
  effect_annotator: effect_annotator(configuration, paths, options),
68
- overwrite: options.fetch(:overwrite))
73
+ overwrite: options.fetch(:overwrite),
74
+ target_writer: target_writer(configuration, options, mode))
69
75
  candidates = generator.run
70
- mode = options.fetch(:mode).to_sym
71
76
 
72
77
  status = case mode
73
- when :write then dispatch_write(candidates, configuration, options)
74
- when :check then dispatch_check(candidates, configuration, options)
78
+ when :write then dispatch_write(candidates, configuration, options, generator)
79
+ when :check then dispatch_check(candidates, configuration, options, generator)
75
80
  else
76
81
  dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
77
82
  0
@@ -86,7 +91,12 @@ module Rigor
86
91
  report_inline_declared(candidates, options)
87
92
  report_withheld_annotations(candidates, options)
88
93
  report_unrenderable(generator.unrenderable)
89
- report_unresolvable_superclasses(generator.unresolvable_superclasses)
94
+ report_unresolvable_superclasses(generator.unresolvable_superclasses, generator)
95
+ end
96
+
97
+ # The parser `run` parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
98
+ def option_parser(options = {})
99
+ build_option_parser(options)
90
100
  end
91
101
 
92
102
  private
@@ -217,16 +227,36 @@ module Rigor
217
227
  # declaring it would fail to build and take the class's type coverage with it. Naming the unresolved
218
228
  # superclass is the actionable part — it is usually one gem's missing RBS standing between the user
219
229
  # and signatures for a whole layer of their app.
220
- def report_unresolvable_superclasses(unresolvable)
221
- return if unresolvable.empty?
230
+ #
231
+ # Issue #2437 — a superclass the project declares in source but this run wrote nothing for gets different
232
+ # advice: no gem RBS is missing; the superclass's own methods could not be typed, or were not written.
233
+ def report_unresolvable_superclasses(unresolvable, generator)
234
+ reports = unresolvable.map { |name, superclass| [name, *generator.unresolvable_superclass_report(superclass)] }
235
+ project, external = reports.partition { |_name, kind, _root| kind == :project }
236
+ .map { |rows| rows.to_h { |name, _kind, root| [name, root] } }
237
+ report_unwritten_project_superclasses(project)
238
+ return if external.empty?
222
239
 
223
240
  @err.puts(
224
- "rigor sig-gen: skipped #{unresolvable.size} class(es) whose superclass no loaded RBS declares. " \
241
+ "rigor sig-gen: skipped #{external.size} class(es) whose superclass no loaded RBS declares. " \
225
242
  "A signature for them would fail to build (`RBS::NoSuperclassFoundError`) and would leave the " \
226
243
  "class LESS typed than it is now. Provide RBS for the superclass — `rbs collection install`, the " \
227
244
  "gem's own `sig/`, or a `signature_paths:` entry — and re-run."
228
245
  )
229
- unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
246
+ external.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
247
+ end
248
+
249
+ def report_unwritten_project_superclasses(project)
250
+ return if project.empty?
251
+
252
+ @err.puts(
253
+ "rigor sig-gen: skipped #{project.size} class(es) whose superclass this run wrote no signature for. " \
254
+ "The superclass is declared in the project's source, but its own methods could not be typed, or its " \
255
+ "`sig/` file could not be written, so a signature for these subclasses would fail to build " \
256
+ "(`RBS::NoSuperclassFoundError`). Annotate or type the superclass's methods, fix its `sig/` file, or " \
257
+ "hand-write its declaration, and re-run."
258
+ )
259
+ project.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
230
260
  end
231
261
 
232
262
  # #1991 — the path errors `rigor check` reports for the arguments: a path that is neither a directory nor a
@@ -265,18 +295,19 @@ module Rigor
265
295
  end
266
296
 
267
297
  # @return exit status — non-zero when a file the user asked to write could not be written.
268
- def dispatch_write(candidates, configuration, options)
269
- results = build_writer(configuration, options).write_all(candidates)
298
+ def dispatch_write(candidates, configuration, options, generator)
299
+ writer = build_writer(configuration, options)
300
+ results = writer.write_all(candidates, header_spelling: generator.method(:generic_header_spelling))
270
301
 
271
302
  refused = refused_candidates(candidates)
272
- SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused)
303
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused,
304
+ left_bare: writer.left_bare)
273
305
  SigGen::Renderer.refusal_lines(refused).each { |line| @err.puts(line) }
274
- # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8,
275
- # or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the user asked for a
276
- # write and did not get one, so the command must not report success — a green `sig-gen --write` in CI
277
- # would otherwise mean nothing.
278
- refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
279
- results.any? { |result| refusals.include?(result.action) } || !refused.empty? ? 1 : 0
306
+ # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8 or
307
+ # does not parse, or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the
308
+ # user asked for a write and did not get one, so the command must not report success — a green
309
+ # `sig-gen --write` in CI would otherwise mean nothing.
310
+ results.any? { |result| TARGET_REFUSALS.include?(result.action) } || !refused.empty? ? 1 : 0
280
311
  end
281
312
 
282
313
  # ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
@@ -292,11 +323,15 @@ module Rigor
292
323
  # --overwrite` would apply it.
293
324
  #
294
325
  # @return 1 when `sig/` is out of date (or a write would be refused), 0 when it is current.
295
- def dispatch_check(candidates, configuration, options)
296
- results = build_writer(configuration, options, dry_run: true).write_all(candidates)
326
+ #
327
+ # A generic header held back from the repair (#2605) is reported, but does not fail the check: `--write`
328
+ # cannot change it, and it was failing the project's env build before the run as well.
329
+ def dispatch_check(candidates, configuration, options, generator)
330
+ writer = build_writer(configuration, options, dry_run: true)
331
+ results = writer.write_all(candidates, header_spelling: generator.method(:generic_header_spelling))
297
332
  refused = refused_candidates(candidates)
298
333
  SigGen::Renderer.new(out: @out).render_check(results: results, format: options.fetch(:format),
299
- refused: refused)
334
+ refused: refused, left_bare: writer.left_bare)
300
335
  stale = SigGen::Renderer.out_of_date(results)
301
336
  return 0 if stale.empty? && refused.empty?
302
337
 
@@ -305,16 +340,40 @@ module Rigor
305
340
  end
306
341
 
307
342
  def report_check_failure(stale, refused)
308
- unless stale.empty?
309
- @err.puts("rigor sig-gen --check: #{stale.size} signature file(s) out of date; " \
343
+ pending, refusals = stale.partition { |result| !TARGET_REFUSALS.include?(result.action) }
344
+ unless pending.empty?
345
+ @err.puts("rigor sig-gen --check: #{pending.size} signature file(s) out of date; " \
310
346
  "run `rigor sig-gen --write` with the same options to update them.")
311
347
  end
348
+ @err.puts(refused_targets_line(refusals)) unless refusals.empty?
312
349
  return if refused.empty?
313
350
 
314
351
  @err.puts("rigor sig-gen --check: #{refused.size} method(s) whose inline declaration and sig/ copy " \
315
352
  "disagree; make them agree, or pass --overwrite to replace the sig/ member.")
316
353
  end
317
354
 
355
+ # A target `--write` refuses is not one it would update: it is counted apart from the out-of-date targets. Where
356
+ # the existing file is the user's to repair (encoding, parse), it is named for a hand fix. An invalid generated
357
+ # file is a Rigor rendering bug (its REFUSED line says so), so it is counted but never listed as a hand fix.
358
+ def refused_targets_line(results)
359
+ by_hand = results.reject { |result| result.action == :skipped_invalid_rbs }
360
+ .map { |result| result.target_path.to_s }
361
+ head = "rigor sig-gen --check: #{results.size} signature file(s) refused; `--write` does not update them"
362
+ if by_hand.empty?
363
+ return "#{head}. The generated RBS did not parse: a Rigor bug, see its REFUSED line on stdout."
364
+ end
365
+
366
+ "#{head}. Repair by hand: #{by_hand.join(', ')}."
367
+ end
368
+
369
+ # #2461 — only a run that writes or compares files asks whether the writer will accept each target: where a
370
+ # target lands depends on the working directory, and print, diff and JSON output must not.
371
+ def target_writer(configuration, options, mode)
372
+ return nil unless %i[write check].include?(mode)
373
+
374
+ build_writer(configuration, options, dry_run: true)
375
+ end
376
+
318
377
  def build_writer(configuration, options, dry_run: false)
319
378
  layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
320
379
  path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
@@ -426,7 +485,7 @@ module Rigor
426
485
  opts.on("--no-cache", "Do not read or write the analysis cache (effect collection only)") do
427
486
  options[:no_cache] = true
428
487
  end
429
- opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
488
+ Options.add_format(opts, options, "Output format: text or json")
430
489
  opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
431
490
  options[:params] = value
432
491
  end
@@ -456,11 +515,9 @@ module Rigor
456
515
 
457
516
  def validation_error(options)
458
517
  mode = options.fetch(:mode)
459
- format = options.fetch(:format)
460
518
  params = options.fetch(:params)
461
519
 
462
520
  return "--print, --diff, --write, and --check are mutually exclusive flags; pick one" if mode == "conflict"
463
- return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
464
521
  return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
465
522
  if params == "observed-strict"
466
523
  return "--params=observed-strict is reserved until the capability-role catalog ships"