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
@@ -27,10 +27,8 @@ module Rigor
27
27
  #
28
28
  # The invariant the verify harness (and the spec) assert: `#recheck`'s merged diagnostics are
29
29
  # byte-identical to a full `--no-cache` re-analysis of the edited tree, in the same order (#1524; the gate
30
- # itself compares sorted sets). This is the `--verify-incremental` acceptance gate, here without disk
31
- # persistence or CLI wiring (the cache is in-process). It models the body tier only: an edit that adds /
32
- # removes / moves a *file* is outside the analyzed set it maintains and falls to a fresh {#baseline} (the
33
- # structural tier is a later slice).
30
+ # itself compares sorted sets). The `--verify-incremental` gate runs this engine; `#run_incremental` adds the
31
+ # snapshot persistence that `--incremental` uses.
34
32
  # The class-length budget is relaxed: this is one cohesive orchestrator of the incremental state
35
33
  # (per-file diagnostics cache, the file-level / symbol-level / negative dependency graphs, and the ADR-85
36
34
  # seed bundles), clearer read together than split across micro-classes that would all share the same ivars.
@@ -101,8 +99,8 @@ module Rigor
101
99
  # ADR-85 WD2 — per-file discovery seed bundles keyed by logical path. A cold baseline builds them; a
102
100
  # warm recheck folds them (re-walking only changed files) and refreshes the set. Ride the snapshot.
103
101
  @seed_bundles = {}
104
- @cache = {} # analyzed path => [Diagnostic]
105
- @sources = {} # analyzed path => Set<source path it read from>
102
+ @cache = {} # analyzed path => its diagnostics
103
+ @sources = {} # analyzed path => the source paths it read from
106
104
  @digests = {} # analyzed path => ADR-87 packed stat-digest entry at last analysis
107
105
  # Issue #1583 — discovered-not-analysed path => the same packed entry, for a run over path arguments that
108
106
  # #684 widened to the configured `paths:`. Empty on every run over the configured paths.
@@ -110,25 +108,29 @@ module Rigor
110
108
  @analyzed = [] # the project files analyzed last round
111
109
  @dependents = {} # inverted @sources (file-level)
112
110
  # ADR-46 slice 4 — symbol-granularity tracking.
113
- @symbol_sources = {} # consumer => { source_path => Set<"ClassName#method"> }
114
- @ancestry_sources = {} # consumer => Set<source_path> (class-ancestry deps)
111
+ @symbol_sources = {} # consumer => { source_path => the "ClassName#method" symbols it read }
112
+ @ancestry_sources = {} # consumer => source paths (class-ancestry deps)
115
113
  @symbol_fingerprints = {} # path => { "ClassName#method" => sha256_hex }
116
- @symbol_dependents = {} # [source, symbol] => Set<consumer>
117
- @ancestry_dependents = {} # source => Set<consumer> (inverted ancestry_sources)
114
+ @symbol_dependents = {} # [source, symbol] => consumers
115
+ @ancestry_dependents = {} # source => consumers (inverted ancestry_sources)
118
116
  # ADR-46 slice 3 — negative (missing) dependencies: a consumer that
119
117
  # looked up a name and resolved nothing must be re-checked when that
120
118
  # name later appears (e.g. a `call.unresolved-toplevel` whose target
121
119
  # is defined by a later edit).
122
- @missing = {} # consumer => Set<"kind:name"> it looked up and missed
123
- @negative_dependents = {} # "kind:name" => Set<consumer> (inverted @missing)
124
- @class_decls = {} # path => Set<qualified class name declared in the file>
120
+ @missing = {} # consumer => the "kind:name" keys it looked up and missed
121
+ @negative_dependents = {} # "kind:name" => consumers (inverted @missing)
122
+ @class_decls = {} # path => qualified class names declared in the file
125
123
  # Issue #644 — the value-constant twin: path => { qualified constant name => publication descriptor }.
126
124
  # Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
127
125
  # the `constant:` edges a reader records at every reference, resolved or not.
128
126
  @constant_decls = {}
129
- # ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects: }.
127
+ # ADR-89 WD2 — per-def observed-key return summaries: [path, symbol] => { keys:, returns:, effects:,
128
+ # guard_constants: }.
130
129
  # Harvested from the ADR-84 return memo after each run; drives the behavioural-stability gate.
131
130
  @return_summaries = {}
131
+ # Issue #3180 — what the ADR-89 WD2 gate's probe read for the pairs it passed, from the closure decision
132
+ # until {#absorb} folds it into the skipped readers; nil when no pair passed.
133
+ @gate_observations = nil
132
134
  # ADR-67 WD6c lift — the `parameter_inference:` seed table the cached diagnostics were computed
133
135
  # under (`{}` when the gate is off — the gate state is constant across a snapshot's lifetime because
134
136
  # the configuration is part of the global fingerprint). A recheck recomputes the table fresh and
@@ -265,7 +267,7 @@ module Rigor
265
267
  # digest (issue #1536).
266
268
  @seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: true)
267
269
  absorb_dependency_graph(runner)
268
- @return_summaries = runner.return_summaries # ADR-89 WD2 — the full-run behavioural surface.
270
+ @return_summaries = baseline_return_summaries(runner)
269
271
  # ADR-67 WD6c lift — the seed table the runner's own pre-pass computed ({} when the gate is off).
270
272
  # Reading it back, rather than computing it here, keeps the baseline single-collect.
271
273
  @param_table = runner.param_inferred_types
@@ -393,6 +395,8 @@ module Rigor
393
395
  # whole project — what a `sig/` edit already costs through the snapshot fingerprint.
394
396
  def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
395
397
  @stale_plugin_readers = plugin_read_affected | disclosure_refreshers(removed)
398
+ # Issue #3180 — a recheck that passed the gate and then re-baselined absorbed nothing: start without it.
399
+ @gate_observations = nil
396
400
  return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
397
401
 
398
402
  (gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
@@ -579,8 +583,10 @@ module Rigor
579
583
  # surfaces are its return type and its content-mutation effects (no instance/class variable write, no
580
584
  # `yield`, no implicit-self call whose transitive effect a caller could observe). For a candidate the
581
585
  # session compares its content-mutation effects (pure AST) then re-evaluates its return at every
582
- # persisted key (a runner probe); a pair passes only when BOTH are unchanged. Any missing summary,
583
- # ineligible def, changed effect, changed return, or uncomputable key keeps the pair — the conservative
586
+ # persisted key (a runner probe), with the published-constant spellings its body reads (issue #3186,
587
+ # {#guard_constants_unmoved?}); a pair passes only when all three are unchanged. Any missing summary (which is
588
+ # also how a def whose keys could not all be recorded is stored, issues #3116 and #3160), ineligible def,
589
+ # changed effect, changed return, or uncomputable key keeps the pair — the conservative
584
590
  # direction the `--verify-incremental` and byte-identical recheck specs backstop.
585
591
  def behaviourally_unstable_pairs(changed_pairs, unstable, scan_index)
586
592
  return changed_pairs if @return_summaries.empty? || scan_index.nil?
@@ -633,9 +639,21 @@ module Rigor
633
639
  runner = build_runner
634
640
  files = @paths ? runner.analysis_file_set(@paths) : runner.analysis_file_set
635
641
  rebind_buffer(files)
636
- files
642
+ admit_new_buffer(files)
637
643
  end
638
644
 
645
+ # Issue #2233 — a buffer for a file that is not on disk (a new file) is in no expansion, so the set above never
646
+ # names it: the recheck neither analyses it nor sees its declarations, and the run answers "re-analysed 0 file(s)"
647
+ # with no diagnostics for the buffer. Option A (single-file scope) analyses the logical path whether or not it
648
+ # exists; option B joins it to the set the same way, as an added file, so the closure analyses it like any new
649
+ # file. A buffer already matched by {#rebind_buffer} is in `files` and is returned as it is.
650
+ def admit_new_buffer(files)
651
+ return files if @buffer.nil? || files.include?(@buffer.logical_path)
652
+
653
+ files + [@buffer.logical_path]
654
+ end
655
+ private :admit_new_buffer
656
+
639
657
  # Verification engine (the `--verify-incremental` gate): with NO source edit, re-analyze `subset` fresh
640
658
  # and serve every other analyzed file from the baseline cache. Because nothing on disk changed, the
641
659
  # merged result MUST equal a full analysis — so this exercises the subset-analysis and cache-merge
@@ -1226,10 +1244,79 @@ module Rigor
1226
1244
  # with anything its closure demanded for itself, so this is the same union the next run replays.
1227
1245
  @run_level_rows = runner.run_level_rows
1228
1246
  @synthetic_surface_digest = synthetic_surface_digest_of(runner)
1229
- refresh_return_summaries(runner, analyze_set)
1247
+ refresh_gated_state(runner, analyze_set)
1230
1248
  refresh_effect_collections(runner, analyze_set)
1231
1249
  end
1232
1250
 
1251
+ # The summaries, then the skipped readers' edges, in that order ({#refresh_return_summaries}), with what the
1252
+ # gate's probe observed for this recheck taken and cleared. The clear is a guard no spec pins:
1253
+ # {#affected_closure} resets the slot before every {#absorb}, so nothing reads it again. It only keeps a
1254
+ # consumed observation from outliving its recheck, should absorb gain another caller.
1255
+ def refresh_gated_state(runner, analyze_set)
1256
+ observations = @gate_observations
1257
+ @gate_observations = nil
1258
+ refresh_return_summaries(runner, analyze_set, observations)
1259
+ absorb_gate_reads(observations, analyze_set)
1260
+ end
1261
+
1262
+ # Issue #3180 — give every reader the ADR-89 WD2 gate skipped the edges its probe recorded for the pair it
1263
+ # passed ({#gate_observations}): the reader reaches those defs, classes and names through the edited def now,
1264
+ # and no analysis of the reader will record that until something else re-analyses it. The probe evaluated the
1265
+ # def at all of its stored keys and a reader observes it under some of them, so the reader may gain an edge it
1266
+ # does not need; that costs a re-analysis, never an answer. Runs after {#refresh_return_summaries}, which reads
1267
+ # the dependents as they stood before these edges. Independent of the summaries: a pooled recheck over
1268
+ # summaries a sequential run stored can pass the gate too, and its skipped readers need the edges as much.
1269
+ def absorb_gate_reads(observations, analyze_set)
1270
+ return if observations.nil?
1271
+
1272
+ analyzed = analyze_set.to_set
1273
+ skipped = observations.reads.keys.to_h do |pair|
1274
+ [pair, ((@symbol_dependents[pair] || Set.new) - analyzed).to_a]
1275
+ end
1276
+ return if skipped.each_value.all?(&:empty?)
1277
+
1278
+ credit_probe_plugin_rows(observations, skipped)
1279
+ skipped.each { |pair, readers| readers.each { |reader| add_recorded_reads(reader, observations.reads[pair]) } }
1280
+ invert_dependency_graph
1281
+ end
1282
+
1283
+ # Issue #3187 — the plugin reads the probe made while evaluating a passed pair's def, credited to each reader of
1284
+ # the pair it skipped, as an analysis of that reader would have credited them: a later edit of what the plugin
1285
+ # read re-analyses the reader ({#plugin_read_affected}). Like the edges, a reader may gain a row it does not
1286
+ # need, which costs a re-analysis. Run-scoped registrations are not credited: a pair whose probe registered one
1287
+ # does not pass ({#readers_hold_disclosures?}).
1288
+ def credit_probe_plugin_rows(observations, skipped)
1289
+ skipped.each do |pair, readers|
1290
+ rows = observations.plugin_reads[pair]
1291
+ next if rows.nil?
1292
+
1293
+ readers.each { |reader| @plugin_reads[reader] = ((@plugin_reads[reader] || []) | rows).freeze }
1294
+ end
1295
+ end
1296
+
1297
+ # `record`'s reads added to `reader`'s, less the reads of `reader`'s own file (a file records no edge to
1298
+ # itself). Builds new sets: a restored snapshot's are not this session's to mutate. The self-filter on the
1299
+ # ancestry edges is a guard no spec pins: it mirrors the two that are pinned, and no row was built in which
1300
+ # the edited def comes to read the ancestry of a class its skipped reader's file declares.
1301
+ def add_recorded_reads(reader, record)
1302
+ symbols = (@symbol_sources[reader] || {}).dup
1303
+ record.symbol_sources.each do |path, names|
1304
+ symbols[path] = (symbols[path] || Set.new) | names unless path == reader
1305
+ end
1306
+ @symbol_sources[reader] = symbols
1307
+ @ancestry_sources[reader] = (@ancestry_sources[reader] || Set.new) | (record.ancestry_sources - [reader])
1308
+ @sources[reader] = (@sources[reader] || Set.new) | (record.sources - [reader])
1309
+ @missing[reader] = (@missing[reader] || Set.new) | record.missing
1310
+ end
1311
+
1312
+ # The inverted indexes of the four per-consumer edge tables.
1313
+ def invert_dependency_graph
1314
+ @dependents = Incremental.invert(@sources)
1315
+ @symbol_dependents = Incremental.invert_symbols(@symbol_sources)
1316
+ @ancestry_dependents = Incremental.invert(@ancestry_sources)
1317
+ @negative_dependents = Incremental.invert(@missing)
1318
+ end
1319
+
1233
1320
  # ADR-103 WD13 / #382 — the effects analogue of {#refresh_return_summaries}, and the same soundness
1234
1321
  # order: drop every re-analyzed file's collection first, then fold this run's harvest in. A file the
1235
1322
  # recheck did NOT analyze keeps the collection the snapshot carried, which is exactly the reuse the
@@ -1245,21 +1332,97 @@ module Rigor
1245
1332
  @effect_plugin_digest = effect_plugin_digest_of(runner)
1246
1333
  end
1247
1334
 
1248
- # ADR-89 WD2 — replace the behavioural summaries of every re-analyzed file with THIS run's harvest: drop
1249
- # the analyzed files' old summaries first, then fold the fresh ones. Dropping first is the soundness
1250
- # invariant — a summary for a file is valid only if it came from a run that analyzed that file. On the
1251
- # sequential path (the default + `--verify-incremental`) the harvest is complete, so a re-analyzed def's
1252
- # summary is simply refreshed. On a pooled recheck the callee bodies are evaluated in worker processes,
1253
- # so the main-process memo harvest is sparse; dropping first means a re-analyzed file with no fresh
1254
- # summary loses its stale one (WD2 then conservatively re-checks its dependents) rather than comparing a
1255
- # later edit against a pre-edit summary. Files NOT re-analyzed keep their (unchanged, valid) summaries.
1256
- # A def whose symbol dependents WD2 skipped is observed under fewer keys, so its refreshed summary may
1257
- # shrink — sound (fewer keys = more conservative) and self-correcting (an empty summary stops the gate
1258
- # firing, so the callers re-analyse and repopulate).
1259
- def refresh_return_summaries(runner, analyze_set)
1335
+ # ADR-89 WD2 / issues #3160, #3180 — bring the behavioural summaries up to date with THIS run. The invariant
1336
+ # the gate reads them under: a pair's summary holds every key under which a CURRENT symbol dependent of the
1337
+ # def observes it, directly or through other defs, each with the def's current return; and every file that
1338
+ # observes the def is one of its symbol dependents. A summary that cannot be shown to is deleted, and a pair
1339
+ # with no summary keeps its dependents, so every doubt below resolves to "no summary".
1340
+ #
1341
+ # A run's harvest ({Runner#return_summaries}) is what the files it analysed observed. The keys the gate's
1342
+ # probe observed for the pairs it passed (`observations`, {#gate_observations}) count as this run's too: they
1343
+ # are what the readers the gate skipped observe through the edited def. So per pair:
1344
+ #
1345
+ # - the run cannot list the keys it observed (a pool run, a def past the per-def or total cap, a call the
1346
+ # memo refused to store): delete;
1347
+ # - every dependent was analysed: the harvest is the whole key set, and replaces the summary (deleting it
1348
+ # when no analysed file observed the def, which is where a def with no dependent left ends up);
1349
+ # - otherwise the summary keeps the keys of the dependents that were not analysed, and gains this run's
1350
+ # ({#merged_return_summary}), which are none when no dependent was analysed. The kept returns are current:
1351
+ # a dependent stays out of the closure of its callee's edit only when the gate proved the returns unmoved
1352
+ # at every kept key, or when the def's own text did not change. With no summary to keep, the other
1353
+ # dependents' keys are unknown.
1354
+ #
1355
+ # Fewer keys are never the conservative direction: each key protects the readers that observe the def under
1356
+ # it, so a summary missing one skips a reader whose answer the edit moved.
1357
+ #
1358
+ # "Every dependent" is read off the edges as they stood BEFORE {#absorb_gate_reads} adds the probe's. A
1359
+ # skipped reader that gains its first edge to a def there observes it only through the pair the gate passed,
1360
+ # so the probe's keys are all of its keys; one that already held an edge was not analysed, and the pair keeps
1361
+ # its stored keys.
1362
+ def refresh_return_summaries(runner, analyze_set, observations = nil)
1363
+ harvest = harvest_with_gate_summaries(runner.return_summaries, observations&.summaries)
1260
1364
  analyzed = analyze_set.to_set
1261
- @return_summaries.reject! { |(path, _symbol), _| analyzed.include?(path) }
1262
- @return_summaries.merge!(runner.return_summaries)
1365
+ (@return_summaries.keys | (harvest || {}).keys).each do |pair|
1366
+ dependents = @symbol_dependents[pair] || Set.new
1367
+ summary = refreshed_return_summary(pair, harvest, dependents.subset?(analyzed))
1368
+ summary.nil? ? @return_summaries.delete(pair) : @return_summaries[pair] = summary
1369
+ end
1370
+ @return_summaries = capped_return_summaries(@return_summaries)
1371
+ end
1372
+
1373
+ # Issue #3180 — `harvest` (this run's) with the keys the gate's probe observed (`probed`, see
1374
+ # {#gate_observations}) counted as observed by this run: they are what the readers the gate skipped observe
1375
+ # through the def it passed, each with the callee's current return. A def either side cannot list the keys
1376
+ # of has none, and so has one whose keys together pass the per-def cap. A run with no harvest stays without.
1377
+ def harvest_with_gate_summaries(harvest, probed)
1378
+ return harvest if harvest.nil? || probed.nil? || probed.empty?
1379
+
1380
+ probed.each_with_object(harvest.dup) do |(pair, summary), merged|
1381
+ mine = merged.fetch(pair, summary)
1382
+ merged[pair] = mine.nil? || summary.nil? ? nil : merged_return_summary(mine, summary)
1383
+ end
1384
+ end
1385
+
1386
+ # ADR-89 WD2 — the full-run behavioural surface: every file was analysed, so the harvest is every reader's.
1387
+ # A def the run could not list the keys of (nil in the harvest) and a pool run (no harvest) record nothing.
1388
+ def baseline_return_summaries(runner)
1389
+ capped_return_summaries((runner.return_summaries || {}).compact)
1390
+ end
1391
+
1392
+ # `pair`'s summary after a run whose harvest is `harvest` (nil for a run that has none), or nil for none.
1393
+ # `complete` says every dependent of the def was analysed by that run.
1394
+ def refreshed_return_summary(pair, harvest, complete)
1395
+ return nil if harvest.nil?
1396
+
1397
+ fresh = harvest[pair]
1398
+ return nil if fresh.nil? && harvest.key?(pair)
1399
+ return fresh if complete
1400
+
1401
+ merged_return_summary(@return_summaries[pair], fresh)
1402
+ end
1403
+
1404
+ # Issue #3160 — `kept` (the summary of a def some of whose dependents this run did not analyse) with the
1405
+ # keys `fresh` (this run's harvest for the def, or nil) adds. A kept key stays unless `fresh` holds the same
1406
+ # key with the same return; one whose return differs stays beside it, which no re-evaluation can satisfy, so
1407
+ # the gate keeps the def's dependents. nil when there is nothing to keep or the keys pass the per-def cap.
1408
+ def merged_return_summary(kept, fresh)
1409
+ return nil if kept.nil?
1410
+ return kept if fresh.nil?
1411
+
1412
+ rows = fresh[:keys].zip(fresh[:returns])
1413
+ rows += kept[:keys].zip(kept[:returns]).reject { |row| rows.include?(row) }
1414
+ return nil if rows.size > Runner::RETURN_SUMMARY_KEYS_PER_DEF
1415
+
1416
+ { keys: rows.map(&:first), returns: rows.map(&:last), effects: fresh[:effects],
1417
+ guard_constants: fresh[:guard_constants] }
1418
+ end
1419
+
1420
+ # The first {Runner::RETURN_SUMMARY_TOTAL_CAP} of `summaries`, so the snapshot stays small however many runs
1421
+ # fed it. A def past the cap has no summary, which keeps its dependents.
1422
+ def capped_return_summaries(summaries)
1423
+ return summaries if summaries.size <= Runner::RETURN_SUMMARY_TOTAL_CAP
1424
+
1425
+ summaries.first(Runner::RETURN_SUMMARY_TOTAL_CAP).to_h
1263
1426
  end
1264
1427
 
1265
1428
  # Evict a removed file from every per-file map so its stale diagnostics are never served and it drops
@@ -1292,10 +1455,7 @@ module Rigor
1292
1455
  @ancestry_sources[path] = record.ancestry_sources.dup
1293
1456
  @missing[path] = record.missing.dup
1294
1457
  end
1295
- @dependents = Incremental.invert(@sources)
1296
- @symbol_dependents = Incremental.invert_symbols(@symbol_sources)
1297
- @ancestry_dependents = Incremental.invert(@ancestry_sources)
1298
- @negative_dependents = Incremental.invert(@missing)
1458
+ invert_dependency_graph
1299
1459
  @symbol_fingerprints.merge!(runner.symbol_fingerprints)
1300
1460
  # Wholesale replace (the subset runner's pre-pass is complete): a file that lost its last class must
1301
1461
  # drop out of the map so a later re-add registers as an appearance.
@@ -1384,6 +1544,11 @@ module Rigor
1384
1544
  # (`ResolutionChain#record_hooks`), so a new file that taints `Comparable` with `include(*MODS)` re-checks the
1385
1545
  # readers of a chain through it. It is not `mark:`, which a plain reopening of a module moves (the bucket's
1386
1546
  # module mark) and which would re-check every reader of `Kernel` on any `module Kernel` reopening.
1547
+ #
1548
+ # Issue #3005 — a moved {Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK} fires the owner's `mixin:` keys too: a new
1549
+ # file whose body-level `send(:then)` marks an external `Comparable` makes a reader of a chain through it decline,
1550
+ # and that reader filed `mixin:Comparable`, not `mark:Comparable`. The mark moves only with such a call, not on a
1551
+ # plain reopening.
1387
1552
  def mark_affected(paths, summary)
1388
1553
  def_index = summary && summary[:def_index]
1389
1554
  before = paths.map { |path| @seed_bundles.dig(path, :parameter_envelopes) }
@@ -1391,16 +1556,22 @@ module Rigor
1391
1556
  tainted = Incremental.changed_unpositioned_owners(
1392
1557
  paths.map { |path| @seed_bundles.dig(path, :unpositioned_mixins) },
1393
1558
  [def_index && def_index[:unpositioned_mixins]]
1394
- )
1559
+ ) | dynamic_owners(moved)
1395
1560
  return Set.new if moved.empty? && tainted.empty?
1396
1561
 
1397
1562
  keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
1398
1563
  keys.concat(mixin_keys(tainted))
1399
1564
  extended = Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK
1400
- keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended }
1565
+ meta = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
1566
+ keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended || mark == meta }
1401
1567
  Incremental.negative_closure(keys, @negative_dependents)
1402
1568
  end
1403
1569
 
1570
+ def dynamic_owners(moved)
1571
+ dynamic = Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK
1572
+ moved.filter_map { |(class_name, mark)| class_name if mark == dynamic }
1573
+ end
1574
+
1404
1575
  # Issue #1661 — the `mixin:` keys an owner of a moved unpositioned edge fires. The table keys a receiver-form
1405
1576
  # owner by the spelling written (`class File; Constants.include(*MODS); end` is `"Constants"`), not the constant
1406
1577
  # Ruby resolves it to, while the chain files the qualified candidate (`mixin:File::Constants`). The table does
@@ -1505,7 +1676,7 @@ module Rigor
1505
1676
  end
1506
1677
 
1507
1678
  # The qualified class/module names declared in the pre-parsed `index` (shared with
1508
- # {#symbol_fingerprints_from_index}, so the changed set is parsed once). `{ path => Set<class name> }`.
1679
+ # {#symbol_fingerprints_from_index}, so the changed set is parsed once), keyed by path.
1509
1680
  def class_declarations_from_index(index)
1510
1681
  return {} if index.nil?
1511
1682
 
@@ -1545,7 +1716,9 @@ module Rigor
1545
1716
 
1546
1717
  # ADR-89 WD2 — true when the ONLY cross-file body surfaces of `node` a symbol dependent can consume are
1547
1718
  # its return type and its content-mutation effects — so comparing exactly those two is a COMPLETE
1548
- # behavioural check. A def qualifies when its body writes no instance / class variable (an ivar
1719
+ # behavioural check. (Issue #3186: the truthiness rule's one-hop body scan reads a third, the published
1720
+ # constants the body names, which the probe compares too; no node kind makes a def ineligible for it.)
1721
+ # A def qualifies when its body writes no instance / class variable (an ivar
1549
1722
  # definite-assignment a same-class caller consumes), does not `yield` (a block-parameter surface a
1550
1723
  # caller passing a block consumes), and makes no implicit-self call (which could carry a transitive
1551
1724
  # shared-state write). Deliberately conservative + purely syntactic: a def with a self-call or a
@@ -1597,29 +1770,104 @@ module Rigor
1597
1770
  end
1598
1771
 
1599
1772
  def content_effects_for(node)
1600
- (@effects_evaluator ||= Inference::StatementEvaluator.new(scope: Scope.empty))
1601
- .content_mutated_parameter_positions(node)
1773
+ effects_evaluator.content_mutated_parameter_positions(node)
1602
1774
  rescue StandardError
1603
1775
  nil
1604
1776
  end
1605
1777
 
1778
+ # The evaluator {#content_effects_for} asks, kept for as long as the calling thread's {TypingState} is the one it
1779
+ # was built under. An evaluator caches that state, and the owner evaluators it builds cache theirs, so one kept
1780
+ # across the language server's save rounds, each on a thread of its own, would keep writing its run-scoped memos
1781
+ # into a finished round's state, which that round's release has already emptied (ADR-129).
1782
+ def effects_evaluator
1783
+ state = TypingState.current
1784
+ unless @effects_state.equal?(state)
1785
+ @effects_state = state
1786
+ @effects_evaluator = Inference::StatementEvaluator.new(scope: Scope.empty)
1787
+ end
1788
+ @effects_evaluator
1789
+ end
1790
+
1606
1791
  # ADR-89 WD2 — the subset of `pairs` whose (edited) def re-evaluates to the SAME return descriptor at
1607
1792
  # every persisted key. Runs one runner probe (`evaluate_return_types` — builds the discovery + env once,
1608
1793
  # no file analysis) over all candidate defs, then keeps a pair only when every key's re-evaluated
1609
1794
  # descriptor is present (not a memo refusal) and equal to the persisted one. A probe failure yields an
1610
1795
  # empty set (keep everything).
1611
1796
  def return_stable_pairs(pairs, scan_index)
1612
- specs = pairs.filter_map { |pair| return_spec_for(pair, scan_index) }
1797
+ specs = pairs.reject { |pair| readers_hold_disclosures?(pair) }.filter_map do |pair|
1798
+ return_spec_for(pair, scan_index)
1799
+ end
1613
1800
  return Set.new if specs.empty?
1614
1801
 
1615
- results = probe_return_types(specs)
1616
- specs.each_with_object(Set.new) do |spec, stable|
1617
- descriptors = results[[spec[:class_name], spec[:method_name], spec[:singleton]]]
1618
- expected = @return_summaries[spec[:pair]][:returns]
1619
- next if descriptors.nil? || descriptors.length != expected.length
1620
- next unless descriptors.each_with_index.all? { |d, i| !d.nil? && d == expected[i] }
1802
+ runner, results = probe_return_types(specs)
1803
+ passed = specs.each_with_object({}) do |spec, pairs|
1804
+ triple = [spec[:class_name], spec[:method_name], spec[:singleton]]
1805
+ next unless returns_unmoved?(spec[:pair], results[triple])
1806
+ next unless guard_constants_unmoved?(spec[:pair], runner.probe_guard_constants[triple])
1807
+ # Issue #3187 — a run-scoped row the probe registered keeps the readers ({#readers_hold_disclosures?}).
1808
+ next if runner.probe_disclosures.key?(triple)
1621
1809
 
1622
- stable << spec[:pair]
1810
+ pairs[spec[:pair]] = triple
1811
+ end
1812
+ @gate_observations = gate_observations(runner, passed) unless passed.empty?
1813
+ passed.keys.to_set
1814
+ end
1815
+
1816
+ # Issue #3187 — whether a symbol dependent of `pair` holds a run-scoped plugin row ({RunDisclosureLedger}), which
1817
+ # keeps the pair's readers, as a probe that registers one does. A skipped reader replays the rows its last
1818
+ # analysis registered, and which of them came through the edited def, under which key, is not recorded; nor
1819
+ # can rows the probe registers be credited to a reader without that key, or ever be retracted once its def
1820
+ # stops registering them. So a reader holding any row is re-analysed, which settles its rows afresh.
1821
+ def readers_hold_disclosures?(pair)
1822
+ (@symbol_dependents[pair] || Set.new).any? { |reader| @plugin_disclosures.files.key?(reader) }
1823
+ end
1824
+
1825
+ # Issue #3186 — whether the probe's `current` constant spellings ({Runner#guard_constants}) are `pair`'s stored
1826
+ # ones. A reader's `flow.always-truthy-condition` withholds when a def it calls reads a constant another file
1827
+ # published, whatever the def returns, so a return-preserving edit that adds or drops such a read moves the
1828
+ # reader's diagnostics. nil on either side (a list the runner could not make, a summary stored before the list
1829
+ # existed) reads as moved.
1830
+ def guard_constants_unmoved?(pair, current)
1831
+ stored = @return_summaries[pair][:guard_constants]
1832
+ !current.nil? && !stored.nil? && current == stored
1833
+ end
1834
+
1835
+ # Whether the probe's `descriptors` are `pair`'s stored returns, key for key, none of them a memo refusal.
1836
+ def returns_unmoved?(pair, descriptors)
1837
+ expected = @return_summaries[pair][:returns]
1838
+ return false if descriptors.nil? || descriptors.length != expected.length
1839
+
1840
+ descriptors.each_with_index.all? { |descriptor, index| !descriptor.nil? && descriptor == expected[index] }
1841
+ end
1842
+
1843
+ # Issue #3180 — what the gate's probe observed for the pairs it passed. A reader the gate skips is not
1844
+ # re-analysed, yet it now reaches whatever the edited def reaches: a callee the def did not call before, or
1845
+ # one it calls under another key. The probe evaluated the def at every key its readers observe it under, so
1846
+ # its reads are a superset of theirs through this def, and its memo holds every key those evaluations put to
1847
+ # another def. `reads` is each passing pair's {DependencyRecorder::Record}; `summaries` is the probe's own
1848
+ # harvest ({Runner#return_summaries}: nil for a def it cannot list the keys of, nil as a whole in pool mode),
1849
+ # cut down to the defs a passing pair read. A failing pair's observations are left out: its readers are all
1850
+ # re-analysed, and the run's harvest speaks for them. Issue #3187 — `plugin_reads` is each passing pair's plugin
1851
+ # boundary rows ({Runner#probe_plugin_reads}); a pair whose evaluation read none has no key.
1852
+ GateObservations = Data.define(:reads, :summaries, :plugin_reads)
1853
+ private_constant :GateObservations
1854
+
1855
+ # `passed` maps each passing pair to its spec triple. The probe recorded reads for every def it evaluated, and
1856
+ # it evaluated each of these: a summary holds at least one key, so a def the probe could not find answered nil
1857
+ # and did not pass.
1858
+ def gate_observations(runner, passed)
1859
+ reads = passed.transform_values { |triple| runner.probe_reads[triple] }
1860
+ observed = reads.each_value.with_object(Set.new) do |record, pairs|
1861
+ record.symbol_sources.each { |path, symbols| symbols.each { |symbol| pairs << [path, symbol] } }
1862
+ end
1863
+ GateObservations.new(reads: reads, summaries: runner.return_summaries&.slice(*observed),
1864
+ plugin_reads: probe_rows(passed, runner.probe_plugin_reads))
1865
+ end
1866
+
1867
+ # `table` (spec triple => rows) re-keyed by each passing pair, leaving out a pair with no rows.
1868
+ def probe_rows(passed, table)
1869
+ passed.each_with_object({}) do |(pair, triple), rows|
1870
+ rows[pair] = table[triple] if table.key?(triple)
1623
1871
  end
1624
1872
  end
1625
1873
 
@@ -1634,10 +1882,15 @@ module Rigor
1634
1882
  singleton: singleton, keys: @return_summaries[pair][:keys] }
1635
1883
  end
1636
1884
 
1885
+ # The probe's runner and its `spec triple => descriptors` answer ({} when the probe failed).
1637
1886
  def probe_return_types(specs)
1638
- build_runner.evaluate_return_types(@paths, specs)
1887
+ runner = build_runner
1888
+ [runner, runner.evaluate_return_types(@paths, specs)]
1889
+ rescue Inference::MemoAudit::Mismatch
1890
+ # ADR-129 WD5 (#2785) — the audit's mismatch (raised by the run's release) fails the session, not the probe.
1891
+ raise
1639
1892
  rescue StandardError
1640
- {}
1893
+ [runner, {}]
1641
1894
  end
1642
1895
 
1643
1896
  TOP_LEVEL_KEY = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
@@ -3,6 +3,7 @@
3
3
  require_relative "../../plugin"
4
4
  require_relative "../../plugin/loader"
5
5
  require_relative "../../plugin/services"
6
+ require_relative "../../plugin/trust_policy"
6
7
  require_relative "../../reflection"
7
8
  require_relative "../../type/combinator"
8
9
 
@@ -107,14 +108,16 @@ module Rigor
107
108
  end
108
109
 
109
110
  # Mirrors `Analysis::WorkerSession`'s services: the shared fact store plus whatever cache store the
110
- # caller holds. Holding the `Services` is what gives access to the fact store the loaded plugins
111
- # share — the loader hands the same instance to every plugin.
111
+ # caller holds, and the run's trust policy — `rigor unused`'s plugin roots read the same scope
112
+ # `rigor check` does (#2586). Holding the `Services` is what gives access to the fact store the
113
+ # loaded plugins share — the loader hands the same instance to every plugin.
112
114
  def build_services(configuration, cache_store)
113
115
  Plugin::Services.new(
114
116
  reflection: Reflection,
115
117
  type: Type::Combinator,
116
118
  configuration: configuration,
117
- cache_store: cache_store
119
+ cache_store: cache_store,
120
+ trust_policy: Plugin::TrustPolicy.for_configuration(configuration)
118
121
  )
119
122
  end
120
123
 
@@ -25,7 +25,7 @@ module Rigor
25
25
  # profile, taken from `Configuration::SeverityProfile::PROFILES`.
26
26
  # - `evidence_tier` — `:high` / `:medium` / `:low` (or `nil` for informational helpers), Rigor's own
27
27
  # confidence that a firing is a true positive, derived from the rule's firing gates. `:high` rules
28
- # fire only on a concrete, statically- known type with no metaprogramming escape (Rigor's
28
+ # fire only on a concrete, statically-known type with no metaprogramming escape (Rigor's
29
29
  # false-positive discipline has already filtered the uncertain cases); `:medium` rules rest on flow /
30
30
  # inference proofs that inherit a documented FP envelope (loop / mutation / RBS-strictness modelling
31
31
  # gaps); `:low` rules are resolution- or coverage-gap signals where a firing often reflects missing
@@ -101,7 +101,9 @@ module Rigor
101
101
  "of the class are open, and an inherited Object / Kernel signature no longer decides " \
102
102
  "`call.wrong-arity` or `call.argument-type-mismatch` on it.",
103
103
  "The program may answer the call another way (#1722): a `method_missing` on the receiver's class or an " \
104
- "ancestor; a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
104
+ "ancestor, in source or in RBS (#2792; an RBS `method_missing` on `BasicObject`, `Object` or " \
105
+ "`Kernel` does not count); " \
106
+ "a module mixed into either that defines the method (`Integer.include(Ext)` counts wherever " \
105
107
  "it is written); singleton methods given to the receiver local (`s.extend(M)`, " \
106
108
  "`define_singleton_method`, `class << s`); or, for a receiver typed by its class's name rather than a " \
107
109
  "literal, a project subclass that defines the method."
@@ -1087,6 +1089,40 @@ module Rigor
1087
1089
  since: "0.4.0"
1088
1090
  ),
1089
1091
 
1092
+ CheckRules::RULE_PLUGIN_API_SURFACE => Entry.new(
1093
+ id: CheckRules::RULE_PLUGIN_API_SURFACE,
1094
+ summary: "Plugin code calls a member a `Rigor::Plugin` interface does not declare.",
1095
+ fires_when: [
1096
+ "The call's receiver is typed by a `Rigor::Plugin` interface (`_Scope`, `_Environment`, " \
1097
+ "`_Services`, `_FactStore`, `_Configuration`), possibly with `nil`: a hook's `scope`, " \
1098
+ "`scope.environment`, `services.fact_store`, and so on.",
1099
+ "The interface does not declare the called method, and neither does `Object` (with `Kernel` and " \
1100
+ "`BasicObject`).",
1101
+ "The call sits inside a class whose superclass chain reaches `Rigor::Plugin::Base`.",
1102
+ "The `plugin-api-surface` bleeding-edge feature is enabled, or a `severity_overrides:` key names " \
1103
+ "`plugin.api-surface` (the rule resolves `:off` otherwise; a dot-free `plugin` key does not reach it)."
1104
+ ],
1105
+ does_not_fire_when: [
1106
+ "The interface declares the method (`scope.type_of(node)`), or every object answers it " \
1107
+ "(`scope.nil?`, `scope.respond_to?(...)`). A call that a `respond_to?` guard protects still fires.",
1108
+ "The value lost its interface type: a join with an untyped value, a helper parameter with no " \
1109
+ "signature, or the result of a `&.` call on a value that may be nil. It reads as `untyped` there.",
1110
+ "The call is outside a `Rigor::Plugin::Base` subclass, or inside `Rigor::Plugin::Base` itself.",
1111
+ "The `plugin-api-surface` bleeding-edge feature is not enabled (the default)."
1112
+ ],
1113
+ suppression: "`# rigor:disable plugin.api-surface` on the call line, or " \
1114
+ "`disable: [\"plugin.api-surface\"]` in `.rigor.yml`. The fix is to call a member the " \
1115
+ "interface declares or a public namespace (`Rigor::Reflection`, ...).",
1116
+ severity_authored: :warning,
1117
+ severity_by_profile: { lenient: :off, balanced: :off, strict: :off },
1118
+ # The receiver's interface type comes from Rigor's own signatures and the member list is the
1119
+ # interface's declaration, so a firing is a call outside the declared surface. It ships `:off`
1120
+ # behind `bleeding_edge:` because it is a new discipline (ADR-50 Decision 3), not for lack of
1121
+ # confidence.
1122
+ evidence_tier: :high,
1123
+ since: "0.4.8"
1124
+ ),
1125
+
1090
1126
  CheckRules::RULE_SUPPRESSION_UNKNOWN_MARKER => Entry.new(
1091
1127
  id: CheckRules::RULE_SUPPRESSION_UNKNOWN_MARKER,
1092
1128
  summary: "A comment uses a suppression marker Rigor does not recognise " \
@@ -1120,7 +1156,7 @@ module Rigor
1120
1156
 
1121
1157
  module_function
1122
1158
 
1123
- # Looks up a rule by canonical id, legacy alias, or family wildcard. Returns an Array<Entry>:
1159
+ # Looks up a rule by canonical id, legacy alias, or family wildcard. Returns the matching entries:
1124
1160
  #
1125
1161
  # - canonical id → 1-element array,
1126
1162
  # - legacy alias → 1-element array (resolved to canonical),