rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -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
@@ -8,14 +8,14 @@ require_relative "options"
8
8
  require_relative "check_runner_factory"
9
9
  require_relative "../configuration"
10
10
  require_relative "../lens"
11
+ require_relative "../lens/claims"
11
12
 
12
13
  module Rigor
13
14
  class CLI
14
15
  # `rigor lens FILE...` — a declaration map of the files with type provenance, computed as `rigor check`
15
16
  # computes them (ADR-113; output contract docs/internal-spec/lens.md). Experimental until `lens/v1`.
16
17
  class LensCommand < Command
17
- USAGE = "Usage: rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE..."
18
- FORMATS = %w[text json].freeze
18
+ USAGE = "Usage: rigor lens [--format=text|json] [--claims=REQUEST] [--config=PATH] [--workers=N] FILE..."
19
19
 
20
20
  def run
21
21
  options = parse_options
@@ -26,27 +26,46 @@ module Rigor
26
26
  config_path = options.fetch(:config) || Configuration.discover
27
27
  # The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
28
28
  root = Dir.pwd
29
+ request = options[:claims] && Lens::Claims::Request.read(options[:claims], root: root, arguments: @argv)
29
30
  workers = CheckRunnerFactory.resolve_workers(options, configuration)
30
- outcome = Lens::Run.new(configuration: configuration, root: root, arguments: @argv, workers: workers).call
31
+ run = Lens::Run.new(configuration: configuration, root: root, arguments: @argv, workers: workers)
32
+ outcome = run.call
31
33
  document = Lens::Document.new(outcome: outcome, configuration: configuration, root: root,
32
34
  config_path: config_path).to_h
35
+ attach_claims(document, request, outcome, run)
33
36
  @err.puts("rigor lens: #{outcome.warm ? 'warm' : 'cold'} — #{@argv.size} target(s)")
34
37
  render(document, options.fetch(:format))
35
38
  outcome.targets.any? { |target| target.status == "ok" } ? 0 : 1
39
+ rescue Lens::Claims::IntegrityError => e
40
+ usage_error(e.message)
41
+ CLI::EXIT_USAGE
36
42
  end
37
43
 
38
- private
39
-
40
- def parse_options
41
- options = { format: "text", config: nil, workers: nil }
44
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
45
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
46
+ def option_parser(options = {})
42
47
  OptionParser.new do |opts|
43
48
  opts.banner = USAGE
44
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
49
+ Options.add_format(opts, options, "Output format: text (default) or json")
45
50
  opts.on("--workers=N", Integer, "Worker processes for a cold run") { |value| options[:workers] = value }
46
51
  Options.add_config(opts, options)
47
- end.parse!(@argv)
48
- return usage_error("--format must be text or json") unless FORMATS.include?(options[:format])
52
+ opts.on("--claims=REQUEST", "Check documentation claims (requires --format=json)") do |value|
53
+ options[:claims] = value
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def attach_claims(document, request, outcome, run)
61
+ document["claims"] = Lens::Claims.new(request: request, outcome: outcome, run: run).call if request
62
+ end
63
+
64
+ def parse_options
65
+ options = { format: "text", config: nil, workers: nil }
66
+ option_parser(options).parse!(@argv)
49
67
  return usage_error("no FILE given") if @argv.empty?
68
+ return usage_error("--claims requires --format=json") if options[:claims] && options[:format] != "json"
50
69
 
51
70
  options
52
71
  rescue OptionParser::ParseError => e
@@ -4,6 +4,7 @@ require_relative "command"
4
4
  require_relative "options"
5
5
  require_relative "../runtime/jit"
6
6
 
7
+ require "json"
7
8
  require "optionparser"
8
9
 
9
10
  module Rigor
@@ -15,6 +16,179 @@ module Rigor
15
16
  class LspCommand < Command
16
17
  USAGE = "Usage: rigor lsp [options]"
17
18
 
19
+ # Records each JSON-RPC message one per line, prefixed with the direction (`<-` read from the client, `->`
20
+ # written to it) and a timestamp. Only the message framing is logged; the server's own debug output joins the
21
+ # same file through `$stderr` (see {#with_debug_output}).
22
+ #
23
+ # It never raises into the protocol path. A message the JSON encoder rejects (raw invalid UTF-8 in a string) is
24
+ # logged as a scrubbed rendering, so its line is still written. Any IO failure disables the log for the rest of
25
+ # the session: records are dropped, and debug output falls back to `fallback` (the real stderr). Nothing is
26
+ # `warn`ed into the failing log.
27
+ class WireLog
28
+ def initialize(io, fallback)
29
+ @io = io
30
+ @fallback = fallback
31
+ @broken = false
32
+ end
33
+
34
+ def record(direction, message)
35
+ deliver("#{stamp} #{direction} #{render(message)}\n")
36
+ nil
37
+ end
38
+
39
+ # Debug output written to `$stderr` while the session runs (`Kernel#warn` calls `write`). The parts are joined
40
+ # as binary, so an odd encoding in one part cannot raise here.
41
+ def write(*parts)
42
+ text = parts.map { |part| part.to_s.b }.join
43
+ @fallback.write(text) unless deliver(text)
44
+ text.bytesize
45
+ end
46
+
47
+ def print(*parts)
48
+ write(*parts)
49
+ nil
50
+ end
51
+
52
+ def puts(*args)
53
+ lines = if args.empty?
54
+ ["\n"]
55
+ else
56
+ args.flatten.map { |arg| arg.to_s.end_with?("\n") ? arg.to_s : "#{arg}\n" }
57
+ end
58
+ write(*lines)
59
+ nil
60
+ end
61
+
62
+ def printf(format_string, *)
63
+ write(format(format_string, *))
64
+ nil
65
+ end
66
+
67
+ def putc(char)
68
+ write(char.is_a?(Integer) ? char.chr : char.to_s[0])
69
+ char
70
+ end
71
+
72
+ def syswrite(text)
73
+ write(text)
74
+ end
75
+
76
+ def <<(part)
77
+ write(part)
78
+ self
79
+ end
80
+
81
+ def flush
82
+ self
83
+ end
84
+
85
+ # The exception that escaped the server loop, with its backtrace, so a crash is in the log, not only on stderr.
86
+ def crash(error)
87
+ lines = ["#{stamp} ! #{error.class}: #{error.message}\n"]
88
+ lines.concat((error.backtrace || []).map { |frame| " #{frame}\n" })
89
+ text = lines.join
90
+ @fallback.write(text) unless deliver(text)
91
+ nil
92
+ end
93
+
94
+ def close
95
+ @io.close unless @io.closed?
96
+ end
97
+
98
+ # Every other IO method (`tty?`, `closed?`, `fileno`, `sync=`, `to_io` for `system(err: $stderr)`, ...) goes to
99
+ # the current sink: the file while the log works, the real stderr once it has failed.
100
+ def method_missing(name, ...)
101
+ return super unless sink.respond_to?(name)
102
+
103
+ sink.public_send(name, ...)
104
+ end
105
+
106
+ def respond_to_missing?(name, include_private = false)
107
+ sink.respond_to?(name) || super
108
+ end
109
+
110
+ private
111
+
112
+ def sink
113
+ @broken ? @fallback : @io
114
+ end
115
+
116
+ # Writes `text` to the file. Returns false when the log is (or just became) unusable. Only a failure of the
117
+ # write itself disables the log.
118
+ def deliver(text)
119
+ return false if @broken
120
+
121
+ @io.write(text)
122
+ true
123
+ rescue StandardError
124
+ @broken = true
125
+ false
126
+ end
127
+
128
+ def stamp
129
+ Time.now.strftime("%FT%T.%6N%:z")
130
+ end
131
+
132
+ # The JSON line for `message`. One the encoder rejects (invalid UTF-8, nesting past its limit, a `to_json` that
133
+ # raises) is rendered from its scrubbed copy, or from its `inspect` when even that does not encode.
134
+ def render(message)
135
+ JSON.generate(message)
136
+ rescue StandardError
137
+ begin
138
+ JSON.generate(scrub(message))
139
+ rescue StandardError
140
+ JSON.generate(message.inspect)
141
+ end
142
+ end
143
+
144
+ def scrub(value)
145
+ case value
146
+ when Hash then value.to_h { |key, val| [scrub(key), scrub(val)] }
147
+ when Array then value.map { |item| scrub(item) }
148
+ when Symbol then scrub(value.to_s).to_sym
149
+ when String then value.dup.force_encoding(Encoding::UTF_8).scrub
150
+ else value
151
+ end
152
+ end
153
+ end
154
+
155
+ # Wraps the framed reader so every request or notification it yields is logged before the Loop dispatches it.
156
+ class WireLogReader
157
+ def initialize(inner, log)
158
+ @inner = inner
159
+ @log = log
160
+ end
161
+
162
+ def read(&block)
163
+ @inner.read do |message|
164
+ @log.record("<-", message)
165
+ block.call(message)
166
+ end
167
+ end
168
+
169
+ def close
170
+ @inner.close
171
+ end
172
+ end
173
+
174
+ # Wraps the framed writer so every outgoing message is logged as it goes on the wire. It sits inside
175
+ # `SynchronizedWriter`, so the log line and the frame it describes are written under the same lock.
176
+ class WireLogWriter
177
+ def initialize(inner, log)
178
+ @inner = inner
179
+ @log = log
180
+ end
181
+
182
+ def write(message)
183
+ @log.record("->", message.merge(jsonrpc: "2.0"))
184
+ @inner.write(message)
185
+ end
186
+
187
+ def close
188
+ @inner.close
189
+ end
190
+ end
191
+
18
192
  # @return CLI exit status.
19
193
  def run
20
194
  options = parse_options
@@ -26,6 +200,9 @@ module Rigor
26
200
  return CLI::EXIT_USAGE
27
201
  end
28
202
 
203
+ log = open_wire_log(options.fetch(:log))
204
+ return CLI::EXIT_USAGE if log == :usage_error
205
+
29
206
  # A long-lived server always outlasts the JIT compile cost, so enable
30
207
  # YJIT at boot rather than on the analysis deadline (Runtime::Jit).
31
208
  Runtime::Jit.enable_now
@@ -34,23 +211,83 @@ module Rigor
34
211
  require_relative "../configuration"
35
212
  require "language_server-protocol"
36
213
 
37
- # STDIN is read frame-by-frame via the gem's `Io::Reader`; STDOUT is wrapped in `SynchronizedWriter` so
38
- # concurrent writes from the main dispatch thread + the Debouncer's async threads don't interleave frames. The
39
- # Loop runs until either STDIN hits EOF or `server.exited?`; the process then exits with the server's recorded
40
- # code (0 after a clean shutdown+exit, 1 otherwise).
41
- writer = LanguageServer::SynchronizedWriter.new(
42
- ::LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
43
- )
44
- server, loop_runner = build_server(writer: writer, config_path: options.fetch(:config))
45
- loop_runner.run
46
- server.exit_code || 0
214
+ begin
215
+ serve(config_path: options.fetch(:config), log: log)
216
+ ensure
217
+ log&.close
218
+ end
219
+ end
220
+
221
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
222
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
223
+ def option_parser(options = {})
224
+ OptionParser.new do |opts|
225
+ opts.banner = USAGE
226
+ opts.on("--transport=NAME", "Transport (default: stdio; only stdio supported in v1)") do |value|
227
+ options[:transport] = value
228
+ end
229
+ opts.on("--log=PATH",
230
+ "Append the LSP wire log and server debug output to PATH " \
231
+ "(default: no wire log; debug output goes to stderr)") do |value|
232
+ options[:log] = value
233
+ end
234
+ Options.add_config(opts, options)
235
+ end
47
236
  end
48
237
 
49
238
  private
50
239
 
240
+ # STDIN is read frame-by-frame via the gem's `Io::Reader`; STDOUT is wrapped in `SynchronizedWriter` so
241
+ # concurrent writes from the main dispatch thread + the Debouncer's async threads don't interleave frames. The
242
+ # Loop runs until either STDIN hits EOF or `server.exited?`; the process then exits with the server's recorded
243
+ # code (0 after a clean shutdown+exit, 1 otherwise). With a wire log, both directions are logged inside the lock.
244
+ def serve(config_path:, log:)
245
+ reader = ::LanguageServer::Protocol::Transport::Io::Reader.new($stdin)
246
+ frame_writer = ::LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
247
+ if log
248
+ reader = WireLogReader.new(reader, log)
249
+ frame_writer = WireLogWriter.new(frame_writer, log)
250
+ end
251
+ writer = LanguageServer::SynchronizedWriter.new(frame_writer)
252
+ server, loop_runner = build_server(writer: writer, reader: reader, config_path: config_path)
253
+ with_debug_output(log) { loop_runner.run }
254
+ server.exit_code || 0
255
+ end
256
+
257
+ # Opens PATH for appending with synchronous writes, so a crash loses no logged line. Returns nil without a
258
+ # PATH, the open file, or `:usage_error` after reporting why it could not be opened.
259
+ def open_wire_log(path)
260
+ return nil if path.nil?
261
+
262
+ io = File.new(path, "a")
263
+ io.sync = true
264
+ WireLog.new(io, $stderr)
265
+ rescue SystemCallError => e
266
+ @err.puts("rigor lsp: cannot open --log file #{path.inspect}: #{e.message}")
267
+ :usage_error
268
+ end
269
+
270
+ # Routes the server's debug output (`warn` and the Ruby runtime's warnings, both written to `$stderr`) into the
271
+ # wire log for the length of the session. An exception that escapes the loop is written to the log before
272
+ # `$stderr` is restored and the exception is re-raised. Without a log, `$stderr` is left alone.
273
+ def with_debug_output(log)
274
+ return yield if log.nil?
275
+
276
+ saved = $stderr
277
+ begin
278
+ $stderr = log
279
+ yield
280
+ rescue StandardError, ScriptError, SystemStackError => e
281
+ log.crash(e)
282
+ raise
283
+ ensure
284
+ $stderr = saved
285
+ end
286
+ end
287
+
51
288
  # Builds the full collaborator graph from a fresh `Configuration` + `ProjectContext`. Returns `[server, loop]` so
52
289
  # the caller drives the loop and reads `server.exit_code` for the process exit status.
53
- def build_server(writer:, config_path:) # rubocop:disable Metrics/MethodLength
290
+ def build_server(writer:, reader:, config_path:) # rubocop:disable Metrics/MethodLength
54
291
  configuration = Configuration.load(config_path)
55
292
  # ProjectContext caches Environment + Cache::Store across requests so hover / publish hit the warm path.
56
293
  # Invalidated by `workspace/didChangeWatchedFiles` and `workspace/didChangeConfiguration`; every save round
@@ -89,7 +326,7 @@ module Rigor
89
326
  project_context: project_context
90
327
  )
91
328
  loop_runner = LanguageServer::Loop.new(
92
- reader: ::LanguageServer::Protocol::Transport::Io::Reader.new($stdin),
329
+ reader: reader,
93
330
  writer: writer,
94
331
  server: server
95
332
  )
@@ -99,17 +336,7 @@ module Rigor
99
336
  def parse_options
100
337
  options = { transport: "stdio", log: nil, config: nil }
101
338
 
102
- parser = OptionParser.new do |opts|
103
- opts.banner = USAGE
104
- opts.on("--transport=NAME", "Transport (default: stdio; only stdio supported in v1)") do |value|
105
- options[:transport] = value
106
- end
107
- opts.on("--log=PATH", "Write LSP wire log + server debug to PATH (default: stderr)") do |value|
108
- options[:log] = value
109
- end
110
- Options.add_config(opts, options)
111
- end
112
- parser.parse!(@argv)
339
+ option_parser(options).parse!(@argv)
113
340
  options
114
341
  rescue OptionParser::ParseError => e
115
342
  @err.puts(e.message)
@@ -42,12 +42,10 @@ module Rigor
42
42
  0
43
43
  end
44
44
 
45
- private
46
-
47
- def parse_options
48
- options = { transport: "stdio", config: nil }
49
-
50
- parser = OptionParser.new do |opts|
45
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
46
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
47
+ def option_parser(options = {})
48
+ OptionParser.new do |opts|
51
49
  opts.banner = USAGE
52
50
  opts.on("--transport=NAME",
53
51
  "Transport (default: stdio; only stdio is supported in v1)") do |value|
@@ -58,7 +56,14 @@ module Rigor
58
56
  options[:config] = Options.config_path(value)
59
57
  end
60
58
  end
61
- parser.parse!(@argv)
59
+ end
60
+
61
+ private
62
+
63
+ def parse_options
64
+ options = { transport: "stdio", config: nil }
65
+
66
+ option_parser(options).parse!(@argv)
62
67
  options
63
68
  rescue OptionParser::ParseError => e
64
69
  @err.puts(e.message)
@@ -40,7 +40,7 @@ module Rigor
40
40
  # @return one entry per path.
41
41
  def run(paths:, scanner:, environment:, configuration:, workers:)
42
42
  # Force the full RBS load on the parent so children copy-on-write inherit a warm environment rather
43
- # than each rebuilding it after the fork. A no-op on the sequential path but cheap.
43
+ # than each rebuilding it after the fork. Skipped when the run is sequential.
44
44
  environment.rbs_loader&.prewarm if Inference::ForkMap.parallel?([workers, paths.size].min)
45
45
 
46
46
  payloads = Inference::ForkMap.call(items: paths, workers: workers) do |slice|
@@ -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
@@ -22,7 +23,7 @@ module Rigor
22
23
  private
23
24
 
24
25
  def render_json(report)
25
- @out.puts(JSON.pretty_generate(report.to_h))
26
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(report.to_h)))
26
27
  end
27
28
 
28
29
  def render_text(report)
@@ -46,7 +47,7 @@ module Rigor
46
47
  end
47
48
 
48
49
  # Issue #686 — the count that makes the command exit non-zero, on stdout beside the ratio it qualifies
49
- # (the stderr twin in `CoverageMutation#warn_unmeasured_files` carries the explanation).
50
+ # (the stderr twin in {MeasurementIntegrityWarning} carries the explanation).
50
51
  def render_unmeasured_files(report)
51
52
  count = report.unmeasured_files
52
53
  return if count.zero?
@@ -16,6 +16,13 @@ module Rigor
16
16
  module Options
17
17
  module_function
18
18
 
19
+ # The formats `parser` accepts for `--format`, or nil when {.add_format} did not define the switch. A
20
+ # block-validated switch carries no list of its own, so {.add_format} leaves it on the parser for the CLI
21
+ # catalogue (ADR-126 WD3) to read.
22
+ def accepted_formats(parser)
23
+ parser.instance_variable_get(:@rigor_accepted_formats)
24
+ end
25
+
19
26
  # Defines the standard `--config=PATH` flag on `parser`, writing the path into `options[:config]`. Used by every
20
27
  # subcommand that loads a `.rigor.yml`; the few whose `--config` help text is intentionally bespoke (`diff`,
21
28
  # `mcp`, `show-bleedingedge`) keep their own `parser.on` and call {.config_path} for the same check.
@@ -25,6 +32,20 @@ module Rigor
25
32
  end
26
33
  end
27
34
 
35
+ # Defines `--format=FORMAT` restricted to `formats`, so an unsupported value is an `OptionParser::InvalidArgument`
36
+ # raised while the options are parsed (#2502). A check left to the renderer ran the whole analysis first, and a
37
+ # renderer that read only `format == "json"` printed text for any other value. The match is exact: an
38
+ # OptionParser enum would also complete a prefix (`--format=j`), which then turns ambiguous the day a second
39
+ # format shares it. The renderers' own "unsupported format" branches remain as a guard against internal drift.
40
+ def add_format(parser, options, description, formats: %w[text json])
41
+ parser.instance_variable_set(:@rigor_accepted_formats, formats.dup.freeze)
42
+ parser.on("--format=FORMAT", description) do |value|
43
+ raise OptionParser::InvalidArgument, value unless formats.include?(value)
44
+
45
+ options[:format] = value
46
+ end
47
+ end
48
+
28
49
  # The `--config` value, checked where the flag is read (#1992). A named file that is not there is an error, not
29
50
  # a request for the defaults: the run would otherwise analyse under a configuration the caller did not ask for,
30
51
  # and an MCP caller would get a result for it with no sign anything was missing. Auto-discovery, with no
@@ -42,22 +63,40 @@ module Rigor
42
63
  def add_editor_mode(parser, options)
43
64
  parser.on("--tmp-file=PATH",
44
65
  "Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
66
+ note_repeated_editor_flag(options, :tmp_file, "--tmp-file")
45
67
  options[:tmp_file] = value
46
68
  end
47
69
  parser.on("--instead-of=PATH",
48
70
  "Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
71
+ note_repeated_editor_flag(options, :instead_of, "--instead-of")
49
72
  options[:instead_of] = value
50
73
  end
51
74
  end
52
75
 
76
+ # Issue #2233 — editor mode binds one buffer per run, so a second `--tmp-file` or `--instead-of` cannot be
77
+ # honoured beside the first. Overwriting it dropped the first pair with no message; the repeat is recorded here
78
+ # and rejected by {resolve_buffer_binding} as a usage error instead.
79
+ def note_repeated_editor_flag(options, key, flag)
80
+ return if options[key].nil?
81
+
82
+ (options[:repeated_editor_flags] ||= []) << flag
83
+ end
84
+
53
85
  # Resolves the editor-mode buffer binding from parsed `options`: returns nil when neither editor-mode flag is set,
54
86
  # a `Analysis::BufferBinding` when the pair is valid, or `:usage_error` (after writing the reason to `err`) when
55
- # the flags are unpaired or the temp file is unreadable.
87
+ # a flag is repeated, the flags are unpaired, or the temp file is unreadable.
56
88
  def resolve_buffer_binding(options, err:)
57
89
  tmp = options[:tmp_file]
58
90
  instead = options[:instead_of]
59
91
  return nil if tmp.nil? && instead.nil?
60
92
 
93
+ repeated = Array(options[:repeated_editor_flags])
94
+ unless repeated.empty?
95
+ err.puts("#{repeated.join(' and ')} given more than once; editor mode takes one buffer per run " \
96
+ "(a single --tmp-file / --instead-of pair)")
97
+ return :usage_error
98
+ end
99
+
61
100
  if tmp.nil? || instead.nil?
62
101
  err.puts("--tmp-file and --instead-of must appear together")
63
102
  return :usage_error