rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../source/node_children"
4
+
3
5
  module Rigor
4
6
  module Analysis
5
7
  # Issues #665 / #674 / #683 / #686 / #696 — the ONE definition of "an internal failure made this run
@@ -104,12 +106,52 @@ module Rigor
104
106
  # into. Built here, not at either rescue site, so the two twins cannot drift (issue #665) — and so
105
107
  # the appended crash frame is derived identically on the sequential and pooled paths. Keeps the
106
108
  # {CHECK_RULE_MESSAGE_PREFIX} prefix every consumer matches on; the frame is a trailing hint.
107
- def check_rule_message(error)
109
+ def check_rule_message(error, root = nil)
110
+ return stack_overflow_message(error, root) if error.is_a?(SystemStackError)
111
+
108
112
  base = "#{CHECK_RULE_MESSAGE_PREFIX}: #{error.class}: #{error.message}"
109
113
  frame = crash_frame(error)
110
114
  frame ? "#{base} (#{frame})" : base
111
115
  end
112
116
 
117
+ # The crash-row message for a rescued error, for the per-file rescues of `Runner` and `WorkerSession`. Re-raises a
118
+ # `SystemStackError` raised from C (no backtrace): that one leaves no stack to recover on and a second would kill
119
+ # the process with a signal, so it keeps the run-level abort ({CLI::FATAL_ERRORS}, exit 70). `parse_result` may be
120
+ # nil when the error predates the parse.
121
+ def contained_message!(error, parse_result)
122
+ raise error if error.is_a?(SystemStackError) && error.backtrace.nil?
123
+
124
+ check_rule_message(error, parse_result&.value)
125
+ end
126
+
127
+ # #2455 — the AST depth above which a `SystemStackError` is read as the file's own nesting rather than a Rigor
128
+ # recursion defect. The recursive walkers overflow from about 700 nested `if`s (twice as many AST levels) and
129
+ # 1,300 nested array literals or chained calls, so a file past this bound is one they cannot be expected to walk.
130
+ DEEP_NESTING_BOUND = 500
131
+
132
+ DEEP_NESTING_DETAIL = "this file is nested too deeply to analyse (stack level too deep); " \
133
+ "split the deeply nested expression"
134
+ STACK_DEFECT_DETAIL = "stack level too deep; this is a defect in Rigor, please report it at " \
135
+ "https://github.com/rigortype/rigor/issues"
136
+
137
+ # No crash frame: the frame that overflowed is whichever the stack ran out in, which differs by mode and cache and
138
+ # would make the row unstable. `root` is nil when the file never parsed, which reads as the defect wording.
139
+ def stack_overflow_message(error, root)
140
+ deep = !root.nil? && Source::NodeChildren.nested_deeper_than?(root, DEEP_NESTING_BOUND)
141
+ "#{CHECK_RULE_MESSAGE_PREFIX}: #{error.class}: #{deep ? DEEP_NESTING_DETAIL : STACK_DEFECT_DETAIL}"
142
+ end
143
+
144
+ # ADR-129 WD5 (#2785) — `diagnostics` (one file's) with a crash row for each of `errors`, raised errors the
145
+ # typing path may have swallowed under a `rescue StandardError` (`MemoAudit`'s mismatches), so a swallowed one
146
+ # is reported exactly as one no rescue caught. An error that did reach the file's crash row is not repeated.
147
+ def with_swallowed(path, diagnostics, errors)
148
+ return diagnostics if errors.empty?
149
+
150
+ seen = diagnostics.map(&:message)
151
+ rows = errors.map { |error| check_rule_message(error) }.uniq.reject { |message| seen.include?(message) }
152
+ diagnostics + rows.map { |message| Diagnostic.new(path: path, line: 1, column: 1, message: message) }
153
+ end
154
+
113
155
  # The first `lib/rigor/` backtrace frame — the raise site, path made repo-relative so it reads the
114
156
  # same whether Rigor runs from a checkout or an installed gem. A LOCATION, not an attribution: a
115
157
  # bundled or third-party plugin also lives under `lib/rigor/<plugin>/`, so a frame here says where
@@ -149,8 +191,8 @@ module Rigor
149
191
 
150
192
  # True when `diagnostic` reports a failure inside Rigor that left the run readable but invalid as a
151
193
  # measurement of Rigor — see {ANALYZER_DEFECT_RULES}. Orthogonal to {.discards_file_analysis?}: the
152
- # user-facing tier reads the run; the Rigor-measuring tier should refuse it (the mutation fuzz does,
153
- # the ADR-69 kill oracle does not yet — #790).
194
+ # user-facing tier reads the run; the Rigor-measuring tier should refuse it (the mutation fuzz and the
195
+ # ADR-69 kill oracle both do, #790).
154
196
  def analyzer_defect?(diagnostic)
155
197
  ANALYZER_DEFECT_RULES.include?(diagnostic.rule)
156
198
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../typing_state"
4
+
3
5
  module Rigor
4
6
  module Analysis
5
7
  # ADR-46 slice 1 — records, per analyzed file, which OTHER source files its analysis read declarations /
@@ -12,9 +14,67 @@ module Rigor
12
14
  #
13
15
  # Modelled on {Inference::BudgetTrace}: process-thread-local state, a cheap disabled fast path, and a
14
16
  # frozen snapshot for consumers.
15
- module DependencyRecorder
16
- KEY = :__rigor_dependency_recorder__
17
- private_constant :KEY
17
+ module DependencyRecorder # rubocop:disable Metrics/ModuleLength
18
+ # ADR-129 WD1 — the installed accumulator is a Frame whose contents are a Resource for its suspension count, the
19
+ # one mutation that touches ({record_for}); its filings are entries of the bound Table {READS} and are journaled
20
+ # instead. WD4 shuts the `type_of` memo under recording only until these reads are journaled, which they now are.
21
+ # The activation count below is a process switch, outside the typing state (WD5).
22
+ RECORDER = TypingState.frame(
23
+ :dependency_recorder,
24
+ contents: :resource, reason: "the consumer's accumulator a recording installs; each read files into it",
25
+ # G1′ witness: the dependency reads are a journaled Table (WD4), so the memo may run under recording, and typing
26
+ # reads the accumulator (`read_site`, `read_name`) inside `type_of`.
27
+ witness: "spec/fixtures/typing_state_witnesses/dependency_recorder.rb"
28
+ )
29
+ private_constant :RECORDER
30
+
31
+ # ADR-129 WD1 — the recorded dependency reads are a bound Table: the installed {Accumulator} and each open
32
+ # {Capture} are its stores, keyed on the event tuples they deduplicate on (a frozen `[path, symbol_or_nil]` pair
33
+ # for a positive edge, a `"kind:name"` String for a name-keyed one). Each event that files into a store logs a
34
+ # read of its key (`true` when the store already held it, else {TypingState::ABSENT}) and, when it adds, a write
35
+ # of `true`, while a capture is open, so a later `type_of` memo can validate the reads and replay the writes.
36
+ # The keys are built only then: a run with no journal capture open allocates nothing for the journal, whether
37
+ # or not an ADR-84 capture is open.
38
+ # ADR-129 WD3 — {READS}'s entry protocol over an {Accumulator} or a {Capture}: a positive edge's
39
+ # `[path, symbol_or_nil]` pair is held in the accumulator's symbol or ancestry table (a capture's `reads`), a
40
+ # name-keyed `"kind:name"` String in `missing`. Every entry is `true`; a replayed write files the edge the way the
41
+ # event that logged it did.
42
+ module Entries
43
+ module_function
44
+
45
+ def entry(store, key)
46
+ held = if key.is_a?(String) then store.missing.include?(key)
47
+ elsif store.is_a?(Capture) then store.reads.include?(key)
48
+ else store.holds_read?(key[0], key[1])
49
+ end
50
+ held ? true : TypingState::ABSENT
51
+ end
52
+
53
+ def put(store, key, _value)
54
+ if key.is_a?(String) then store.missing.add(key)
55
+ elsif store.is_a?(Capture) then store.reads.add(key)
56
+ else store.add_read(key[0], key[1])
57
+ end
58
+ end
59
+
60
+ def delete(store, key)
61
+ if key.is_a?(String) then store.missing.delete(key)
62
+ elsif store.is_a?(Capture) then store.reads.delete(key)
63
+ else store.remove_read(key[0], key[1])
64
+ end
65
+ end
66
+
67
+ def container(_state, _slot)
68
+ raise ArgumentError, "dependency reads are a bound table: a store is an accumulator or a capture"
69
+ end
70
+ end
71
+
72
+ READS = TypingState.table(
73
+ :dependency_reads,
74
+ bound: true, entries: Entries,
75
+ reason: "event tuple => true per accumulator and per open capture; set-only, in first-filing order"
76
+ )
77
+ private_constant :READS
18
78
 
19
79
  # ADR-84 WD2 — thread-local Array of active {Capture}s (nil when none). A capture observes-and-forwards:
20
80
  # every read recorded while it is active flows into the current consumer's accumulator exactly as before
@@ -27,14 +87,24 @@ module Rigor
27
87
  # each outer one's: the inner capture was pushed after the outer, and everything added since reached both.
28
88
  # {withhold} swaps in a fresh stack and restores this one untouched, so it keeps the property too.
29
89
  # {tee_read} and {tee_missing} rely on it to stop at the first capture that already holds an event.
30
- CAPTURE_KEY = :__rigor_dependency_capture_stack__
31
- private_constant :CAPTURE_KEY
90
+ #
91
+ # ADR-129 WD3 — the stack is an id-determined Resource: every change of it ({capture}'s push and pop,
92
+ # {withhold}'s swap and restore) takes a fresh state id, so reading it on the event paths is exempt.
93
+ CAPTURES = TypingState.resource(
94
+ :dependency_capture_stack,
95
+ read_exemption: :id_determined,
96
+ reason: "the open capture windows, pushed per window and swapped by withhold"
97
+ )
98
+ private_constant :CAPTURES
32
99
 
33
100
  # The read sets an {Accumulator} or {Capture} already holds in full, by identity: replayed into it, or
34
101
  # captured by a window it saw every event of ({capture}). Both only ever grow and a {ReadSet} is frozen,
35
102
  # so replaying such a set into the same target again adds nothing. Recording replays the same stored
36
103
  # read set many times over: on Rigor's own `lib`, 71% of the replays into a consumer repeated one
37
104
  # already made into it, carrying 65% of the replayed events.
105
+ #
106
+ # ADR-129 — the held marks are an extensional dedupe cache over the store's own {READS} entries (every event of
107
+ # a held set is already in the store), so marking and probing them log nothing; {ChainMemory} likewise.
38
108
  module ReplayMemory
39
109
  def holds?(read_set)
40
110
  !@held.nil? && @held.key?(read_set)
@@ -74,8 +144,8 @@ module Rigor
74
144
  @sources = Set.new
75
145
  @missing = Set.new
76
146
  # ADR-46 slice 4 — symbol-granularity tracking.
77
- # `symbol_sources`: source_path → Set<"ClassName#method"> for method-call deps.
78
- # `ancestry_sources`: Set<source_path> for class-ancestry (superclass / include) deps —
147
+ # `symbol_sources`: source_path → the "ClassName#method" names read from it, for method-call deps.
148
+ # `ancestry_sources`: the source paths read for class-ancestry (superclass / include) deps —
79
149
  # file-granularity by nature (a superclass edge touches the whole class).
80
150
  @symbol_sources = Hash.new { |h, k| h[k] = Set.new }
81
151
  @ancestry_sources = Set.new
@@ -90,6 +160,39 @@ module Rigor
90
160
  @suspensions += 1
91
161
  end
92
162
 
163
+ # The positive-edge aggregation shared by {DependencyRecorder.read_site}, {DependencyRecorder.replay} and a
164
+ # replayed {READS} write. `sources` is the union of the other two tables' paths, and this is the only writer of
165
+ # all three, so a read the finer table already holds is already in `sources` and costs one Set probe instead
166
+ # of two. Answers whether the read was new (truthy) or already held (nil), which is its {READS} filing.
167
+ def add_read(path, symbol)
168
+ added = symbol ? @symbol_sources[path].add?(symbol) : @ancestry_sources.add?(path)
169
+ @sources << path if added
170
+ added
171
+ end
172
+
173
+ # Undoes an {#add_read} that filed a new edge (G1's stage-4 form): `sources` keeps `path` while another table
174
+ # still holds it, and a `symbol_sources` set left empty goes, as no read had made it.
175
+ def remove_read(path, symbol)
176
+ if symbol
177
+ symbols = @symbol_sources.key?(path) ? @symbol_sources[path] : nil
178
+ if symbols
179
+ symbols.delete(symbol)
180
+ @symbol_sources.delete(path) if symbols.empty?
181
+ end
182
+ else
183
+ @ancestry_sources.delete(path)
184
+ end
185
+ @sources.delete(path) unless @ancestry_sources.include?(path) || @symbol_sources.key?(path)
186
+ end
187
+
188
+ # Whether the edge {#add_read} files for `path` and `symbol` is held. Reads `symbol_sources` without its
189
+ # default block, so asking adds no empty set.
190
+ def holds_read?(path, symbol)
191
+ return @ancestry_sources.include?(path) unless symbol
192
+
193
+ @symbol_sources.key?(path) && @symbol_sources[path].include?(symbol)
194
+ end
195
+
93
196
  def snapshot
94
197
  frozen_sym = @symbol_sources.transform_values(&:freeze).freeze
95
198
  Record.new(
@@ -103,8 +206,8 @@ module Rigor
103
206
  end
104
207
 
105
208
  # Frozen record of one file's cross-file reads.
106
- # `symbol_sources`: source_path → frozen Set<"ClassName#method"> (method-call edges).
107
- # `ancestry_sources`: frozen Set<source_path> (class-ancestry edges, file-granularity).
209
+ # `symbol_sources`: source_path → frozen set of "ClassName#method" names (method-call edges).
210
+ # `ancestry_sources`: frozen set of source paths (class-ancestry edges, file-granularity).
108
211
  Record = Data.define(:consumer, :sources, :missing, :symbol_sources, :ancestry_sources)
109
212
 
110
213
  # ADR-84 WD2 — a replayable, consumer-independent read-set: `reads` is a frozen Set of frozen
@@ -154,16 +257,22 @@ module Rigor
154
257
  # the frozen {Record}. Nests safely (the inner consumer's reads do not leak to the outer one); restores
155
258
  # the previous recorder on exit.
156
259
  def record_for(consumer)
157
- previous = Thread.current[KEY]
158
- previous&.suspend!
260
+ state = TypingState.current
261
+ previous = state[RECORDER]
262
+ if previous
263
+ state.touch(RECORDER)
264
+ previous.suspend!
265
+ end
159
266
  accumulator = Accumulator.new(consumer.to_s)
160
- Thread.current[KEY] = accumulator
161
- @mutex.synchronize { @active_count += 1 }
162
- yield
163
- accumulator.snapshot
164
- ensure
165
- Thread.current[KEY] = previous
166
- @mutex.synchronize { @active_count -= 1 }
267
+ state.with(RECORDER, accumulator) do
268
+ @mutex.synchronize { @active_count += 1 }
269
+ begin
270
+ yield
271
+ accumulator.snapshot
272
+ ensure
273
+ @mutex.synchronize { @active_count -= 1 }
274
+ end
275
+ end
167
276
  end
168
277
 
169
278
  # Plain integer read (GVL-atomic) — no `Thread.current` lookup on the disabled fast path.
@@ -175,25 +284,26 @@ module Rigor
175
284
  # window. {active?} answers for the process and is the cheap gate; this is the exact one, for a caller
176
285
  # that must know whether an empty {capture} means "nothing to record" or "nothing was watching".
177
286
  def recording?
178
- !Thread.current[KEY].nil?
287
+ !TypingState.current[RECORDER].nil?
179
288
  end
180
289
 
181
290
  # ADR-84 WD2 — runs the block under an observe-and-forward capture window and returns
182
291
  # `[block_result, read_set]`. Reads recorded during the window reach the current consumer's accumulator
183
292
  # unchanged (forwarding) and are additionally collected into the returned frozen {ReadSet} (observing),
184
- # which {replay} can later apply to a different consumer. Nests (see CAPTURE_KEY). Callers guard on
293
+ # which {replay} can later apply to a different consumer. Nests (see CAPTURES). Callers guard on
185
294
  # {active?}; opening a capture with no accumulator active would collect reads no run is recording.
186
295
  def capture
187
- captures = (Thread.current[CAPTURE_KEY] ||= [])
188
- accumulator = Thread.current[KEY]
296
+ state = TypingState.current
297
+ accumulator = state[RECORDER]
189
298
  suspensions = accumulator&.suspensions
190
299
  capture = Capture.new
191
- captures.push(capture)
300
+ state.push(CAPTURES, capture)
192
301
  begin
193
302
  result = yield
194
303
  ensure
195
- captures.pop
304
+ state.pop(CAPTURES)
196
305
  end
306
+ captures = state[CAPTURES]
197
307
  read_set = capture.snapshot
198
308
  note_captured(read_set, captures, accumulator, suspensions)
199
309
  [result, read_set]
@@ -209,7 +319,7 @@ module Rigor
209
319
  return if read_set.equal?(EMPTY_READ_SET)
210
320
 
211
321
  captures.last&.hold!(read_set)
212
- return unless accumulator && Thread.current[KEY].equal?(accumulator)
322
+ return unless accumulator && TypingState.current[RECORDER].equal?(accumulator)
213
323
  return unless accumulator.suspensions == suspensions
214
324
 
215
325
  accumulator.hold!(read_set)
@@ -223,18 +333,22 @@ module Rigor
223
333
  # edge would make every correct call on a project class re-check on any edit to the class's files.
224
334
  # Returns a nil read set when no recording is active.
225
335
  def withhold
226
- previous = Thread.current[KEY]
336
+ state = TypingState.current
337
+ previous = state[RECORDER]
227
338
  return [yield, nil] if previous.nil?
228
339
 
229
- previous_captures = Thread.current[CAPTURE_KEY]
340
+ previous_captures = state[CAPTURES]
230
341
  capture = Capture.new
231
- Thread.current[KEY] = Accumulator.new(previous.consumer)
232
- Thread.current[CAPTURE_KEY] = [capture]
233
- begin
234
- result = yield
235
- ensure
236
- Thread.current[KEY] = previous
237
- Thread.current[CAPTURE_KEY] = previous_captures
342
+ result = state.with(RECORDER, Accumulator.new(previous.consumer)) do
343
+ # A swap of the whole stack: a write, then a rewrite, so the swapped-in stack is read under a fresh id.
344
+ state.write(CAPTURES, [capture])
345
+ state.rewrite(CAPTURES)
346
+ begin
347
+ yield
348
+ ensure
349
+ state.write(CAPTURES, previous_captures)
350
+ state.rewrite(CAPTURES)
351
+ end
238
352
  end
239
353
  [result, capture.snapshot]
240
354
  end
@@ -243,11 +357,12 @@ module Rigor
243
357
  # here, applying the same per-consumer self-read filter as {read_site}, and tees into every active
244
358
  # capture so an enclosing capture window stays transitive when a memo hit substitutes for a body walk.
245
359
  def replay(read_set)
246
- accumulator = Thread.current[KEY]
360
+ state = TypingState.current
361
+ accumulator = state[RECORDER]
247
362
  return if accumulator.nil? || read_set.nil? || read_set.equal?(EMPTY_READ_SET)
248
363
 
249
- captures = Thread.current[CAPTURE_KEY]
250
- tee_read_set(captures, read_set) if captures && !captures.empty?
364
+ captures = state[CAPTURES]
365
+ tee_read_set(state, captures, read_set) if captures && !captures.empty?
251
366
  # Each set keeps its own insertion order whichever target is filled first, and a skipped replay is one
252
367
  # that could only have re-added what the target already holds.
253
368
  return if accumulator.holds?(read_set)
@@ -255,25 +370,29 @@ module Rigor
255
370
  consumer = accumulator.consumer
256
371
  read_set.reads.each do |pair|
257
372
  path, symbol = pair
258
- accumulate_read(accumulator, path, symbol) unless path == consumer
373
+ next if path == consumer
374
+
375
+ added = accumulate_read(accumulator, path, symbol)
376
+ log_filing(state, accumulator, pair, added) if state.capturing?
259
377
  end
260
378
  missing = accumulator.missing
261
- read_set.missing.each { |entry| missing << entry }
379
+ read_set.missing.each { |entry| file_missing(state, accumulator, missing, entry) }
262
380
  accumulator.hold!(read_set)
263
381
  end
264
382
 
265
383
  # #1590 — runs the block, which files a resolution chain's edges from entry `start` (nil: the root and the
266
384
  # whole chain) against `discovery`, unless the current consumer's accumulator AND the innermost open capture
267
385
  # already hold that filing; the block files the same edges every time, so skipping it there changes nothing.
268
- # Every event reaches every capture open at the time (see CAPTURE_KEY), so the innermost capture holding the
386
+ # Every event reaches every capture open at the time (see CAPTURES), so the innermost capture holding the
269
387
  # filing means every open capture does. A filing the accumulator holds but a capture opened since does not is
270
388
  # filed again, so that capture's read set, replayed elsewhere, still carries it; and {withhold} starts a fresh
271
389
  # accumulator and capture, which hold nothing. The marks are made only once the block has run, as {hold!}'s are.
272
390
  def file_chain_once(chain, start, discovery)
273
- accumulator = Thread.current[KEY]
391
+ state = TypingState.current
392
+ accumulator = state[RECORDER]
274
393
  return if accumulator.nil?
275
394
 
276
- captures = Thread.current[CAPTURE_KEY]
395
+ captures = state[CAPTURES]
277
396
  open = captures && !captures.empty?
278
397
  if accumulator.chain_filed?(chain, start, discovery) &&
279
398
  (!open || captures.last.chain_filed?(chain, start, discovery))
@@ -286,33 +405,78 @@ module Rigor
286
405
  end
287
406
 
288
407
  # {replay}'s share for the open captures. The innermost capture having seen `read_set` already means every
289
- # open capture holds all of it (see CAPTURE_KEY).
290
- def tee_read_set(captures, read_set)
408
+ # open capture holds all of it (see CAPTURES).
409
+ def tee_read_set(state, captures, read_set)
291
410
  innermost = captures.last
292
411
  return if innermost.holds?(read_set)
293
412
 
294
- read_set.reads.each { |pair| tee_read(captures, pair) }
295
- read_set.missing.each { |entry| tee_missing(captures, entry) }
413
+ read_set.reads.each { |pair| tee_read(state, captures, pair) }
414
+ read_set.missing.each { |entry| tee_missing(state, captures, entry) }
296
415
  innermost.hold!(read_set)
297
416
  end
298
417
  private_class_method :tee_read_set
299
418
 
300
419
  # Adds a read pair to the open captures, innermost first, and stops at the first capture that already holds
301
- # it: every capture outside that one holds it too (see CAPTURE_KEY). The capture sets end up exactly as
302
- # adding it to every capture would leave them, in the same insertion order.
303
- def tee_read(captures, pair)
304
- index = captures.size - 1
305
- index -= 1 while index >= 0 && captures[index].reads.add?(pair)
420
+ # it: every capture outside that one holds it too (see CAPTURES). The capture sets end up exactly as
421
+ # adding it to every capture would leave them, in the same insertion order. Each capture probed logs its
422
+ # filing; the ones outside the first holder are not probed, and hold the key by the same property.
423
+ def tee_read(state, captures, pair)
424
+ if state.capturing?
425
+ tee_logged(state, captures, pair, :reads)
426
+ else
427
+ index = captures.size - 1
428
+ index -= 1 while index >= 0 && captures[index].reads.add?(pair)
429
+ end
306
430
  end
307
431
  private_class_method :tee_read
308
432
 
309
433
  # {tee_read} for a name-keyed `missing` entry.
310
- def tee_missing(captures, entry)
311
- index = captures.size - 1
312
- index -= 1 while index >= 0 && captures[index].missing.add?(entry)
434
+ def tee_missing(state, captures, entry)
435
+ if state.capturing?
436
+ tee_logged(state, captures, entry, :missing)
437
+ else
438
+ index = captures.size - 1
439
+ index -= 1 while index >= 0 && captures[index].missing.add?(entry)
440
+ end
313
441
  end
314
442
  private_class_method :tee_missing
315
443
 
444
+ # {tee_read} / {tee_missing} while a capture is open: the same walk, logging each capture's filing.
445
+ def tee_logged(state, captures, key, set)
446
+ index = captures.size - 1
447
+ while index >= 0
448
+ capture = captures[index]
449
+ added = capture.public_send(set).add?(key)
450
+ log_filing(state, capture, key, added)
451
+ break unless added
452
+
453
+ index -= 1
454
+ end
455
+ end
456
+ private_class_method :tee_logged
457
+
458
+ # Logs one filing of `key` into `store` (an {Accumulator} or a {Capture}): a read of the key, which the store held
459
+ # unless the filing `added` it, and the write when it did. Callers log only while a capture is open.
460
+ def log_filing(state, store, key, added)
461
+ state.log_bound_read(READS, store, key, added ? TypingState::ABSENT : true)
462
+ state.log_bound_write(READS, store, key, true, TypingState::ABSENT) if added
463
+ end
464
+ private_class_method :log_filing
465
+
466
+ # A positive edge filed into the consumer's accumulator ({Accumulator#add_read}): truthy when it was new.
467
+ def accumulate_read(accumulator, path, symbol) = accumulator.add_read(path, symbol)
468
+ private_class_method :accumulate_read
469
+
470
+ # Files a name-keyed entry into an accumulator's `missing` set, logging the filing while a capture is open.
471
+ def file_missing(state, accumulator, missing, entry)
472
+ if state.capturing?
473
+ log_filing(state, accumulator, entry, missing.add?(entry))
474
+ else
475
+ missing << entry
476
+ end
477
+ end
478
+ private_class_method :file_missing
479
+
316
480
  # Records that the current consumer read a declaration / body whose definition site is `path_line` (a
317
481
  # `"path:line"` String, or nil). When `symbol` is given (a `"ClassName#method"` String), the read is a
318
482
  # method-call edge and is recorded at symbol granularity in `symbol_sources` in addition to the coarse
@@ -321,17 +485,20 @@ module Rigor
321
485
  # (ADR-84 WD2) observe every valid-path read UNfiltered — the self-read filter is per-consumer and is
322
486
  # re-applied by {replay} against whichever consumer a replay serves.
323
487
  def read_site(path_line, symbol = nil)
324
- accumulator = Thread.current[KEY]
488
+ state = TypingState.current
489
+ accumulator = state[RECORDER]
325
490
  return if accumulator.nil? || path_line.nil?
326
491
 
327
492
  path = site_path(path_line)
328
493
  return unless path
329
494
 
330
- captures = Thread.current[CAPTURE_KEY]
331
- tee_read(captures, [path, symbol].freeze) if captures && !captures.empty?
495
+ captures = state[CAPTURES]
496
+ pair = [path, symbol].freeze if (captures && !captures.empty?) || state.capturing?
497
+ tee_read(state, captures, pair) if captures && !captures.empty?
332
498
  return if path == accumulator.consumer
333
499
 
334
- accumulate_read(accumulator, path, symbol)
500
+ added = accumulate_read(accumulator, path, symbol)
501
+ log_filing(state, accumulator, pair, added) if state.capturing?
335
502
  end
336
503
 
337
504
  # The path part of a `"path:line"` site: what `path_line.split(":", 2).first` answers, without the Array
@@ -355,13 +522,14 @@ module Rigor
355
522
  # currently wins: a second declarer appearing in a file the reader has no edge to must drop the name to
356
523
  # `Dynamic` at that reader, and only a name-keyed edge can see it.
357
524
  def read_name(kind, name)
358
- accumulator = Thread.current[KEY]
525
+ state = TypingState.current
526
+ accumulator = state[RECORDER]
359
527
  return if accumulator.nil?
360
528
 
361
529
  entry = "#{kind}:#{name}"
362
- captures = Thread.current[CAPTURE_KEY]
363
- tee_missing(captures, entry) if captures && !captures.empty?
364
- accumulator.missing.add(entry)
530
+ captures = state[CAPTURES]
531
+ tee_missing(state, captures, entry) if captures && !captures.empty?
532
+ file_missing(state, accumulator, accumulator.missing, entry)
365
533
  end
366
534
 
367
535
  # {read_name} keyed on the LAST SEGMENT of a constant path, `name.delete_prefix("::").split("::").last`,
@@ -370,7 +538,7 @@ module Rigor
370
538
  # objects a call over half a million calls. A name the scan could read differently from `split` (one
371
539
  # that is not ASCII, or carries a run of colons or a trailing one) takes `split`'s answer.
372
540
  def read_last_segment(kind, name)
373
- return if Thread.current[KEY].nil?
541
+ return unless recording?
374
542
 
375
543
  segment = last_segment(name)
376
544
  read_name(kind, segment) if segment
@@ -394,15 +562,16 @@ module Rigor
394
562
  # {read_name} for prebuilt, frozen `"kind:name"` keys: a caller that files the same keys on every read
395
563
  # (a resolution chain's hook edges, #1641) builds them once and adds them here without a String apiece.
396
564
  def read_keys(keys)
397
- accumulator = Thread.current[KEY]
565
+ state = TypingState.current
566
+ accumulator = state[RECORDER]
398
567
  return if accumulator.nil?
399
568
 
400
- captures = Thread.current[CAPTURE_KEY]
569
+ captures = state[CAPTURES]
401
570
  open = captures && !captures.empty?
402
571
  missing = accumulator.missing
403
572
  keys.each do |entry|
404
- tee_missing(captures, entry) if open
405
- missing.add(entry)
573
+ tee_missing(state, captures, entry) if open
574
+ file_missing(state, accumulator, missing, entry)
406
575
  end
407
576
  end
408
577
 
@@ -412,7 +581,7 @@ module Rigor
412
581
  # the name private passes `"*"` as the class; the key fires when any entry of the name moves. A wildcard
413
582
  # slot (`[:singleton, :*]`) is `*` as the method name.
414
583
  def read_visibility(class_name, method_name)
415
- return if Thread.current[KEY].nil?
584
+ return unless recording?
416
585
 
417
586
  read_name(:visibility, "#{class_name}##{method_name}")
418
587
  end
@@ -423,18 +592,6 @@ module Rigor
423
592
  def read_missing(kind, name)
424
593
  read_name(kind, name)
425
594
  end
426
-
427
- # The positive-edge aggregation shared by {read_site} and {replay}. `sources` is the union of the other
428
- # two tables' paths, and this is the only writer of all three, so a read the finer table already holds is
429
- # already in `sources` and costs one Set probe instead of two.
430
- def accumulate_read(accumulator, path, symbol)
431
- if symbol
432
- accumulator.sources << path if accumulator.symbol_sources[path].add?(symbol)
433
- elsif accumulator.ancestry_sources.add?(path)
434
- accumulator.sources << path
435
- end
436
- end
437
- private_class_method :accumulate_read
438
595
  end
439
596
  end
440
597
  end
@@ -25,9 +25,9 @@ module Rigor
25
25
  @mutex = Mutex.new
26
26
  end
27
27
 
28
- # @return frozen snapshot of the recorded boundary-cross events. Each entry is a Data
29
- # with `class_name` (String), `method_name` (Symbol), `gem_name` (String), and `rbs_display` (String
30
- # — the authoritative-side type's human-facing form, embedded into the diagnostic message).
28
+ # @return frozen snapshot of the recorded boundary-cross events. Each entry is a Data with `class_name`,
29
+ # `method_name`, `gem_name`, and `rbs_display` (the authoritative-side type's human-facing form, embedded
30
+ # into the diagnostic message).
31
31
  def entries
32
32
  @mutex.synchronize { @entries.dup.freeze }
33
33
  end
@@ -50,6 +50,17 @@ module Rigor
50
50
  end
51
51
  end
52
52
 
53
+ # The pool drain ({Rigor::Analysis::WorkerSession#drain_reporters}): the frozen entries.
54
+ def snapshot
55
+ entries
56
+ end
57
+
58
+ # Replays one worker's {#snapshot}; the record-time dedup collapses every worker's copy. Nil records
59
+ # nothing.
60
+ def absorb(snapshot)
61
+ Array(snapshot).each { |entry| record(**entry.to_h) }
62
+ end
63
+
53
64
  private
54
65
 
55
66
  def same_key?(existing, entry)
@@ -44,7 +44,7 @@ module Rigor
44
44
  Resolved.new(
45
45
  gem_name: entry.gem,
46
46
  version: spec.version.to_s,
47
- gem_dir: spec.full_gem_path, # rigor:disable undefined-method
47
+ gem_dir: spec.full_gem_path,
48
48
  mode: entry.mode,
49
49
  roots: entry.roots
50
50
  )
@@ -15,10 +15,9 @@ module Rigor
15
15
  :class_to_gem, :budget_overrun_strategy, :gem_modes, :refinements, :bound_constants,
16
16
  :bound_segments
17
17
 
18
- # @param method_catalog — => Symbol}] the flat
19
- # `(class_name, method_name) → :instance | :singleton` table produced by {Walker.walk}, aggregated
20
- # across every resolved gem in the run. The Index itself stays gem-agnostic — the per-gem
21
- # attribution that slice 3's cache descriptor needs lives on `Resolved`, not here.
18
+ # @param method_catalog — the flat `(class_name, method_name) → CatalogEntry` table produced by
19
+ # {Walker.walk}, aggregated across every resolved gem in the run. The Index itself stays gem-agnostic —
20
+ # the per-gem attribution that slice 3's cache descriptor needs lives on `Resolved`, not here.
22
21
  # @param budget_exceeded — gem names whose {Walker} run hit the per-gem catalog cap
23
22
  # (slice 4). The Runner consumes this list to emit one `dynamic.dependency-source.budget-exceeded`
24
23
  # warning per gem.
@@ -110,10 +109,9 @@ module Rigor
110
109
  mode_for(class_name) == :full
111
110
  end
112
111
 
113
- # Looks up the recorded method kind for a `(class_name, method_name)` pair. Returns `:instance` /
114
- # `:singleton` when the walker observed a definition under one of the resolved gems' `roots:`, or
115
- # `nil` otherwise. Slice 2b-ii enriches this with the inferred return type so the dispatcher tier can
116
- # build a `Type::Dynamic` directly from the lookup result.
112
+ # Looks up the `CatalogEntry` (kind, and the heuristic return type) for a `(class_name, method_name)` pair.
113
+ # Returns it when the walker observed a definition under one of the resolved gems' `roots:`, or `nil`
114
+ # otherwise.
117
115
  def contribution_for(class_name:, method_name:)
118
116
  @method_catalog[[class_name, method_name]]
119
117
  end