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
@@ -10,7 +10,10 @@ require_relative "../run_stats"
10
10
  require_relative "../../effects/signature_sources"
11
11
  require_relative "../../rbs_extended/conformance_checker"
12
12
  require_relative "../../runtime/jit"
13
+ require_relative "../../type_of_memo"
13
14
  require_relative "../../cache/engine_source"
15
+ require_relative "run_level_facts"
16
+ require_relative "worker_payload"
14
17
 
15
18
  module Rigor
16
19
  module Analysis
@@ -21,7 +24,7 @@ module Rigor
21
24
  # tables into the shared {RunSnapshots}, and merges per-worker reporter drains back into the runner's
22
25
  # reporter accumulators.
23
26
  #
24
- # Sequential-equivalence contract (docs/internal-spec/ worker-session.md): the pool path MUST produce
27
+ # Sequential-equivalence contract (docs/internal-spec/worker-session.md): the pool path MUST produce
25
28
  # the same diagnostics, in the same order, as the sequential path. The coordinator re-orders worker
26
29
  # results by original path order and replays per-worker reporter entries through the dedupe-on-record
27
30
  # APIs so reporter state matches a single-session run.
@@ -169,6 +172,21 @@ module Rigor
169
172
  end
170
173
  end
171
174
 
175
+ # Issue #2392 — clears every table a run accumulates on the coordinator: the dependency, analysis-read,
176
+ # analysis-disclosure and effect graphs, and the run disclosures and trust refusals. {Runner#run_analysis}
177
+ # calls it once at the top of each run, beside `@snapshots.reset_for_run`, so a long-lived Runner's next
178
+ # run starts as a fresh Runner's would. Never called between the passes of one run, which must see each
179
+ # other's collections. Configuration-derived state (`@record_dependencies`, `@record_effects`, the readers,
180
+ # the reporters) is per-coordinator and is not touched.
181
+ def reset_for_run
182
+ @collected_dependencies = {}
183
+ @collected_analysis_reads = {}
184
+ @collected_analysis_disclosures = {}
185
+ @collected_effects = {}
186
+ @collected_run_disclosures = {}
187
+ @collected_trust_refusals = {}
188
+ end
189
+
172
190
  # ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
173
191
  # silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
174
192
  # cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
@@ -181,6 +199,18 @@ module Rigor
181
199
  @buffer.nil?
182
200
  end
183
201
 
202
+ # ADR-130 WD2/WD5 — `RbsTypeTranslator.with_interface_erasure` and `RbsDispatch.without_interface_dispatch`
203
+ # are per-thread switches, which a Ractor worker would not see: a pooled run under either would analyse with
204
+ # the default engine and pass as the erasure baseline or the dispatch-off mode.
205
+ def refuse_pool_under_interface_switches
206
+ if Rigor::Inference::RbsTypeTranslator.interface_erasure?
207
+ raise ArgumentError, "RbsTypeTranslator.with_interface_erasure supports only a sequential run (workers: 0)"
208
+ end
209
+ return if Rigor::Inference::MethodDispatcher::RbsDispatch.interface_dispatch?
210
+
211
+ raise ArgumentError, "RbsDispatch.without_interface_dispatch supports only a sequential run (workers: 0)"
212
+ end
213
+
184
214
  # ADR-15 Phase 4b — routes per-file analysis to either the sequential coordinator-side Environment
185
215
  # (legacy path, default) or a Ractor worker pool built around {WorkerSession} (opt-in via
186
216
  # `workers:`). The sequential path is bit-for-bit unchanged from v0.1.4 / earlier; the pool path is
@@ -208,6 +238,7 @@ module Rigor
208
238
  source_files = project_files || files
209
239
  result =
210
240
  if pool_mode?
241
+ refuse_pool_under_interface_switches
211
242
  dispatch_pool(files, source_files: source_files)
212
243
  else
213
244
  environment ||= resolve_sequential_environment(source_files: source_files)
@@ -271,16 +302,21 @@ module Rigor
271
302
 
272
303
  env = resolve_sequential_environment(source_files: project_files)
273
304
  end
274
- snapshot_project_signature_state(signature_state_environment(resolve, project_files))
275
- snapshot_effect_annotation_carrier(
276
- annotation_carrier_environment(resolve, in_hand: !env.nil?)&.rbs_loader
277
- )
278
- # `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
279
- # an environment, and force nothing when they did not.
280
- record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
305
+ # Each gate resolves its environment only when its first fact is read, so the carrier's `in_hand:`
306
+ # sees an environment the signature state just built.
307
+ harvest_environment_facts do |gate|
308
+ if gate == :annotation_carrier
309
+ annotation_carrier_environment(resolve, in_hand: !env.nil?)
310
+ else
311
+ signature_state_environment(resolve, project_files)
312
+ end
313
+ end
314
+ # `env` rather than `resolve.call`: read the demand the harvest above just made when it built an
315
+ # environment, and force nothing when it did not.
316
+ @snapshots.absorb(RunLevelFacts.drain(env, demanding: false))
281
317
  # An empty closure analyses no file, so only a deleted file's buffer can make a tuple stale here.
282
318
  replayed = replayed_restored_run_level_rows?(closure: [], redemand_env: resolve)
283
- record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed
319
+ @snapshots.absorb(RunLevelFacts.drain(resolve.call, demanding: true)) unless replayed
284
320
  prepare_rows
285
321
  end
286
322
 
@@ -295,15 +331,15 @@ module Rigor
295
331
 
296
332
  # Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Every
297
333
  # answer is loader-free (the configuration, the plugin registry and, issue #1713, the required-feature
298
- # scan), and with no gate open {#snapshot_project_signature_state} writes the same empty state a nil
299
- # environment gives it.
334
+ # scan), and with no gate open the `:signature_state` collectors write the same empty state a nil
335
+ # environment gives them.
300
336
  def signature_state_environment(resolve, project_files)
301
337
  gated = plugin_signature_paths? || project_signature_paths? || vendored_features_requested?(project_files)
302
338
  gated ? resolve.call : nil
303
339
  end
304
340
 
305
341
  # Issue #1713 — whether this run activates a required-feature-gated vendored directory, whose stand-down
306
- # {#snapshot_project_signature_state} reports: the scan every build of this run gates on, or the
342
+ # {RunLevelFacts::Collect.vendored_standdowns} reports: the scan every build of this run gates on, or the
307
343
  # feature named under `libraries:`.
308
344
  def vendored_features_requested?(project_files)
309
345
  features = project_files.nil? || project_files.empty? ? [] : required_features_for(project_files)
@@ -331,25 +367,28 @@ module Rigor
331
367
  end
332
368
 
333
369
  # Issues #796 / #794 — fold the previous full run's run-level rows into this narrowed run's
334
- # snapshots. The definition-build set is UNIONed (`#record_definition_build_failures` dedupes by
370
+ # snapshots. The definition-build set is UNIONed ({RunLevelFacts::Merge.union_by_class} dedupes by
335
371
  # class), so a class this run demanded for itself and the replayed set agree; the HKT outcome is a
336
- # single tuple whoever demanded it, so `||=` keeps this run's own answer when it has one. Returns
372
+ # single tuple whoever demanded it, so first-wins keeps this run's own answer when it has one. Returns
337
373
  # true when a replay was available, which is what tells {#serve_empty_closure} it owes no HKT demand
338
374
  # of its own — and so no environment.
339
375
  #
340
376
  # @param closure — the files this run analyses; see {#stale_replayed_failure?}.
341
377
  # @param redemand_env — resolves the environment {#redemand_stale_failures} re-derives a stale
342
378
  # failure in; nil (the empty closure, which has no stale failure) re-derives nothing.
379
+ #
380
+ # Every replayable fact ({RunLevelFacts::Fact#replay}) is absorbed by its own merge rule; the
381
+ # definition-build set alone is filtered for staleness first.
343
382
  def replayed_restored_run_level_rows?(closure: nil, redemand_env: nil)
344
383
  rows = @restored_run_level_rows
345
384
  return false if rows.nil?
346
385
 
347
- stale, replayable = Array(rows.definition_build_failures).partition do |failure|
386
+ replayed = rows.to_h
387
+ stale, replayable = Array(replayed[:definition_build_failures]).partition do |failure|
348
388
  stale_replayed_failure?(failure, closure)
349
389
  end
350
- record_definition_build_failures(replayable)
390
+ @snapshots.absorb(replayed.merge(definition_build_failures: replayable))
351
391
  redemand_stale_failures(stale, redemand_env)
352
- record_hkt_scan_failure(rows.hkt_scan_failure)
353
392
  true
354
393
  end
355
394
 
@@ -375,7 +414,7 @@ module Rigor
375
414
  loader = redemand_env.call&.rbs_loader
376
415
  return if loader.nil?
377
416
 
378
- record_definition_build_failures(pending.filter_map { |name| loader.redemand_definition(name) })
417
+ @snapshots.absorb(definition_build_failures: pending.filter_map { |name| loader.redemand_definition(name) })
379
418
  end
380
419
 
381
420
  # A replayed definition-build failure the snapshot's own fingerprint cannot vouch for. The
@@ -419,92 +458,47 @@ module Rigor
419
458
  end
420
459
 
421
460
  def analyze_files_sequentially(files, environment)
422
- snapshot_project_signature_state(environment)
423
- snapshot_effect_annotation_carrier(environment&.rbs_loader)
461
+ harvest_environment_facts { environment }
424
462
  result = files.flat_map { |path| @analyze_file.call(path, environment) }
425
- # Issue #696 — AFTER the loop, deliberately. Definition builds are lazy (ADR-54 WD1: per class, on
426
- # first demand), so a snapshot taken beside the ones above — which run BEFORE `files.flat_map` —
427
- # would read an empty list on every run, including the ones this diagnostic exists for.
428
- record_definition_build_failures(environment&.rbs_loader&.definition_build_failures)
429
- # Issue #784 — same timing contract, same reason: the HKT scan is first demanded from inside a
430
- # file's analysis (the dispatcher's Singleton-receiver tier), so a snapshot taken any earlier
431
- # would read nil on every run. And demanded HERE once more by the run itself, because a subset
432
- # run (`--verify-incremental`'s partition, an incremental recheck's closure) may contain no file
433
- # that demands it — the row must not depend on which files were analysed. See {#hkt_scan_outcome}
434
- # for why a Rigor-owned demand is sound here where #696 forbids it.
435
- record_hkt_scan_failure(hkt_scan_outcome(environment))
436
- if @collect_stats
437
- loader = environment.rbs_loader
438
- @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
439
- @snapshots.signature_paths = loader&.signature_paths || [].freeze
440
- end
463
+ # Issues #696 / #784 — AFTER the loop, deliberately. Definition builds are lazy (ADR-54 WD1: per
464
+ # class, on first demand) and the HKT scan is first demanded from inside a file's analysis, so a
465
+ # read beside the harvest above would find nothing on every run. The HKT registry is demanded here
466
+ # once more by the run itself, because a subset run may contain no file that demands it (see
467
+ # {RunLevelFacts::Collect.hkt_scan_failure} for why a Rigor-owned demand is sound there).
468
+ @snapshots.absorb(RunLevelFacts.drain(environment))
469
+ harvest_stats { environment }
441
470
  result
442
471
  end
443
472
 
444
- # The whole-run findings about the project's OWN `signature_paths:` RBS — the namespaces the loader had
445
- # to synthesize, the files it had to quarantine, the total env-build failure (if any), and the
446
- # `rigor:v1:conforms-to` results. Snapshotted as small plain data (NOT the env) so `#run` can surface
447
- # them as diagnostics without rebuilding it.
448
- #
449
- # All are gated on the project actually declaring `signature_paths:`, for one reason: each forces the
450
- # (otherwise lazy) RBS env to build, and doing that when there is no project sig set would warm
451
- # `.rigor/cache` on a bare `--no-stats` run. The loader has already memoised each answer, so reading
452
- # them here is free.
453
- def snapshot_project_signature_state(environment)
454
- # Issue #610 — NOT behind the `signature_paths:` gate below: the source a bundled plugin's `sig/`
455
- # stands down against is typically an `rbs collection install` the configuration never lists
456
- # there. Gated instead on a loaded plugin contributing signatures at all — the registry answers
457
- # that without touching the env, and it is exactly the set the loader defers — so a project with
458
- # no such plugin reads nothing off its loader here, as before.
459
- @snapshots.signature_standdowns =
460
- plugin_signature_paths? ? signature_standdowns_for(environment&.rbs_loader) : []
461
- # Issue #1713 — NOT behind the `signature_paths:` gate either: what displaces a vendored declaration may
462
- # be a gem's or a collection's copy. The loader answers without building its env when no gated
463
- # directory is active.
464
- @snapshots.vendored_standdowns = vendored_standdowns_for(environment&.rbs_loader)
465
- unless project_signature_paths?
466
- @snapshots.synthesized_namespaces = []
467
- @snapshots.quarantined_signatures = []
468
- @snapshots.conformance_results = []
469
- @snapshots.env_build_failure = nil
470
- return
471
- end
473
+ # ADR-116 WD3 — the `:environment` run-level facts ({RunLevelFacts}), in declaration order, off the
474
+ # environment the block yields for each fact's gate: the project-signature state (#798 — every path
475
+ # takes it, pooled or not, because no per-file analysis produces it) and the #441 effect-annotation
476
+ # carrier. Snapshotted as small plain data (NOT the env) so the environment stays a LOCAL that goes
477
+ # GC-eligible when its path returns.
478
+ def harvest_environment_facts(&)
479
+ @snapshots.absorb(RunLevelFacts.harvest(:environment, harvest_context, &))
480
+ end
472
481
 
473
- # nil-safe: the empty-closure path reaches here with no environment when the project has no files.
474
- loader = environment&.rbs_loader
475
- @snapshots.synthesized_namespaces = loader&.synthesized_namespaces || []
476
- @snapshots.quarantined_signatures = loader&.quarantined_signatures || []
477
- @snapshots.env_build_failure = loader&.env_build_failure
478
- @snapshots.conformance_results = RbsExtended::ConformanceChecker.scan(loader)
482
+ # `RunStats` telemetry ONLY (`class_decl_paths` / `signature_paths`), which a `--no-stats` run
483
+ # legitimately skips. Never gates a diagnostic slot (#798).
484
+ def harvest_stats(&)
485
+ @snapshots.absorb(RunLevelFacts.harvest(:stats, harvest_context, &)) if @collect_stats
479
486
  end
480
487
 
481
- # #441 — the inline stratum of `effect.annotations-unchecked`, carried out of the analysis path as
482
- # small plain data, for the same reason the block above is: the environment stays a LOCAL so it goes
483
- # GC-eligible when the path returns, and the residual pass runs after it.
484
- #
485
- # {Runner#effect_annotation_residual_diagnostics} used to read the loader off `@run_environment`,
486
- # which only the ADR-45 result-cacheable path ever assigns — so a `--no-cache`, `--workers N` or
487
- # `--incremental` run reported a `.rbs` annotation and stayed silent about the identical one written
488
- # as an rbs-inline comment, while a default run reported both. The environment was there either way;
489
- # nothing was carrying it. {Effects::SignatureSources.annotated_carrier} reduces it to at most ONE
490
- # synthesized buffer, which is all a one-`:info`-per-run pass can spend.
491
- #
492
- # Skipped when collection is on — then {Runner::EffectEnvelopePass} reads the loader directly and
493
- # this pass is gated off — so a collecting run pays nothing, and a project with no synthesizing
494
- # plugin answers with an empty array and no regex at all.
495
- def snapshot_effect_annotation_carrier(loader)
496
- virtual = @record_effects ? nil : loader&.virtual_rbs
497
- @snapshots.effect_annotation_carrier = Effects::SignatureSources.annotated_carrier(virtual)
498
- rescue StandardError
499
- @snapshots.effect_annotation_carrier = [].freeze
488
+ def harvest_context
489
+ RunLevelFacts::Context.new(
490
+ project_signature_paths: project_signature_paths?,
491
+ plugin_signature_paths: plugin_signature_paths?,
492
+ record_effects: @record_effects
493
+ )
500
494
  end
501
495
 
502
496
  # Sequential-mode environment resolver. Returns the supplied `environment:` override (with the
503
497
  # runner's fresh per-run reporter pair attached so dispatcher events route to THIS runner's
504
498
  # diagnostics) when present; otherwise builds a fresh Environment per-call via
505
499
  # {#build_runner_environment} — preserving the pre-override behaviour bit-for-bit.
506
- def resolve_sequential_environment(source_files: [])
507
- return build_runner_environment(source_files: source_files) unless @environment_override
500
+ def resolve_sequential_environment(source_files: [], eager: true)
501
+ return build_runner_environment(source_files: source_files, eager: eager) unless @environment_override
508
502
 
509
503
  @environment_override.attach_reporters!(
510
504
  rbs_extended_reporter: @rbs_extended_reporter,
@@ -532,8 +526,10 @@ module Rigor
532
526
  # `project_scope_seed` its worker would need to record complete edges. Without `fork` (Windows) a
533
527
  # recording run degrades to sequential, which records correctly through `@analyze_file`.
534
528
  #
535
- # An effects run (ADR-103 WD13) is pinned for exactly the same reason — the Ractor messages carry
536
- # no side-table channel — and degrades the same way. The degrade is sound rather than merely safe:
529
+ # An effects run (ADR-103 WD13) is pinned the same way: the Ractor `:done` {WorkerPayload} has an
530
+ # `effects` slot, but the Ractor worker never collects into it (its session is built without the
531
+ # recording inputs, and the backend is off-default, #414), so only the fork backend fills it. Without
532
+ # `fork` it degrades the same way. The degrade is sound rather than merely safe:
537
533
  # the sequential fallback still collects, so the effect graph is complete either way.
538
534
  # @param source_files — the file list every worker's / the fallback's environment is
539
535
  # built over — the whole project (issue #793), defaulting to `files` for direct callers.
@@ -564,8 +560,9 @@ module Rigor
564
560
  # loaded plugin's `source_rbs_synthesizer` callable per project source file at env-build time.
565
561
  # Defaults to `[]` for callers that don't have a file list yet (e.g. pre-pass-only build paths); in
566
562
  # that case no synthesised RBS is contributed.
567
- def build_runner_environment(source_files: [])
563
+ def build_runner_environment(source_files: [], eager: true)
568
564
  Environment.for_project(
565
+ eager: eager,
569
566
  libraries: @configuration.libraries,
570
567
  signature_paths: @configuration.signature_paths,
571
568
  cache_store: @cache_store,
@@ -597,18 +594,18 @@ module Rigor
597
594
  end
598
595
 
599
596
  # ADR-15 Phase 4b — Ractor pool around {WorkerSession}. Spawns `@workers` Ractors; each takes the
600
- # shareable payload (Configuration, cache_root String, plugin Blueprint Array, explain Boolean) and
597
+ # shareable payload (configuration, cache root, plugin blueprints, explain flag) and
601
598
  # builds its OWN WorkerSession internally. Files are distributed round-robin across the pool; each
602
599
  # worker writes back to the main Ractor's mailbox via `Ractor.main.send` with one of three message
603
600
  # kinds:
604
601
  #
605
- # - `[:prepare, diagnostics]` — once at startup, the session's `prepare_diagnostics` snapshot. The
606
- # coordinator keeps the FIRST worker's snapshot only (plugin `#prepare` is deterministic per
602
+ # - `[:prepare, diagnostics, refusals]` — once at startup, the session's `prepare_diagnostics` snapshot.
603
+ # The coordinator keeps the FIRST worker's snapshot only (plugin `#prepare` is deterministic per
607
604
  # plugin, so each worker produces the same diagnostic set; surfacing them once avoids N×
608
605
  # duplication).
609
- # - `[:file, path, diagnostics]` — one per analysed file.
610
- # - `[:done, drained_reporters]` — once at exit, the per-worker reporter snapshots for end-of-pool
611
- # merge.
606
+ # - `[:file, path, diagnostics, refusals]` — one per analysed file.
607
+ # - `[:done, payload]` — once at exit, the worker's {WorkerPayload} (its reporter snapshots, drained
608
+ # run-level facts and disclosures) for end-of-pool merge.
612
609
  #
613
610
  # The Ruby 4.0+ Ractor model uses a single per-Ractor mailbox (no `Ractor.yield`); workers push
614
611
  # back via `Ractor.main.send`. The coordinator drains its mailbox via `Ractor.receive` until it has
@@ -695,9 +692,9 @@ module Rigor
695
692
  )
696
693
  )
697
694
  warm_env = prewarm_rbs_cache_for_pool(source_files: source_files, locked_gems: locked_gems)
698
- snapshot_project_signature_state(warm_env)
699
- snapshot_effect_annotation_carrier(warm_env&.rbs_loader)
700
- record_definition_build_failures(warm_env&.rbs_loader&.definition_build_failures)
695
+ harvest_environment_facts { warm_env }
696
+ @snapshots.absorb(RunLevelFacts.drain(warm_env, demanding: false))
697
+ harvest_stats { warm_env }
701
698
 
702
699
  configuration = @configuration
703
700
  cache_root = @cache_store&.root
@@ -705,8 +702,7 @@ module Rigor
705
702
  explain = @explain
706
703
  # ADR-32 WD4 — the full project file list travels into every Ractor worker so each worker's
707
704
  # WorkerSession can invoke loaded plugins' source_rbs_synthesizers at env-build time. The list is
708
- # a frozen Array<String>; cheaply shareable. Issue #793 — it IS the full project now
709
- # (`source_files`), not the analyzed subset this comment always described.
705
+ # a frozen list of strings; cheaply shareable.
710
706
  shareable_source_files = source_files.map { |path| path.to_s.dup.freeze }.freeze
711
707
  features = required_features_for(source_files)
712
708
 
@@ -737,7 +733,10 @@ module Rigor
737
733
  # Issue #1051 — sent with `:done`, not with `:prepare`: this backend builds no parent session,
738
734
  # so the worker's own table is the only place a disclosure exists, and it is complete only
739
735
  # once the worker has finished its files.
740
- main.send([:done, session.drain_reporters, session.drain_run_disclosures])
736
+ main.send([:done, Rigor::Analysis::Runner::WorkerPayload.harvest(session)])
737
+ # `Ractor#send` answers the Ractor; the worker's own value is unread (`join` only), and G3 reads a
738
+ # messaging call as an address only while its value is discarded.
739
+ nil
741
740
  end
742
741
  end
743
742
 
@@ -746,8 +745,11 @@ module Rigor
746
745
  # Ractor's default port, which is the same mailbox the workers push their results to.
747
746
  pool.each { |worker| worker.monitor(Ractor.current.default_port) }
748
747
 
749
- files.each_with_index { |path, index| pool[index % pool.size].send(path) }
750
- pool.each { |worker| worker.send(nil) }
748
+ # A worker that died before its turn has a closed port: `Ractor#send` raises `Ractor::ClosedError`.
749
+ # That is the same death the monitor loop below counts, so it is not a failure of the run — the
750
+ # file simply never reports and lands in the degrade, like any file a dead worker never answered.
751
+ files.each_with_index { |path, index| send_to_worker(pool[index % pool.size], path) }
752
+ pool.each { |worker| send_to_worker(worker, nil) }
751
753
 
752
754
  prepare_diagnostics = nil
753
755
  prepare_refusals = nil
@@ -775,8 +777,7 @@ module Rigor
775
777
  results_by_path[message[1]] = message[2]
776
778
  refusals_by_path[message[1]] = message[3]
777
779
  when :done
778
- merge_worker_reporters(message[1])
779
- merge_run_disclosures(message[2])
780
+ absorb_worker_payload(message[1])
780
781
  end
781
782
  end
782
783
 
@@ -824,8 +825,7 @@ module Rigor
824
825
 
825
826
  environment = build_runner_environment(source_files: source_files)
826
827
  degraded.each { |path| results_by_path[path] = @analyze_file.call(path, environment) }
827
- record_definition_build_failures(environment.rbs_loader&.definition_build_failures)
828
- record_hkt_scan_failure(hkt_scan_outcome(environment))
828
+ @snapshots.absorb(RunLevelFacts.drain(environment))
829
829
  end
830
830
 
831
831
  # ADR-15 Amendment (2026-05-20) — fork-based worker pool, the active backend for `workers > 0`.
@@ -873,14 +873,13 @@ module Rigor
873
873
  # a resulting definition-build failure is already sitting in the parent's loader at the moment
874
874
  # each child copy-on-write-inherits it — every child's own `#drain_reporters` then reports it
875
875
  # right alongside whatever its own slice demanded, with no separate plumbing needed. Left
876
- # unconditional, unlike `#snapshot_fork_pool_stats` below: these are diagnostic rows, not
876
+ # unconditional, unlike `#harvest_stats` below: these are diagnostic rows, not
877
877
  # `RunStats` telemetry, and the stats gate must not decide which diagnostics a run reports (a
878
878
  # `--workers N --no-stats` run used to say strictly LESS than the sequential path over the
879
879
  # same project).
880
- snapshot_project_signature_state(session.environment)
881
- snapshot_effect_annotation_carrier(session.environment.rbs_loader)
882
- # Issue #1051 — taken BEFORE the fork, for the same reason and with the same effect as the two
883
- # snapshots above: `#prepare` ran on the parent when the session was built, so every disclosure a
880
+ harvest_environment_facts { session.environment }
881
+ # Issue #1051 — taken BEFORE the fork, for the same reason and with the same effect as the harvest
882
+ # above: `#prepare` ran on the parent when the session was built, so every disclosure a
884
883
  # plugin registers there (the normal case — a disclosure is a fact about the project's inputs,
885
884
  # known before a file is read) is already in this table, in the same order the sequential path
886
885
  # would produce. Each child inherits the identical table and re-offers it in its payload, where
@@ -891,7 +890,8 @@ module Rigor
891
890
  # session's record of what it already reported and drains only its own slice's, so the prepare-time
892
891
  # refusal is counted once, as `--workers 0` counts it, rather than once per worker.
893
892
  merge_trust_refusals(session.drain_trust_refusals)
894
- snapshot_fork_pool_stats(session) if @collect_stats
893
+ # `RunStats` telemetry only, off the same pre-fork environment.
894
+ harvest_stats { session.environment }
895
895
 
896
896
  worker_count = [@workers, files.size].min
897
897
  slices = files.each_slice((files.size.to_f / worker_count).ceil).to_a
@@ -915,16 +915,10 @@ module Rigor
915
915
  results_by_path[path] = session.analyze(path)
916
916
  refusals_by_path[path] = session.drain_trust_refusals
917
917
  end
918
- merge_worker_reporters(session.drain_reporters)
919
- # The degraded slice ran on the parent session, so a disclosure it registered lives there.
920
- merge_run_disclosures(session.drain_run_disclosures)
921
- # The degraded slice was re-analysed on the parent session, so its dependency records live there,
922
- # not in a child payload — fold them in alongside the successful children's.
923
- if @record_dependencies
924
- collect_dependencies(session.drain_dependencies, session.drain_analysis_reads,
925
- session.drain_analysis_disclosures)
926
- end
927
- @collected_effects.merge!(session.drain_effects) if @record_effects
918
+ # The degraded slice ran on the parent session, so its reporters, run-level facts, disclosures,
919
+ # dependency records and effects live there, not in a child payload — fold them in alongside the
920
+ # successful children's. Its refusals were drained file by file above.
921
+ absorb_worker_payload(WorkerPayload.harvest(session, record_effects: @record_effects))
928
922
  end
929
923
 
930
924
  merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
@@ -957,34 +951,18 @@ module Rigor
957
951
  # mode is opted into for large projects, so a slice reliably outlasts the remainder; a small-project
958
952
  # worker finishes first and stays at no-YJIT parity, exactly as the sequential path would.
959
953
  Runtime::Jit.rearm_after_fork
954
+ TypeOfMemo.forked
960
955
  results = slice.to_h { |path| [path, session.analyze(path)] }
961
- payload = { results: results, reporters: session.drain_reporters,
962
- dependencies: session.drain_dependencies,
963
- analysis_reads: session.drain_analysis_reads,
964
- analysis_disclosures: session.drain_analysis_disclosures,
965
- disclosures: session.drain_run_disclosures,
966
- refusals: session.drain_trust_refusals }
967
- # ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
968
- # marshals exactly the payload it marshalled before the feature existed.
969
- payload[:effects] = session.drain_effects if @record_effects
956
+ # ADR-103 WD13 — the effects slot is filled only on a collecting run.
957
+ payload = WorkerPayload.harvest(session, results: results, record_effects: @record_effects)
970
958
  File.binwrite(out_path, Marshal.dump(payload))
959
+ TypeOfMemo.write_report
971
960
  exit!(0)
972
961
  rescue StandardError
962
+ TypeOfMemo.write_report
973
963
  exit!(1)
974
964
  end
975
965
 
976
- # Issue #798 — `RunStats` telemetry ONLY, now that `#snapshot_project_signature_state` (called
977
- # unconditionally above) owns every diagnostic-bearing slot this method used to ALSO write
978
- # (`quarantined_signatures`, `env_build_failure`): those must not be gated on `@collect_stats`, and
979
- # this method's own name says what is left — `class_decl_paths` / `signature_paths`, read off the
980
- # parent session's loader so end-of-run {RunStats} can attribute the RBS class universe, which a
981
- # `--no-stats` run legitimately skips.
982
- def snapshot_fork_pool_stats(session)
983
- loader = session.environment.rbs_loader
984
- @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
985
- @snapshots.signature_paths = loader&.signature_paths || [].freeze
986
- end
987
-
988
966
  # Waits for every forked child, merges each successful payload into `results_by_path`, and returns
989
967
  # the file paths whose worker exited abnormally (for in-process degrade).
990
968
  # @param refusals_by_slice — issue #1900 — receives each successful payload's trust-refusal drain
@@ -995,16 +973,10 @@ module Rigor
995
973
  _, status = Process.waitpid2(child[:pid])
996
974
  payload = fork_worker_payload(status, child[:out_path])
997
975
  if payload
998
- results_by_path.merge!(payload.fetch(:results))
999
- merge_worker_reporters(payload.fetch(:reporters))
1000
- merge_run_disclosures(payload.fetch(:disclosures, []))
1001
- refusals = payload.fetch(:refusals, [])
976
+ results_by_path.merge!(payload.results)
977
+ absorb_worker_payload(payload)
978
+ refusals = Array(payload.refusals)
1002
979
  refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
1003
- if @record_dependencies
1004
- collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {}),
1005
- payload.fetch(:analysis_disclosures, {}))
1006
- end
1007
- @collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
1008
980
  else
1009
981
  degraded.concat(child[:slice])
1010
982
  end
@@ -1020,7 +992,7 @@ module Rigor
1020
992
  collected_analysis_disclosures.merge!(analysis_disclosures)
1021
993
  end
1022
994
 
1023
- # @return the child's `{results:, reporters:}` payload, or nil when the child exited
995
+ # @return the child's {WorkerPayload}, or nil when the child exited
1024
996
  # abnormally or wrote no readable payload. `Marshal.load` is safe here: the blob was written by
1025
997
  # our own forked child to a temp file we created.
1026
998
  def fork_worker_payload(status, out_path)
@@ -1035,7 +1007,7 @@ module Rigor
1035
1007
  # process — so they say so the same way, naming which backend it was (#414).
1036
1008
  def pool_degraded_diagnostic(count, backend)
1037
1009
  Diagnostic.new(
1038
- path: ".rigor.yml", line: 1, column: 1,
1010
+ path: @configuration.config_display_path, line: 1, column: 1,
1039
1011
  message: "#{backend} pool degraded: #{count} file(s) re-analysed in-process " \
1040
1012
  "after a worker exited abnormally",
1041
1013
  severity: :warning, rule: "pool-degraded", source_family: :builtin
@@ -1093,8 +1065,7 @@ module Rigor
1093
1065
  # sequential analysis with a `:warning` `pool-degraded` diagnostic at run start. The actual
1094
1066
  # per-file analysis runs on the coordinator, identical to the default sequential path.
1095
1067
  # @param source_files — issue #793 — the whole project, so this path's environment
1096
- # carries the same plugin-synthesized RBS the pool workers' would. It used to build over `[]`,
1097
- # i.e. with no synthesized RBS at all, even on a full run.
1068
+ # carries the same plugin-synthesized RBS the pool workers' would.
1098
1069
  #
1099
1070
  # Issue #1900 — it also runs `#prepare` on the coordinator-side registry it analyses with, BEFORE the
1100
1071
  # environment build as the sequential pre-pass does. A pooled run's pre-pass skips `#prepare` (the
@@ -1105,28 +1076,21 @@ module Rigor
1105
1076
  def analyze_files_sequentially_fallback(files, reason:, source_files: files)
1106
1077
  prepare_rows = prepare_coordinator_plugins_once
1107
1078
  environment = build_runner_environment(source_files: source_files)
1108
- # Same set, same ORDER as `#analyze_files_sequentially`. The signature state used to be hand-copied
1109
- # here slot by slot, which dropped `synthesized-namespace` and every conformance row. The conformance
1110
- # scan inside it demands the definition of every `rigor:v1:conforms-to` class, so it has to run
1111
- # BEFORE the definition-build failures are read below, or a failure only that scan reaches is lost.
1112
- snapshot_project_signature_state(environment)
1113
- snapshot_effect_annotation_carrier(environment.rbs_loader)
1079
+ # Same harvest, same ORDER as `#analyze_files_sequentially` (#1201: a hand-copied list here once
1080
+ # dropped `synthesized-namespace` and every conformance row). The conformance scan inside it demands
1081
+ # the definition of every `rigor:v1:conforms-to` class, so it runs BEFORE the definition-build
1082
+ # failures are drained below, or a failure only that scan reaches is lost.
1083
+ harvest_environment_facts { environment }
1114
1084
  diagnostics = prepare_rows + files.flat_map { |path| @analyze_file.call(path, environment) }
1115
- loader = environment.rbs_loader
1116
- # Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
1117
- # failing build, and it must snapshot the same slot the two real paths do. Reached whenever
1118
- # `fork` is unavailable (Windows) and on `--incremental` / effects runs without it: a run that
1119
- # degraded to sequential must not also report less than a sequential run would.
1120
- record_definition_build_failures(loader&.definition_build_failures)
1121
- # Issue #784 — same reasoning: this path's Environment IS the one that reached the scan, so it
1122
- # must snapshot the slot too, or a run that degraded to sequential would report less than a
1123
- # sequential run would. Demanded once by the run as well, for the reason at the sequential site.
1124
- record_hkt_scan_failure(hkt_scan_outcome(environment))
1125
- @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
1126
- @snapshots.signature_paths = loader&.signature_paths || [].freeze
1085
+ # Issues #696 / #784 — this path analyses on the coordinator, so its environment IS the one that
1086
+ # reached the failing build and the scan, and it drains the same facts the sequential path does.
1087
+ # Reached whenever `fork` is unavailable (Windows) and on `--incremental` / effects runs without it:
1088
+ # a run that degraded to sequential must not also report less than a sequential run would.
1089
+ @snapshots.absorb(RunLevelFacts.drain(environment))
1090
+ harvest_stats { environment }
1127
1091
  diagnostics.unshift(
1128
1092
  Diagnostic.new(
1129
- path: ".rigor.yml", line: 1, column: 1,
1093
+ path: @configuration.config_display_path, line: 1, column: 1,
1130
1094
  message: "pool mode degraded to sequential: #{reason}",
1131
1095
  severity: :warning, rule: "pool-degraded", source_family: :builtin
1132
1096
  )
@@ -1142,149 +1106,42 @@ module Rigor
1142
1106
  Array(@prepare_coordinator_plugins.call)
1143
1107
  end
1144
1108
 
1109
+ # One worker drain ({WorkerSession#drain_reporters}): each reporter absorbs its own snapshot, by
1110
+ # its primitives (#785, #805) so every worker's copy of an entry collapses to the single row a
1111
+ # `--workers=0` run prints, and the drained `:analysis` facts fold in by their merge rules — the
1112
+ # definition-build union (#696) and the first-wins HKT outcome (#784). A key an older drain shape
1113
+ # lacks records nothing.
1145
1114
  def merge_worker_reporters(drained)
1146
- merge_rbs_extended_reporter(drained.fetch(:rbs_extended))
1147
- drained.fetch(:boundary_cross).each do |entry|
1148
- @boundary_cross_reporter.record(
1149
- class_name: entry.class_name,
1150
- method_name: entry.method_name,
1151
- gem_name: entry.gem_name,
1152
- rbs_display: entry.rbs_display
1153
- )
1154
- end
1155
- # ADR-32 WD6 — merge per-worker synthesizer failures back into the coordinator's reporter. Fetched
1156
- # with a default empty array so older drains (pre-slice-2) remain compatible.
1157
- #
1158
- # `kind:` rides along (issue #824): without it every drained entry replayed as WD6's default
1159
- # `:failed`, so under `--workers N` a WD12 "parsed but not honoured" row came back as
1160
- # `source-rbs-synthesis-failed` — a different id, and a message telling the user the file
1161
- # contributed nothing when all but one of its annotations bound. Every worker reads the same
1162
- # sources and so hands over the same entries; the reporter's own dedup collapses them. `line:` rides
1163
- # along for the same reason (#1075): a consistency error is positioned at its `.rbs` member.
1164
- Array(drained[:source_rbs_synthesis]).each do |entry|
1165
- @source_rbs_synthesis_reporter.record(
1166
- plugin_id: entry.plugin_id, path: entry.path, message: entry.message, kind: entry.kind,
1167
- line: entry.line
1168
- )
1169
- end
1170
- # Issue #696. Fetched with a default so an older drain stays compatible, exactly as the line above.
1171
- record_definition_build_failures(drained[:definition_build_failures])
1172
- # Issue #784. `Hash#[]` is already a nil default, exactly as `env_build_failure` is snapshotted
1173
- # elsewhere — an older drain shape simply has no key and records nothing.
1174
- record_hkt_scan_failure(drained[:hkt_scan_failure])
1115
+ RunLevelFacts.absorb_reporters(reporters, drained)
1116
+ @snapshots.absorb(RunLevelFacts.drained_facts(drained))
1175
1117
  end
1176
1118
 
1177
- private
1178
-
1179
- # Replays one worker's three {RbsExtended::Reporter} streams into the run's own reporter.
1180
- #
1181
- # `hkt_directive_errors` (issue #785) is read with `Hash#[]`'s nil default rather than `fetch`,
1182
- # exactly as `source_rbs_synthesis` is, so an older drain shape simply records nothing.
1183
- #
1184
- # All three streams replay by their `(path, line, column)` primitives (#785 for the hkt stream, #805
1185
- # for its two elders). Every worker reads the same `.rbs`, so each hands over the same entries, and
1186
- # only a primitive-keyed dedup collapses them back to the single row a `--workers=0` run prints — an
1187
- # `RBS::Location` compares equal only against a location over the same `RBS::Buffer` object, so a
1188
- # location-keyed one would print N copies at `--workers=N`. The entries could not reach here carrying
1189
- # a location anyway: the drain Marshals its payload and an `RBS::Location` has no `_dump`.
1190
- def merge_rbs_extended_reporter(rbs)
1191
- rbs.fetch(:unresolved_payloads).each do |entry|
1192
- @rbs_extended_reporter.record_unresolved(
1193
- payload: entry.payload, path: entry.path, line: entry.line, column: entry.column
1194
- )
1195
- end
1196
- rbs.fetch(:lossy_projections).each do |entry|
1197
- @rbs_extended_reporter.record_lossy_projection(
1198
- head: entry.head, path: entry.path, line: entry.line, column: entry.column
1199
- )
1200
- end
1201
- Array(rbs[:hkt_directive_errors]).each do |entry|
1202
- @rbs_extended_reporter.record_hkt_error(
1203
- message: entry.message, path: entry.path, line: entry.line, column: entry.column
1204
- )
1205
- end
1206
- replay_deprecated_forms(rbs)
1207
- end
1208
-
1209
- # ADR-109 WD3 — the fourth stream, read with the nil default like the hkt one so an older drain
1210
- # shape records nothing.
1211
- def replay_deprecated_forms(rbs)
1212
- Array(rbs[:deprecated_forms]).each do |entry|
1213
- @rbs_extended_reporter.record_deprecated_form(
1214
- payload: entry.payload, replacement: entry.replacement,
1215
- path: entry.path, line: entry.line, column: entry.column
1216
- )
1119
+ # One {WorkerPayload}, minus its per-file results and its refusals, which each backend merges in its
1120
+ # own file order.
1121
+ def absorb_worker_payload(payload)
1122
+ merge_worker_reporters(payload.reporters)
1123
+ merge_run_disclosures(payload.disclosures)
1124
+ if @record_dependencies
1125
+ collect_dependencies(payload.dependencies || {}, payload.analysis_reads || {},
1126
+ payload.analysis_disclosures || {})
1217
1127
  end
1128
+ @collected_effects.merge!(payload.effects || {}) if @record_effects
1218
1129
  end
1219
1130
 
1220
- # Issue #696 — accumulate the per-class `RBS::DefinitionBuilder` failures this run observed, deduped
1221
- # by class name and kept in first-seen order.
1222
- #
1223
- # Accumulating rather than assigning is what makes a pooled run say what a sequential one says: each
1224
- # worker holds its OWN loader and its own memo (the `@state` that also gates the stderr banner, which
1225
- # is why the banner repeats per worker — #295), so a class that fails in two workers arrives twice
1226
- # and a class only one worker touched arrives once. The union over the workers is the set the RUN
1227
- # hit; neither worker alone is.
1228
- #
1229
- # NOT gated on `project_signature_paths?`, unlike {#snapshot_project_signature_state}. That gate
1230
- # exists because each condition there forces the otherwise-lazy env build, which would warm the cache
1231
- # on a project with no sig set of its own. Nothing is forced here — this reads what the analysis
1232
- # already demanded — and the failure is not a project-sig-only condition anyway: two BUNDLED sources
1233
- # declaring the same method collapse a class with no `signature_paths:` in sight (the
1234
- # `bigdecimal` / `BigMath` shape, #299). Gating it would suppress exactly the variant the user cannot
1235
- # fix by editing their own `sig/`.
1236
- def record_definition_build_failures(failures)
1237
- return if failures.nil? || failures.empty?
1238
-
1239
- # Issue #1902 — one tuple per class, chosen by {Environment::RbsLoader.definition_build_detail_order}
1240
- # rather than by arrival: a fresh record and a replayed one (or two workers' records) for the same
1241
- # class can name different failures, and the earlier one used to win.
1242
- @snapshots.definition_build_failures =
1243
- (@snapshots.definition_build_failures + failures)
1244
- .group_by(&:first)
1245
- .map { |_, candidates| candidates.min_by { |c| Environment::RbsLoader.definition_build_detail_order(c) } }
1246
- .freeze
1247
- end
1131
+ private
1248
1132
 
1249
- # Issue #784 — first-wins, unlike {#record_definition_build_failures}'s accumulate-and-dedup. That
1250
- # method accumulates because each pool WORKER owns its own loader and its own per-class memo, so a
1251
- # collapsed class can genuinely be observed by only some workers and the run's set is the union.
1252
- # The HKT scan has no such per-worker variation: `Environment#hkt_registry` builds from the SAME
1253
- # `signature_paths:` overlay every worker was handed, so every worker that demands it either all
1254
- # raise identically or all succeed — there is only ever one tuple to record, and `||=` is correct
1255
- # (and cheap) rather than an accumulate-and-dedup this slot never needs.
1256
- def record_hkt_scan_failure(tuple)
1257
- @snapshots.hkt_scan_failure ||= tuple
1133
+ # `Ractor#send` to a worker whose port is already closed (it died before this send, whether before
1134
+ # dispatch or part-way through it). The death is observed through the monitor notice, and the unsent
1135
+ # file is re-analysed by the degrade.
1136
+ def send_to_worker(worker, message)
1137
+ worker.send(message)
1138
+ rescue Ractor::ClosedError
1139
+ nil
1258
1140
  end
1259
1141
 
1260
- # Issue #784 — demand the registry once on behalf of the run, then read the slot. The seam in
1261
- # {Environment#hkt_registry} is demand-driven, and nothing guarantees any analysed file demands it:
1262
- # a `--verify-incremental` partition or an incremental closure can miss every `Klass.method` call,
1263
- # and then the slot is nil after the loop and the run says nothing — while `--incremental` has also
1264
- # dropped the row from its per-file cache. Demanding here makes the outcome a property of the RUN,
1265
- # not of which files happened to be in it.
1266
- #
1267
- # This is sound where #696's "a demand that is Rigor's own MUST NOT contribute" is not, and the
1268
- # difference is the shape of what is recorded. #696 reports a per-class LIST whose membership is
1269
- # "the classes the analysis demanded"; a Rigor-internal demand adds classes the user never asked
1270
- # about and makes that list vary with configuration. The HKT scan is ONE build with ONE outcome —
1271
- # the same tuple whoever demands it — so an extra demand cannot change what is reported, only
1272
- # guarantee it is observed.
1273
- #
1274
- # What it costs, stated plainly because {#record_definition_build_failures}'s comment promises
1275
- # "nothing is forced here": the demand DOES force the RBS env build when the environment has not
1276
- # built it yet (the scan reads the loader). On every path that reaches this method that build is
1277
- # either already done (the loop demanded it) or the same load one analysed file would have paid:
1278
- # a Marshal load when a cache store exists, the parse a full run pays under `--no-cache`. Never a
1279
- # build on a project with no files (see {#analyze_files}). Memoised, so on a reused Environment
1280
- # this is a hash read.
1281
- #
1282
- # @return the recorded tuple, or nil (no environment, or the scan built)
1283
- def hkt_scan_outcome(environment)
1284
- return nil if environment.nil?
1285
-
1286
- environment.hkt_registry
1287
- environment.hkt_scan_failure
1142
+ def reporters
1143
+ { rbs_extended: @rbs_extended_reporter, boundary_cross: @boundary_cross_reporter,
1144
+ source_rbs_synthesis: @source_rbs_synthesis_reporter }
1288
1145
  end
1289
1146
 
1290
1147
  # True when the project declares its own `signature_paths:` (the only place the
@@ -1294,25 +1151,13 @@ module Rigor
1294
1151
  !(paths.nil? || paths.empty?)
1295
1152
  end
1296
1153
 
1297
- # Issue #610 — see {#snapshot_project_signature_state}. The registry's `signature_paths` is what
1154
+ # Issue #610 — see {RunLevelFacts::Collect.signature_standdowns}. The registry's `signature_paths` is what
1298
1155
  # `Environment.for_project` defers, so this is the loader-free form of "is anything deferred?".
1299
1156
  def plugin_signature_paths?
1300
1157
  registry = plugin_registry
1301
1158
  registry.respond_to?(:signature_paths) && !registry.signature_paths.empty?
1302
1159
  end
1303
1160
 
1304
- # Empty for a loader with nothing deferred, without touching its env.
1305
- def signature_standdowns_for(loader)
1306
- return [] if loader.nil? || !loader.respond_to?(:deferred_signature_paths)
1307
- return [] if loader.deferred_signature_paths.empty?
1308
-
1309
- loader.signature_standdowns
1310
- end
1311
-
1312
- def vendored_standdowns_for(loader)
1313
- loader.respond_to?(:vendored_standdowns) ? loader.vendored_standdowns : []
1314
- end
1315
-
1316
1161
  def plugin_registry
1317
1162
  @plugin_registry_reader.call
1318
1163
  end