rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -11,6 +11,8 @@ require_relative "../../effects/signature_sources"
11
11
  require_relative "../../rbs_extended/conformance_checker"
12
12
  require_relative "../../runtime/jit"
13
13
  require_relative "../../cache/engine_source"
14
+ require_relative "run_level_facts"
15
+ require_relative "worker_payload"
14
16
 
15
17
  module Rigor
16
18
  module Analysis
@@ -21,7 +23,7 @@ module Rigor
21
23
  # tables into the shared {RunSnapshots}, and merges per-worker reporter drains back into the runner's
22
24
  # reporter accumulators.
23
25
  #
24
- # Sequential-equivalence contract (docs/internal-spec/ worker-session.md): the pool path MUST produce
26
+ # Sequential-equivalence contract (docs/internal-spec/worker-session.md): the pool path MUST produce
25
27
  # the same diagnostics, in the same order, as the sequential path. The coordinator re-orders worker
26
28
  # results by original path order and replays per-worker reporter entries through the dedupe-on-record
27
29
  # APIs so reporter state matches a single-session run.
@@ -118,6 +120,12 @@ module Rigor
118
120
  @collected_analysis_reads ||= {}
119
121
  end
120
122
 
123
+ # Issue #2020 — the per-file run-scoped registrations the fork pool's workers recorded, folded into
124
+ # `Runner#analysis_disclosures` beside {#collected_analysis_reads}.
125
+ def collected_analysis_disclosures
126
+ @collected_analysis_disclosures ||= {}
127
+ end
128
+
121
129
  # ADR-103 WD12 — the per-file {Effects::FileCollection}s the fork pool captured this run (empty
122
130
  # unless the configuration carries an `effects:` block and a pool run occurred). The runner merges
123
131
  # these with its own sequential records before the post-pool fixpoint.
@@ -163,6 +171,21 @@ module Rigor
163
171
  end
164
172
  end
165
173
 
174
+ # Issue #2392 — clears every table a run accumulates on the coordinator: the dependency, analysis-read,
175
+ # analysis-disclosure and effect graphs, and the run disclosures and trust refusals. {Runner#run_analysis}
176
+ # calls it once at the top of each run, beside `@snapshots.reset_for_run`, so a long-lived Runner's next
177
+ # run starts as a fresh Runner's would. Never called between the passes of one run, which must see each
178
+ # other's collections. Configuration-derived state (`@record_dependencies`, `@record_effects`, the readers,
179
+ # the reporters) is per-coordinator and is not touched.
180
+ def reset_for_run
181
+ @collected_dependencies = {}
182
+ @collected_analysis_reads = {}
183
+ @collected_analysis_disclosures = {}
184
+ @collected_effects = {}
185
+ @collected_run_disclosures = {}
186
+ @collected_trust_refusals = {}
187
+ end
188
+
166
189
  # ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
167
190
  # silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
168
191
  # cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
@@ -265,16 +288,21 @@ module Rigor
265
288
 
266
289
  env = resolve_sequential_environment(source_files: project_files)
267
290
  end
268
- snapshot_project_signature_state(signature_state_environment(resolve, project_files))
269
- snapshot_effect_annotation_carrier(
270
- annotation_carrier_environment(resolve, in_hand: !env.nil?)&.rbs_loader
271
- )
272
- # `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
273
- # an environment, and force nothing when they did not.
274
- record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
291
+ # Each gate resolves its environment only when its first fact is read, so the carrier's `in_hand:`
292
+ # sees an environment the signature state just built.
293
+ harvest_environment_facts do |gate|
294
+ if gate == :annotation_carrier
295
+ annotation_carrier_environment(resolve, in_hand: !env.nil?)
296
+ else
297
+ signature_state_environment(resolve, project_files)
298
+ end
299
+ end
300
+ # `env` rather than `resolve.call`: read the demand the harvest above just made when it built an
301
+ # environment, and force nothing when it did not.
302
+ @snapshots.absorb(RunLevelFacts.drain(env, demanding: false))
275
303
  # An empty closure analyses no file, so only a deleted file's buffer can make a tuple stale here.
276
304
  replayed = replayed_restored_run_level_rows?(closure: [], redemand_env: resolve)
277
- record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed
305
+ @snapshots.absorb(RunLevelFacts.drain(resolve.call, demanding: true)) unless replayed
278
306
  prepare_rows
279
307
  end
280
308
 
@@ -289,15 +317,15 @@ module Rigor
289
317
 
290
318
  # Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Every
291
319
  # answer is loader-free (the configuration, the plugin registry and, issue #1713, the required-feature
292
- # scan), and with no gate open {#snapshot_project_signature_state} writes the same empty state a nil
293
- # environment gives it.
320
+ # scan), and with no gate open the `:signature_state` collectors write the same empty state a nil
321
+ # environment gives them.
294
322
  def signature_state_environment(resolve, project_files)
295
323
  gated = plugin_signature_paths? || project_signature_paths? || vendored_features_requested?(project_files)
296
324
  gated ? resolve.call : nil
297
325
  end
298
326
 
299
327
  # Issue #1713 — whether this run activates a required-feature-gated vendored directory, whose stand-down
300
- # {#snapshot_project_signature_state} reports: the scan every build of this run gates on, or the
328
+ # {RunLevelFacts::Collect.vendored_standdowns} reports: the scan every build of this run gates on, or the
301
329
  # feature named under `libraries:`.
302
330
  def vendored_features_requested?(project_files)
303
331
  features = project_files.nil? || project_files.empty? ? [] : required_features_for(project_files)
@@ -325,25 +353,28 @@ module Rigor
325
353
  end
326
354
 
327
355
  # Issues #796 / #794 — fold the previous full run's run-level rows into this narrowed run's
328
- # snapshots. The definition-build set is UNIONed (`#record_definition_build_failures` dedupes by
356
+ # snapshots. The definition-build set is UNIONed ({RunLevelFacts::Merge.union_by_class} dedupes by
329
357
  # class), so a class this run demanded for itself and the replayed set agree; the HKT outcome is a
330
- # single tuple whoever demanded it, so `||=` keeps this run's own answer when it has one. Returns
358
+ # single tuple whoever demanded it, so first-wins keeps this run's own answer when it has one. Returns
331
359
  # true when a replay was available, which is what tells {#serve_empty_closure} it owes no HKT demand
332
360
  # of its own — and so no environment.
333
361
  #
334
362
  # @param closure — the files this run analyses; see {#stale_replayed_failure?}.
335
363
  # @param redemand_env — resolves the environment {#redemand_stale_failures} re-derives a stale
336
364
  # failure in; nil (the empty closure, which has no stale failure) re-derives nothing.
365
+ #
366
+ # Every replayable fact ({RunLevelFacts::Fact#replay}) is absorbed by its own merge rule; the
367
+ # definition-build set alone is filtered for staleness first.
337
368
  def replayed_restored_run_level_rows?(closure: nil, redemand_env: nil)
338
369
  rows = @restored_run_level_rows
339
370
  return false if rows.nil?
340
371
 
341
- stale, replayable = Array(rows.definition_build_failures).partition do |failure|
372
+ replayed = rows.to_h
373
+ stale, replayable = Array(replayed[:definition_build_failures]).partition do |failure|
342
374
  stale_replayed_failure?(failure, closure)
343
375
  end
344
- record_definition_build_failures(replayable)
376
+ @snapshots.absorb(replayed.merge(definition_build_failures: replayable))
345
377
  redemand_stale_failures(stale, redemand_env)
346
- record_hkt_scan_failure(rows.hkt_scan_failure)
347
378
  true
348
379
  end
349
380
 
@@ -369,7 +400,7 @@ module Rigor
369
400
  loader = redemand_env.call&.rbs_loader
370
401
  return if loader.nil?
371
402
 
372
- record_definition_build_failures(pending.filter_map { |name| loader.redemand_definition(name) })
403
+ @snapshots.absorb(definition_build_failures: pending.filter_map { |name| loader.redemand_definition(name) })
373
404
  end
374
405
 
375
406
  # A replayed definition-build failure the snapshot's own fingerprint cannot vouch for. The
@@ -413,84 +444,39 @@ module Rigor
413
444
  end
414
445
 
415
446
  def analyze_files_sequentially(files, environment)
416
- snapshot_project_signature_state(environment)
417
- snapshot_effect_annotation_carrier(environment&.rbs_loader)
447
+ harvest_environment_facts { environment }
418
448
  result = files.flat_map { |path| @analyze_file.call(path, environment) }
419
- # Issue #696 — AFTER the loop, deliberately. Definition builds are lazy (ADR-54 WD1: per class, on
420
- # first demand), so a snapshot taken beside the ones above — which run BEFORE `files.flat_map` —
421
- # would read an empty list on every run, including the ones this diagnostic exists for.
422
- record_definition_build_failures(environment&.rbs_loader&.definition_build_failures)
423
- # Issue #784 — same timing contract, same reason: the HKT scan is first demanded from inside a
424
- # file's analysis (the dispatcher's Singleton-receiver tier), so a snapshot taken any earlier
425
- # would read nil on every run. And demanded HERE once more by the run itself, because a subset
426
- # run (`--verify-incremental`'s partition, an incremental recheck's closure) may contain no file
427
- # that demands it — the row must not depend on which files were analysed. See {#hkt_scan_outcome}
428
- # for why a Rigor-owned demand is sound here where #696 forbids it.
429
- record_hkt_scan_failure(hkt_scan_outcome(environment))
430
- if @collect_stats
431
- loader = environment.rbs_loader
432
- @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
433
- @snapshots.signature_paths = loader&.signature_paths || [].freeze
434
- end
449
+ # Issues #696 / #784 — AFTER the loop, deliberately. Definition builds are lazy (ADR-54 WD1: per
450
+ # class, on first demand) and the HKT scan is first demanded from inside a file's analysis, so a
451
+ # read beside the harvest above would find nothing on every run. The HKT registry is demanded here
452
+ # once more by the run itself, because a subset run may contain no file that demands it (see
453
+ # {RunLevelFacts::Collect.hkt_scan_failure} for why a Rigor-owned demand is sound there).
454
+ @snapshots.absorb(RunLevelFacts.drain(environment))
455
+ harvest_stats { environment }
435
456
  result
436
457
  end
437
458
 
438
- # The whole-run findings about the project's OWN `signature_paths:` RBS — the namespaces the loader had
439
- # to synthesize, the files it had to quarantine, the total env-build failure (if any), and the
440
- # `rigor:v1:conforms-to` results. Snapshotted as small plain data (NOT the env) so `#run` can surface
441
- # them as diagnostics without rebuilding it.
442
- #
443
- # All are gated on the project actually declaring `signature_paths:`, for one reason: each forces the
444
- # (otherwise lazy) RBS env to build, and doing that when there is no project sig set would warm
445
- # `.rigor/cache` on a bare `--no-stats` run. The loader has already memoised each answer, so reading
446
- # them here is free.
447
- def snapshot_project_signature_state(environment)
448
- # Issue #610 — NOT behind the `signature_paths:` gate below: the source a bundled plugin's `sig/`
449
- # stands down against is typically an `rbs collection install` the configuration never lists
450
- # there. Gated instead on a loaded plugin contributing signatures at all — the registry answers
451
- # that without touching the env, and it is exactly the set the loader defers — so a project with
452
- # no such plugin reads nothing off its loader here, as before.
453
- @snapshots.signature_standdowns =
454
- plugin_signature_paths? ? signature_standdowns_for(environment&.rbs_loader) : []
455
- # Issue #1713 — NOT behind the `signature_paths:` gate either: what displaces a vendored declaration may
456
- # be a gem's or a collection's copy. The loader answers without building its env when no gated
457
- # directory is active.
458
- @snapshots.vendored_standdowns = vendored_standdowns_for(environment&.rbs_loader)
459
- unless project_signature_paths?
460
- @snapshots.synthesized_namespaces = []
461
- @snapshots.quarantined_signatures = []
462
- @snapshots.conformance_results = []
463
- @snapshots.env_build_failure = nil
464
- return
465
- end
459
+ # ADR-116 WD3 — the `:environment` run-level facts ({RunLevelFacts}), in declaration order, off the
460
+ # environment the block yields for each fact's gate: the project-signature state (#798 — every path
461
+ # takes it, pooled or not, because no per-file analysis produces it) and the #441 effect-annotation
462
+ # carrier. Snapshotted as small plain data (NOT the env) so the environment stays a LOCAL that goes
463
+ # GC-eligible when its path returns.
464
+ def harvest_environment_facts(&)
465
+ @snapshots.absorb(RunLevelFacts.harvest(:environment, harvest_context, &))
466
+ end
466
467
 
467
- # nil-safe: the empty-closure path reaches here with no environment when the project has no files.
468
- loader = environment&.rbs_loader
469
- @snapshots.synthesized_namespaces = loader&.synthesized_namespaces || []
470
- @snapshots.quarantined_signatures = loader&.quarantined_signatures || []
471
- @snapshots.env_build_failure = loader&.env_build_failure
472
- @snapshots.conformance_results = RbsExtended::ConformanceChecker.scan(loader)
468
+ # `RunStats` telemetry ONLY (`class_decl_paths` / `signature_paths`), which a `--no-stats` run
469
+ # legitimately skips. Never gates a diagnostic slot (#798).
470
+ def harvest_stats(&)
471
+ @snapshots.absorb(RunLevelFacts.harvest(:stats, harvest_context, &)) if @collect_stats
473
472
  end
474
473
 
475
- # #441 — the inline stratum of `effect.annotations-unchecked`, carried out of the analysis path as
476
- # small plain data, for the same reason the block above is: the environment stays a LOCAL so it goes
477
- # GC-eligible when the path returns, and the residual pass runs after it.
478
- #
479
- # {Runner#effect_annotation_residual_diagnostics} used to read the loader off `@run_environment`,
480
- # which only the ADR-45 result-cacheable path ever assigns — so a `--no-cache`, `--workers N` or
481
- # `--incremental` run reported a `.rbs` annotation and stayed silent about the identical one written
482
- # as an rbs-inline comment, while a default run reported both. The environment was there either way;
483
- # nothing was carrying it. {Effects::SignatureSources.annotated_carrier} reduces it to at most ONE
484
- # synthesized buffer, which is all a one-`:info`-per-run pass can spend.
485
- #
486
- # Skipped when collection is on — then {Runner::EffectEnvelopePass} reads the loader directly and
487
- # this pass is gated off — so a collecting run pays nothing, and a project with no synthesizing
488
- # plugin answers with an empty array and no regex at all.
489
- def snapshot_effect_annotation_carrier(loader)
490
- virtual = @record_effects ? nil : loader&.virtual_rbs
491
- @snapshots.effect_annotation_carrier = Effects::SignatureSources.annotated_carrier(virtual)
492
- rescue StandardError
493
- @snapshots.effect_annotation_carrier = [].freeze
474
+ def harvest_context
475
+ RunLevelFacts::Context.new(
476
+ project_signature_paths: project_signature_paths?,
477
+ plugin_signature_paths: plugin_signature_paths?,
478
+ record_effects: @record_effects
479
+ )
494
480
  end
495
481
 
496
482
  # Sequential-mode environment resolver. Returns the supplied `environment:` override (with the
@@ -526,8 +512,10 @@ module Rigor
526
512
  # `project_scope_seed` its worker would need to record complete edges. Without `fork` (Windows) a
527
513
  # recording run degrades to sequential, which records correctly through `@analyze_file`.
528
514
  #
529
- # An effects run (ADR-103 WD13) is pinned for exactly the same reason — the Ractor messages carry
530
- # no side-table channel — and degrades the same way. The degrade is sound rather than merely safe:
515
+ # An effects run (ADR-103 WD13) is pinned the same way: the Ractor `:done` {WorkerPayload} has an
516
+ # `effects` slot, but the Ractor worker never collects into it (its session is built without the
517
+ # recording inputs, and the backend is off-default, #414), so only the fork backend fills it. Without
518
+ # `fork` it degrades the same way. The degrade is sound rather than merely safe:
531
519
  # the sequential fallback still collects, so the effect graph is complete either way.
532
520
  # @param source_files — the file list every worker's / the fallback's environment is
533
521
  # built over — the whole project (issue #793), defaulting to `files` for direct callers.
@@ -591,18 +579,18 @@ module Rigor
591
579
  end
592
580
 
593
581
  # ADR-15 Phase 4b — Ractor pool around {WorkerSession}. Spawns `@workers` Ractors; each takes the
594
- # shareable payload (Configuration, cache_root String, plugin Blueprint Array, explain Boolean) and
582
+ # shareable payload (configuration, cache root, plugin blueprints, explain flag) and
595
583
  # builds its OWN WorkerSession internally. Files are distributed round-robin across the pool; each
596
584
  # worker writes back to the main Ractor's mailbox via `Ractor.main.send` with one of three message
597
585
  # kinds:
598
586
  #
599
- # - `[:prepare, diagnostics]` — once at startup, the session's `prepare_diagnostics` snapshot. The
600
- # coordinator keeps the FIRST worker's snapshot only (plugin `#prepare` is deterministic per
587
+ # - `[:prepare, diagnostics, refusals]` — once at startup, the session's `prepare_diagnostics` snapshot.
588
+ # The coordinator keeps the FIRST worker's snapshot only (plugin `#prepare` is deterministic per
601
589
  # plugin, so each worker produces the same diagnostic set; surfacing them once avoids N×
602
590
  # duplication).
603
- # - `[:file, path, diagnostics]` — one per analysed file.
604
- # - `[:done, drained_reporters]` — once at exit, the per-worker reporter snapshots for end-of-pool
605
- # merge.
591
+ # - `[:file, path, diagnostics, refusals]` — one per analysed file.
592
+ # - `[:done, payload]` — once at exit, the worker's {WorkerPayload} (its reporter snapshots, drained
593
+ # run-level facts and disclosures) for end-of-pool merge.
606
594
  #
607
595
  # The Ruby 4.0+ Ractor model uses a single per-Ractor mailbox (no `Ractor.yield`); workers push
608
596
  # back via `Ractor.main.send`. The coordinator drains its mailbox via `Ractor.receive` until it has
@@ -689,9 +677,9 @@ module Rigor
689
677
  )
690
678
  )
691
679
  warm_env = prewarm_rbs_cache_for_pool(source_files: source_files, locked_gems: locked_gems)
692
- snapshot_project_signature_state(warm_env)
693
- snapshot_effect_annotation_carrier(warm_env&.rbs_loader)
694
- record_definition_build_failures(warm_env&.rbs_loader&.definition_build_failures)
680
+ harvest_environment_facts { warm_env }
681
+ @snapshots.absorb(RunLevelFacts.drain(warm_env, demanding: false))
682
+ harvest_stats { warm_env }
695
683
 
696
684
  configuration = @configuration
697
685
  cache_root = @cache_store&.root
@@ -699,8 +687,7 @@ module Rigor
699
687
  explain = @explain
700
688
  # ADR-32 WD4 — the full project file list travels into every Ractor worker so each worker's
701
689
  # WorkerSession can invoke loaded plugins' source_rbs_synthesizers at env-build time. The list is
702
- # a frozen Array<String>; cheaply shareable. Issue #793 — it IS the full project now
703
- # (`source_files`), not the analyzed subset this comment always described.
690
+ # a frozen list of strings; cheaply shareable.
704
691
  shareable_source_files = source_files.map { |path| path.to_s.dup.freeze }.freeze
705
692
  features = required_features_for(source_files)
706
693
 
@@ -731,7 +718,7 @@ module Rigor
731
718
  # Issue #1051 — sent with `:done`, not with `:prepare`: this backend builds no parent session,
732
719
  # so the worker's own table is the only place a disclosure exists, and it is complete only
733
720
  # once the worker has finished its files.
734
- main.send([:done, session.drain_reporters, session.drain_run_disclosures])
721
+ main.send([:done, Rigor::Analysis::Runner::WorkerPayload.harvest(session)])
735
722
  end
736
723
  end
737
724
 
@@ -740,8 +727,11 @@ module Rigor
740
727
  # Ractor's default port, which is the same mailbox the workers push their results to.
741
728
  pool.each { |worker| worker.monitor(Ractor.current.default_port) }
742
729
 
743
- files.each_with_index { |path, index| pool[index % pool.size].send(path) }
744
- pool.each { |worker| worker.send(nil) }
730
+ # A worker that died before its turn has a closed port: `Ractor#send` raises `Ractor::ClosedError`.
731
+ # That is the same death the monitor loop below counts, so it is not a failure of the run — the
732
+ # file simply never reports and lands in the degrade, like any file a dead worker never answered.
733
+ files.each_with_index { |path, index| send_to_worker(pool[index % pool.size], path) }
734
+ pool.each { |worker| send_to_worker(worker, nil) }
745
735
 
746
736
  prepare_diagnostics = nil
747
737
  prepare_refusals = nil
@@ -769,8 +759,7 @@ module Rigor
769
759
  results_by_path[message[1]] = message[2]
770
760
  refusals_by_path[message[1]] = message[3]
771
761
  when :done
772
- merge_worker_reporters(message[1])
773
- merge_run_disclosures(message[2])
762
+ absorb_worker_payload(message[1])
774
763
  end
775
764
  end
776
765
 
@@ -818,8 +807,7 @@ module Rigor
818
807
 
819
808
  environment = build_runner_environment(source_files: source_files)
820
809
  degraded.each { |path| results_by_path[path] = @analyze_file.call(path, environment) }
821
- record_definition_build_failures(environment.rbs_loader&.definition_build_failures)
822
- record_hkt_scan_failure(hkt_scan_outcome(environment))
810
+ @snapshots.absorb(RunLevelFacts.drain(environment))
823
811
  end
824
812
 
825
813
  # ADR-15 Amendment (2026-05-20) — fork-based worker pool, the active backend for `workers > 0`.
@@ -867,14 +855,13 @@ module Rigor
867
855
  # a resulting definition-build failure is already sitting in the parent's loader at the moment
868
856
  # each child copy-on-write-inherits it — every child's own `#drain_reporters` then reports it
869
857
  # right alongside whatever its own slice demanded, with no separate plumbing needed. Left
870
- # unconditional, unlike `#snapshot_fork_pool_stats` below: these are diagnostic rows, not
858
+ # unconditional, unlike `#harvest_stats` below: these are diagnostic rows, not
871
859
  # `RunStats` telemetry, and the stats gate must not decide which diagnostics a run reports (a
872
860
  # `--workers N --no-stats` run used to say strictly LESS than the sequential path over the
873
861
  # same project).
874
- snapshot_project_signature_state(session.environment)
875
- snapshot_effect_annotation_carrier(session.environment.rbs_loader)
876
- # Issue #1051 — taken BEFORE the fork, for the same reason and with the same effect as the two
877
- # snapshots above: `#prepare` ran on the parent when the session was built, so every disclosure a
862
+ harvest_environment_facts { session.environment }
863
+ # Issue #1051 — taken BEFORE the fork, for the same reason and with the same effect as the harvest
864
+ # above: `#prepare` ran on the parent when the session was built, so every disclosure a
878
865
  # plugin registers there (the normal case — a disclosure is a fact about the project's inputs,
879
866
  # known before a file is read) is already in this table, in the same order the sequential path
880
867
  # would produce. Each child inherits the identical table and re-offers it in its payload, where
@@ -885,7 +872,8 @@ module Rigor
885
872
  # session's record of what it already reported and drains only its own slice's, so the prepare-time
886
873
  # refusal is counted once, as `--workers 0` counts it, rather than once per worker.
887
874
  merge_trust_refusals(session.drain_trust_refusals)
888
- snapshot_fork_pool_stats(session) if @collect_stats
875
+ # `RunStats` telemetry only, off the same pre-fork environment.
876
+ harvest_stats { session.environment }
889
877
 
890
878
  worker_count = [@workers, files.size].min
891
879
  slices = files.each_slice((files.size.to_f / worker_count).ceil).to_a
@@ -909,13 +897,10 @@ module Rigor
909
897
  results_by_path[path] = session.analyze(path)
910
898
  refusals_by_path[path] = session.drain_trust_refusals
911
899
  end
912
- merge_worker_reporters(session.drain_reporters)
913
- # The degraded slice ran on the parent session, so a disclosure it registered lives there.
914
- merge_run_disclosures(session.drain_run_disclosures)
915
- # The degraded slice was re-analysed on the parent session, so its dependency records live there,
916
- # not in a child payload — fold them in alongside the successful children's.
917
- collect_dependencies(session.drain_dependencies, session.drain_analysis_reads) if @record_dependencies
918
- @collected_effects.merge!(session.drain_effects) if @record_effects
900
+ # The degraded slice ran on the parent session, so its reporters, run-level facts, disclosures,
901
+ # dependency records and effects live there, not in a child payload — fold them in alongside the
902
+ # successful children's. Its refusals were drained file by file above.
903
+ absorb_worker_payload(WorkerPayload.harvest(session, record_effects: @record_effects))
919
904
  end
920
905
 
921
906
  merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
@@ -949,32 +934,14 @@ module Rigor
949
934
  # worker finishes first and stays at no-YJIT parity, exactly as the sequential path would.
950
935
  Runtime::Jit.rearm_after_fork
951
936
  results = slice.to_h { |path| [path, session.analyze(path)] }
952
- payload = { results: results, reporters: session.drain_reporters,
953
- dependencies: session.drain_dependencies,
954
- analysis_reads: session.drain_analysis_reads,
955
- disclosures: session.drain_run_disclosures,
956
- refusals: session.drain_trust_refusals }
957
- # ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
958
- # marshals exactly the payload it marshalled before the feature existed.
959
- payload[:effects] = session.drain_effects if @record_effects
937
+ # ADR-103 WD13 — the effects slot is filled only on a collecting run.
938
+ payload = WorkerPayload.harvest(session, results: results, record_effects: @record_effects)
960
939
  File.binwrite(out_path, Marshal.dump(payload))
961
940
  exit!(0)
962
941
  rescue StandardError
963
942
  exit!(1)
964
943
  end
965
944
 
966
- # Issue #798 — `RunStats` telemetry ONLY, now that `#snapshot_project_signature_state` (called
967
- # unconditionally above) owns every diagnostic-bearing slot this method used to ALSO write
968
- # (`quarantined_signatures`, `env_build_failure`): those must not be gated on `@collect_stats`, and
969
- # this method's own name says what is left — `class_decl_paths` / `signature_paths`, read off the
970
- # parent session's loader so end-of-run {RunStats} can attribute the RBS class universe, which a
971
- # `--no-stats` run legitimately skips.
972
- def snapshot_fork_pool_stats(session)
973
- loader = session.environment.rbs_loader
974
- @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
975
- @snapshots.signature_paths = loader&.signature_paths || [].freeze
976
- end
977
-
978
945
  # Waits for every forked child, merges each successful payload into `results_by_path`, and returns
979
946
  # the file paths whose worker exited abnormally (for in-process degrade).
980
947
  # @param refusals_by_slice — issue #1900 — receives each successful payload's trust-refusal drain
@@ -985,15 +952,10 @@ module Rigor
985
952
  _, status = Process.waitpid2(child[:pid])
986
953
  payload = fork_worker_payload(status, child[:out_path])
987
954
  if payload
988
- results_by_path.merge!(payload.fetch(:results))
989
- merge_worker_reporters(payload.fetch(:reporters))
990
- merge_run_disclosures(payload.fetch(:disclosures, []))
991
- refusals = payload.fetch(:refusals, [])
955
+ results_by_path.merge!(payload.results)
956
+ absorb_worker_payload(payload)
957
+ refusals = Array(payload.refusals)
992
958
  refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
993
- if @record_dependencies
994
- collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {}))
995
- end
996
- @collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
997
959
  else
998
960
  degraded.concat(child[:slice])
999
961
  end
@@ -1001,13 +963,15 @@ module Rigor
1001
963
  degraded
1002
964
  end
1003
965
 
1004
- # ADR-46 / issue #1553 — fold one worker's dependency records and per-file plugin reads.
1005
- def collect_dependencies(dependencies, analysis_reads)
966
+ # ADR-46 / issues #1553, #2020 — fold one worker's dependency records, per-file plugin reads and per-file
967
+ # run-scoped registrations.
968
+ def collect_dependencies(dependencies, analysis_reads, analysis_disclosures)
1006
969
  @collected_dependencies.merge!(dependencies)
1007
970
  collected_analysis_reads.merge!(analysis_reads)
971
+ collected_analysis_disclosures.merge!(analysis_disclosures)
1008
972
  end
1009
973
 
1010
- # @return the child's `{results:, reporters:}` payload, or nil when the child exited
974
+ # @return the child's {WorkerPayload}, or nil when the child exited
1011
975
  # abnormally or wrote no readable payload. `Marshal.load` is safe here: the blob was written by
1012
976
  # our own forked child to a temp file we created.
1013
977
  def fork_worker_payload(status, out_path)
@@ -1080,8 +1044,7 @@ module Rigor
1080
1044
  # sequential analysis with a `:warning` `pool-degraded` diagnostic at run start. The actual
1081
1045
  # per-file analysis runs on the coordinator, identical to the default sequential path.
1082
1046
  # @param source_files — issue #793 — the whole project, so this path's environment
1083
- # carries the same plugin-synthesized RBS the pool workers' would. It used to build over `[]`,
1084
- # i.e. with no synthesized RBS at all, even on a full run.
1047
+ # carries the same plugin-synthesized RBS the pool workers' would.
1085
1048
  #
1086
1049
  # Issue #1900 — it also runs `#prepare` on the coordinator-side registry it analyses with, BEFORE the
1087
1050
  # environment build as the sequential pre-pass does. A pooled run's pre-pass skips `#prepare` (the
@@ -1092,25 +1055,18 @@ module Rigor
1092
1055
  def analyze_files_sequentially_fallback(files, reason:, source_files: files)
1093
1056
  prepare_rows = prepare_coordinator_plugins_once
1094
1057
  environment = build_runner_environment(source_files: source_files)
1095
- # Same set, same ORDER as `#analyze_files_sequentially`. The signature state used to be hand-copied
1096
- # here slot by slot, which dropped `synthesized-namespace` and every conformance row. The conformance
1097
- # scan inside it demands the definition of every `rigor:v1:conforms-to` class, so it has to run
1098
- # BEFORE the definition-build failures are read below, or a failure only that scan reaches is lost.
1099
- snapshot_project_signature_state(environment)
1100
- snapshot_effect_annotation_carrier(environment.rbs_loader)
1058
+ # Same harvest, same ORDER as `#analyze_files_sequentially` (#1201: a hand-copied list here once
1059
+ # dropped `synthesized-namespace` and every conformance row). The conformance scan inside it demands
1060
+ # the definition of every `rigor:v1:conforms-to` class, so it runs BEFORE the definition-build
1061
+ # failures are drained below, or a failure only that scan reaches is lost.
1062
+ harvest_environment_facts { environment }
1101
1063
  diagnostics = prepare_rows + files.flat_map { |path| @analyze_file.call(path, environment) }
1102
- loader = environment.rbs_loader
1103
- # Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
1104
- # failing build, and it must snapshot the same slot the two real paths do. Reached whenever
1105
- # `fork` is unavailable (Windows) and on `--incremental` / effects runs without it: a run that
1106
- # degraded to sequential must not also report less than a sequential run would.
1107
- record_definition_build_failures(loader&.definition_build_failures)
1108
- # Issue #784 — same reasoning: this path's Environment IS the one that reached the scan, so it
1109
- # must snapshot the slot too, or a run that degraded to sequential would report less than a
1110
- # sequential run would. Demanded once by the run as well, for the reason at the sequential site.
1111
- record_hkt_scan_failure(hkt_scan_outcome(environment))
1112
- @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
1113
- @snapshots.signature_paths = loader&.signature_paths || [].freeze
1064
+ # Issues #696 / #784 — this path analyses on the coordinator, so its environment IS the one that
1065
+ # reached the failing build and the scan, and it drains the same facts the sequential path does.
1066
+ # Reached whenever `fork` is unavailable (Windows) and on `--incremental` / effects runs without it:
1067
+ # a run that degraded to sequential must not also report less than a sequential run would.
1068
+ @snapshots.absorb(RunLevelFacts.drain(environment))
1069
+ harvest_stats { environment }
1114
1070
  diagnostics.unshift(
1115
1071
  Diagnostic.new(
1116
1072
  path: ".rigor.yml", line: 1, column: 1,
@@ -1129,149 +1085,42 @@ module Rigor
1129
1085
  Array(@prepare_coordinator_plugins.call)
1130
1086
  end
1131
1087
 
1088
+ # One worker drain ({WorkerSession#drain_reporters}): each reporter absorbs its own snapshot, by
1089
+ # its primitives (#785, #805) so every worker's copy of an entry collapses to the single row a
1090
+ # `--workers=0` run prints, and the drained `:analysis` facts fold in by their merge rules — the
1091
+ # definition-build union (#696) and the first-wins HKT outcome (#784). A key an older drain shape
1092
+ # lacks records nothing.
1132
1093
  def merge_worker_reporters(drained)
1133
- merge_rbs_extended_reporter(drained.fetch(:rbs_extended))
1134
- drained.fetch(:boundary_cross).each do |entry|
1135
- @boundary_cross_reporter.record(
1136
- class_name: entry.class_name,
1137
- method_name: entry.method_name,
1138
- gem_name: entry.gem_name,
1139
- rbs_display: entry.rbs_display
1140
- )
1141
- end
1142
- # ADR-32 WD6 — merge per-worker synthesizer failures back into the coordinator's reporter. Fetched
1143
- # with a default empty array so older drains (pre-slice-2) remain compatible.
1144
- #
1145
- # `kind:` rides along (issue #824): without it every drained entry replayed as WD6's default
1146
- # `:failed`, so under `--workers N` a WD12 "parsed but not honoured" row came back as
1147
- # `source-rbs-synthesis-failed` — a different id, and a message telling the user the file
1148
- # contributed nothing when all but one of its annotations bound. Every worker reads the same
1149
- # sources and so hands over the same entries; the reporter's own dedup collapses them. `line:` rides
1150
- # along for the same reason (#1075): a consistency error is positioned at its `.rbs` member.
1151
- Array(drained[:source_rbs_synthesis]).each do |entry|
1152
- @source_rbs_synthesis_reporter.record(
1153
- plugin_id: entry.plugin_id, path: entry.path, message: entry.message, kind: entry.kind,
1154
- line: entry.line
1155
- )
1156
- end
1157
- # Issue #696. Fetched with a default so an older drain stays compatible, exactly as the line above.
1158
- record_definition_build_failures(drained[:definition_build_failures])
1159
- # Issue #784. `Hash#[]` is already a nil default, exactly as `env_build_failure` is snapshotted
1160
- # elsewhere — an older drain shape simply has no key and records nothing.
1161
- record_hkt_scan_failure(drained[:hkt_scan_failure])
1094
+ RunLevelFacts.absorb_reporters(reporters, drained)
1095
+ @snapshots.absorb(RunLevelFacts.drained_facts(drained))
1162
1096
  end
1163
1097
 
1164
- private
1165
-
1166
- # Replays one worker's three {RbsExtended::Reporter} streams into the run's own reporter.
1167
- #
1168
- # `hkt_directive_errors` (issue #785) is read with `Hash#[]`'s nil default rather than `fetch`,
1169
- # exactly as `source_rbs_synthesis` is, so an older drain shape simply records nothing.
1170
- #
1171
- # All three streams replay by their `(path, line, column)` primitives (#785 for the hkt stream, #805
1172
- # for its two elders). Every worker reads the same `.rbs`, so each hands over the same entries, and
1173
- # only a primitive-keyed dedup collapses them back to the single row a `--workers=0` run prints — an
1174
- # `RBS::Location` compares equal only against a location over the same `RBS::Buffer` object, so a
1175
- # location-keyed one would print N copies at `--workers=N`. The entries could not reach here carrying
1176
- # a location anyway: the drain Marshals its payload and an `RBS::Location` has no `_dump`.
1177
- def merge_rbs_extended_reporter(rbs)
1178
- rbs.fetch(:unresolved_payloads).each do |entry|
1179
- @rbs_extended_reporter.record_unresolved(
1180
- payload: entry.payload, path: entry.path, line: entry.line, column: entry.column
1181
- )
1182
- end
1183
- rbs.fetch(:lossy_projections).each do |entry|
1184
- @rbs_extended_reporter.record_lossy_projection(
1185
- head: entry.head, path: entry.path, line: entry.line, column: entry.column
1186
- )
1098
+ # One {WorkerPayload}, minus its per-file results and its refusals, which each backend merges in its
1099
+ # own file order.
1100
+ def absorb_worker_payload(payload)
1101
+ merge_worker_reporters(payload.reporters)
1102
+ merge_run_disclosures(payload.disclosures)
1103
+ if @record_dependencies
1104
+ collect_dependencies(payload.dependencies || {}, payload.analysis_reads || {},
1105
+ payload.analysis_disclosures || {})
1187
1106
  end
1188
- Array(rbs[:hkt_directive_errors]).each do |entry|
1189
- @rbs_extended_reporter.record_hkt_error(
1190
- message: entry.message, path: entry.path, line: entry.line, column: entry.column
1191
- )
1192
- end
1193
- replay_deprecated_forms(rbs)
1107
+ @collected_effects.merge!(payload.effects || {}) if @record_effects
1194
1108
  end
1195
1109
 
1196
- # ADR-109 WD3 — the fourth stream, read with the nil default like the hkt one so an older drain
1197
- # shape records nothing.
1198
- def replay_deprecated_forms(rbs)
1199
- Array(rbs[:deprecated_forms]).each do |entry|
1200
- @rbs_extended_reporter.record_deprecated_form(
1201
- payload: entry.payload, replacement: entry.replacement,
1202
- path: entry.path, line: entry.line, column: entry.column
1203
- )
1204
- end
1205
- end
1206
-
1207
- # Issue #696 — accumulate the per-class `RBS::DefinitionBuilder` failures this run observed, deduped
1208
- # by class name and kept in first-seen order.
1209
- #
1210
- # Accumulating rather than assigning is what makes a pooled run say what a sequential one says: each
1211
- # worker holds its OWN loader and its own memo (the `@state` that also gates the stderr banner, which
1212
- # is why the banner repeats per worker — #295), so a class that fails in two workers arrives twice
1213
- # and a class only one worker touched arrives once. The union over the workers is the set the RUN
1214
- # hit; neither worker alone is.
1215
- #
1216
- # NOT gated on `project_signature_paths?`, unlike {#snapshot_project_signature_state}. That gate
1217
- # exists because each condition there forces the otherwise-lazy env build, which would warm the cache
1218
- # on a project with no sig set of its own. Nothing is forced here — this reads what the analysis
1219
- # already demanded — and the failure is not a project-sig-only condition anyway: two BUNDLED sources
1220
- # declaring the same method collapse a class with no `signature_paths:` in sight (the
1221
- # `bigdecimal` / `BigMath` shape, #299). Gating it would suppress exactly the variant the user cannot
1222
- # fix by editing their own `sig/`.
1223
- def record_definition_build_failures(failures)
1224
- return if failures.nil? || failures.empty?
1225
-
1226
- # Issue #1902 — one tuple per class, chosen by {Environment::RbsLoader.definition_build_detail_order}
1227
- # rather than by arrival: a fresh record and a replayed one (or two workers' records) for the same
1228
- # class can name different failures, and the earlier one used to win.
1229
- @snapshots.definition_build_failures =
1230
- (@snapshots.definition_build_failures + failures)
1231
- .group_by(&:first)
1232
- .map { |_, candidates| candidates.min_by { |c| Environment::RbsLoader.definition_build_detail_order(c) } }
1233
- .freeze
1234
- end
1110
+ private
1235
1111
 
1236
- # Issue #784 — first-wins, unlike {#record_definition_build_failures}'s accumulate-and-dedup. That
1237
- # method accumulates because each pool WORKER owns its own loader and its own per-class memo, so a
1238
- # collapsed class can genuinely be observed by only some workers and the run's set is the union.
1239
- # The HKT scan has no such per-worker variation: `Environment#hkt_registry` builds from the SAME
1240
- # `signature_paths:` overlay every worker was handed, so every worker that demands it either all
1241
- # raise identically or all succeed — there is only ever one tuple to record, and `||=` is correct
1242
- # (and cheap) rather than an accumulate-and-dedup this slot never needs.
1243
- def record_hkt_scan_failure(tuple)
1244
- @snapshots.hkt_scan_failure ||= tuple
1112
+ # `Ractor#send` to a worker whose port is already closed (it died before this send, whether before
1113
+ # dispatch or part-way through it). The death is observed through the monitor notice, and the unsent
1114
+ # file is re-analysed by the degrade.
1115
+ def send_to_worker(worker, message)
1116
+ worker.send(message)
1117
+ rescue Ractor::ClosedError
1118
+ nil
1245
1119
  end
1246
1120
 
1247
- # Issue #784 — demand the registry once on behalf of the run, then read the slot. The seam in
1248
- # {Environment#hkt_registry} is demand-driven, and nothing guarantees any analysed file demands it:
1249
- # a `--verify-incremental` partition or an incremental closure can miss every `Klass.method` call,
1250
- # and then the slot is nil after the loop and the run says nothing — while `--incremental` has also
1251
- # dropped the row from its per-file cache. Demanding here makes the outcome a property of the RUN,
1252
- # not of which files happened to be in it.
1253
- #
1254
- # This is sound where #696's "a demand that is Rigor's own MUST NOT contribute" is not, and the
1255
- # difference is the shape of what is recorded. #696 reports a per-class LIST whose membership is
1256
- # "the classes the analysis demanded"; a Rigor-internal demand adds classes the user never asked
1257
- # about and makes that list vary with configuration. The HKT scan is ONE build with ONE outcome —
1258
- # the same tuple whoever demands it — so an extra demand cannot change what is reported, only
1259
- # guarantee it is observed.
1260
- #
1261
- # What it costs, stated plainly because {#record_definition_build_failures}'s comment promises
1262
- # "nothing is forced here": the demand DOES force the RBS env build when the environment has not
1263
- # built it yet (the scan reads the loader). On every path that reaches this method that build is
1264
- # either already done (the loop demanded it) or the same load one analysed file would have paid:
1265
- # a Marshal load when a cache store exists, the parse a full run pays under `--no-cache`. Never a
1266
- # build on a project with no files (see {#analyze_files}). Memoised, so on a reused Environment
1267
- # this is a hash read.
1268
- #
1269
- # @return the recorded tuple, or nil (no environment, or the scan built)
1270
- def hkt_scan_outcome(environment)
1271
- return nil if environment.nil?
1272
-
1273
- environment.hkt_registry
1274
- environment.hkt_scan_failure
1121
+ def reporters
1122
+ { rbs_extended: @rbs_extended_reporter, boundary_cross: @boundary_cross_reporter,
1123
+ source_rbs_synthesis: @source_rbs_synthesis_reporter }
1275
1124
  end
1276
1125
 
1277
1126
  # True when the project declares its own `signature_paths:` (the only place the
@@ -1281,25 +1130,13 @@ module Rigor
1281
1130
  !(paths.nil? || paths.empty?)
1282
1131
  end
1283
1132
 
1284
- # Issue #610 — see {#snapshot_project_signature_state}. The registry's `signature_paths` is what
1133
+ # Issue #610 — see {RunLevelFacts::Collect.signature_standdowns}. The registry's `signature_paths` is what
1285
1134
  # `Environment.for_project` defers, so this is the loader-free form of "is anything deferred?".
1286
1135
  def plugin_signature_paths?
1287
1136
  registry = plugin_registry
1288
1137
  registry.respond_to?(:signature_paths) && !registry.signature_paths.empty?
1289
1138
  end
1290
1139
 
1291
- # Empty for a loader with nothing deferred, without touching its env.
1292
- def signature_standdowns_for(loader)
1293
- return [] if loader.nil? || !loader.respond_to?(:deferred_signature_paths)
1294
- return [] if loader.deferred_signature_paths.empty?
1295
-
1296
- loader.signature_standdowns
1297
- end
1298
-
1299
- def vendored_standdowns_for(loader)
1300
- loader.respond_to?(:vendored_standdowns) ? loader.vendored_standdowns : []
1301
- end
1302
-
1303
1140
  def plugin_registry
1304
1141
  @plugin_registry_reader.call
1305
1142
  end