rigortype 0.4.7 → 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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  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 +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -124,9 +124,13 @@ module Rigor
124
124
  # Diffed on a recheck to find every constant whose PUBLISHED answer could have moved, which satisfies
125
125
  # the `constant:` edges a reader records at every reference, resolved or not.
126
126
  @constant_decls = {}
127
- # 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: }.
128
129
  # Harvested from the ADR-84 return memo after each run; drives the behavioural-stability gate.
129
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
130
134
  # ADR-67 WD6c lift — the `parameter_inference:` seed table the cached diagnostics were computed
131
135
  # under (`{}` when the gate is off — the gate state is constant across a snapshot's lifetime because
132
136
  # the configuration is part of the global fingerprint). A recheck recomputes the table fresh and
@@ -263,7 +267,7 @@ module Rigor
263
267
  # digest (issue #1536).
264
268
  @seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: true)
265
269
  absorb_dependency_graph(runner)
266
- @return_summaries = runner.return_summaries # ADR-89 WD2 — the full-run behavioural surface.
270
+ @return_summaries = baseline_return_summaries(runner)
267
271
  # ADR-67 WD6c lift — the seed table the runner's own pre-pass computed ({} when the gate is off).
268
272
  # Reading it back, rather than computing it here, keeps the baseline single-collect.
269
273
  @param_table = runner.param_inferred_types
@@ -391,6 +395,8 @@ module Rigor
391
395
  # whole project — what a `sig/` edit already costs through the snapshot fingerprint.
392
396
  def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
393
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
394
400
  return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
395
401
 
396
402
  (gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
@@ -577,8 +583,10 @@ module Rigor
577
583
  # surfaces are its return type and its content-mutation effects (no instance/class variable write, no
578
584
  # `yield`, no implicit-self call whose transitive effect a caller could observe). For a candidate the
579
585
  # session compares its content-mutation effects (pure AST) then re-evaluates its return at every
580
- # persisted key (a runner probe); a pair passes only when BOTH are unchanged. Any missing summary,
581
- # 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
582
590
  # direction the `--verify-incremental` and byte-identical recheck specs backstop.
583
591
  def behaviourally_unstable_pairs(changed_pairs, unstable, scan_index)
584
592
  return changed_pairs if @return_summaries.empty? || scan_index.nil?
@@ -1236,10 +1244,79 @@ module Rigor
1236
1244
  # with anything its closure demanded for itself, so this is the same union the next run replays.
1237
1245
  @run_level_rows = runner.run_level_rows
1238
1246
  @synthetic_surface_digest = synthetic_surface_digest_of(runner)
1239
- refresh_return_summaries(runner, analyze_set)
1247
+ refresh_gated_state(runner, analyze_set)
1240
1248
  refresh_effect_collections(runner, analyze_set)
1241
1249
  end
1242
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
+
1243
1320
  # ADR-103 WD13 / #382 — the effects analogue of {#refresh_return_summaries}, and the same soundness
1244
1321
  # order: drop every re-analyzed file's collection first, then fold this run's harvest in. A file the
1245
1322
  # recheck did NOT analyze keeps the collection the snapshot carried, which is exactly the reuse the
@@ -1255,21 +1332,97 @@ module Rigor
1255
1332
  @effect_plugin_digest = effect_plugin_digest_of(runner)
1256
1333
  end
1257
1334
 
1258
- # ADR-89 WD2 — replace the behavioural summaries of every re-analyzed file with THIS run's harvest: drop
1259
- # the analyzed files' old summaries first, then fold the fresh ones. Dropping first is the soundness
1260
- # invariant — a summary for a file is valid only if it came from a run that analyzed that file. On the
1261
- # sequential path (the default + `--verify-incremental`) the harvest is complete, so a re-analyzed def's
1262
- # summary is simply refreshed. On a pooled recheck the callee bodies are evaluated in worker processes,
1263
- # so the main-process memo harvest is sparse; dropping first means a re-analyzed file with no fresh
1264
- # summary loses its stale one (WD2 then conservatively re-checks its dependents) rather than comparing a
1265
- # later edit against a pre-edit summary. Files NOT re-analyzed keep their (unchanged, valid) summaries.
1266
- # A def whose symbol dependents WD2 skipped is observed under fewer keys, so its refreshed summary may
1267
- # shrink — sound (fewer keys = more conservative) and self-correcting (an empty summary stops the gate
1268
- # firing, so the callers re-analyse and repopulate).
1269
- 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)
1270
1364
  analyzed = analyze_set.to_set
1271
- @return_summaries.reject! { |(path, _symbol), _| analyzed.include?(path) }
1272
- @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
1273
1426
  end
1274
1427
 
1275
1428
  # Evict a removed file from every per-file map so its stale diagnostics are never served and it drops
@@ -1302,10 +1455,7 @@ module Rigor
1302
1455
  @ancestry_sources[path] = record.ancestry_sources.dup
1303
1456
  @missing[path] = record.missing.dup
1304
1457
  end
1305
- @dependents = Incremental.invert(@sources)
1306
- @symbol_dependents = Incremental.invert_symbols(@symbol_sources)
1307
- @ancestry_dependents = Incremental.invert(@ancestry_sources)
1308
- @negative_dependents = Incremental.invert(@missing)
1458
+ invert_dependency_graph
1309
1459
  @symbol_fingerprints.merge!(runner.symbol_fingerprints)
1310
1460
  # Wholesale replace (the subset runner's pre-pass is complete): a file that lost its last class must
1311
1461
  # drop out of the map so a later re-add registers as an appearance.
@@ -1394,6 +1544,11 @@ module Rigor
1394
1544
  # (`ResolutionChain#record_hooks`), so a new file that taints `Comparable` with `include(*MODS)` re-checks the
1395
1545
  # readers of a chain through it. It is not `mark:`, which a plain reopening of a module moves (the bucket's
1396
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.
1397
1552
  def mark_affected(paths, summary)
1398
1553
  def_index = summary && summary[:def_index]
1399
1554
  before = paths.map { |path| @seed_bundles.dig(path, :parameter_envelopes) }
@@ -1401,7 +1556,7 @@ module Rigor
1401
1556
  tainted = Incremental.changed_unpositioned_owners(
1402
1557
  paths.map { |path| @seed_bundles.dig(path, :unpositioned_mixins) },
1403
1558
  [def_index && def_index[:unpositioned_mixins]]
1404
- )
1559
+ ) | dynamic_owners(moved)
1405
1560
  return Set.new if moved.empty? && tainted.empty?
1406
1561
 
1407
1562
  keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
@@ -1412,6 +1567,11 @@ module Rigor
1412
1567
  Incremental.negative_closure(keys, @negative_dependents)
1413
1568
  end
1414
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
+
1415
1575
  # Issue #1661 — the `mixin:` keys an owner of a moved unpositioned edge fires. The table keys a receiver-form
1416
1576
  # owner by the spelling written (`class File; Constants.include(*MODS); end` is `"Constants"`), not the constant
1417
1577
  # Ruby resolves it to, while the chain files the qualified candidate (`mixin:File::Constants`). The table does
@@ -1556,7 +1716,9 @@ module Rigor
1556
1716
 
1557
1717
  # ADR-89 WD2 — true when the ONLY cross-file body surfaces of `node` a symbol dependent can consume are
1558
1718
  # its return type and its content-mutation effects — so comparing exactly those two is a COMPLETE
1559
- # 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
1560
1722
  # definite-assignment a same-class caller consumes), does not `yield` (a block-parameter surface a
1561
1723
  # caller passing a block consumes), and makes no implicit-self call (which could carry a transitive
1562
1724
  # shared-state write). Deliberately conservative + purely syntactic: a def with a self-call or a
@@ -1608,29 +1770,104 @@ module Rigor
1608
1770
  end
1609
1771
 
1610
1772
  def content_effects_for(node)
1611
- (@effects_evaluator ||= Inference::StatementEvaluator.new(scope: Scope.empty))
1612
- .content_mutated_parameter_positions(node)
1773
+ effects_evaluator.content_mutated_parameter_positions(node)
1613
1774
  rescue StandardError
1614
1775
  nil
1615
1776
  end
1616
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
+
1617
1791
  # ADR-89 WD2 — the subset of `pairs` whose (edited) def re-evaluates to the SAME return descriptor at
1618
1792
  # every persisted key. Runs one runner probe (`evaluate_return_types` — builds the discovery + env once,
1619
1793
  # no file analysis) over all candidate defs, then keeps a pair only when every key's re-evaluated
1620
1794
  # descriptor is present (not a memo refusal) and equal to the persisted one. A probe failure yields an
1621
1795
  # empty set (keep everything).
1622
1796
  def return_stable_pairs(pairs, scan_index)
1623
- 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
1624
1800
  return Set.new if specs.empty?
1625
1801
 
1626
- results = probe_return_types(specs)
1627
- specs.each_with_object(Set.new) do |spec, stable|
1628
- descriptors = results[[spec[:class_name], spec[:method_name], spec[:singleton]]]
1629
- expected = @return_summaries[spec[:pair]][:returns]
1630
- next if descriptors.nil? || descriptors.length != expected.length
1631
- 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)
1632
1809
 
1633
- 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)
1634
1871
  end
1635
1872
  end
1636
1873
 
@@ -1645,10 +1882,15 @@ module Rigor
1645
1882
  singleton: singleton, keys: @return_summaries[pair][:keys] }
1646
1883
  end
1647
1884
 
1885
+ # The probe's runner and its `spec triple => descriptors` answer ({} when the probe failed).
1648
1886
  def probe_return_types(specs)
1649
- 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
1650
1892
  rescue StandardError
1651
- {}
1893
+ [runner, {}]
1652
1894
  end
1653
1895
 
1654
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
 
@@ -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 " \
@@ -99,7 +99,8 @@ module Rigor
99
99
  gems: [Cache::RbsDescriptor.rbs_gem_entry],
100
100
  dependencies: dependency_source_entries(configuration),
101
101
  configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) +
102
- template_unit_entries(template_units_digest) + shadow_harness_entries + [
102
+ collection_setup_hint_entries(configuration) + template_unit_entries(template_units_digest) +
103
+ shadow_harness_entries + [
103
104
  config_entry("configuration", Marshal.dump(configuration.to_h)),
104
105
  config_entry("engine",
105
106
  "#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{explain}"),
@@ -179,6 +180,24 @@ module Rigor
179
180
  [lockfile_entry("bundler.lockfile", bundler), lockfile_entry("rbs_collection.lockfile", collection)]
180
181
  end
181
182
 
183
+ # Issue #3030 — the `rbs.coverage.collection-not-installed` hint is a run-level row read off the working
184
+ # directory (an `rbs_collection.yaml`, a gem_rbs_collection `_test/` layout), which neither the lockfile slot
185
+ # nor the configuration covers: without this slot a warm run replayed the hint after the file went, or
186
+ # missed it after the file came. No slot when there is no hint, so every other project keeps its key.
187
+ def collection_setup_hint_entries(configuration)
188
+ hint = collection_setup_hint(configuration)
189
+ hint.nil? ? [] : [config_entry("rbs_collection.setup_hint", hint.to_s)]
190
+ end
191
+
192
+ # The hint {Analysis::Runner::DiagnosticAggregator} reports, read the same way. Shared with
193
+ # {Cache::IncrementalSnapshot.fingerprint}.
194
+ def collection_setup_hint(configuration)
195
+ Environment::RbsCollectionDiscovery.missing_lockfile_hint(
196
+ lockfile_path: configuration.rbs_collection_lockfile,
197
+ auto_detect: configuration.rbs_collection_auto_detect
198
+ )
199
+ end
200
+
182
201
  # ADR-10 slice 3, issue #1814 — the `dependencies.source_inference` gems' `(name, version, mode)` rows, which the
183
202
  # walker's output depends on and no lockfile digest covers when the project has none or the gem sits beside the
184
203
  # bundle. Shared by this key and by {Cache::IncrementalSnapshot.fingerprint}, as the lockfiles are. A project
@@ -216,12 +235,17 @@ module Rigor
216
235
 
217
236
  # The `rbs.libraries` config slot reconstructed from configuration alone — byte-identical to the
218
237
  # loader's `RbsDescriptor.libraries_entry(loader.libraries)` because `Environment.for_project` merges
219
- # exactly `DEFAULT_LIBRARIES + config.libraries` (uniq) into `loader.libraries`, plus the
238
+ # exactly {Environment::BaseLibraries.resolve} — `DEFAULT_LIBRARIES + config.libraries` + the collection lockfile
239
+ # stdlib entries (issue #2946), uniq — into `loader.libraries`, plus the
220
240
  # `Environment::RequiredFeatures` tokens the run's source activates (issue #1700). Those are left out
221
241
  # here: reading every source file would cost the boot-slimming probe the time it exists to save, and a
222
242
  # project that requires a gated feature then only misses this probe, never hits a wrong slot.
223
243
  def libraries_config_entries(configuration)
224
- merged = (Environment::DEFAULT_LIBRARIES + configuration.libraries.map(&:to_s)).uniq
244
+ merged = Environment::BaseLibraries.resolve(
245
+ configuration.libraries,
246
+ rbs_collection_lockfile: configuration.rbs_collection_lockfile,
247
+ rbs_collection_auto_detect: configuration.rbs_collection_auto_detect
248
+ )
225
249
  [Cache::RbsDescriptor.libraries_entry(merged)]
226
250
  end
227
251
  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
  # Issue #2020 — what one file's analysis contributed to the run-scoped plugin tables ({Plugin::Base#disclose_once}
@@ -17,16 +19,27 @@ module Rigor
17
19
  ATTRIBUTION_KEY = :rigor_run_disclosure_attribution
18
20
  private_constant :ATTRIBUTION_KEY
19
21
 
22
+ # ADR-129 WD3 / #2814 — the registrations sink is a Resource: a `type_of` memo hit that served an answer without
23
+ # re-running the plugin hook would not append the registration the miss appended, so every {.attribute} refuses
24
+ # each open capture. Its storage stays the thread variable above (ADR-129 WD5 shape 2); this slot declares its
25
+ # kind.
26
+ SINK = TypingState.resource(
27
+ :run_disclosure_attribution,
28
+ reason: "every run-scoped plugin registration appends to the collecting sink, which a memo hit would not replay"
29
+ )
30
+ private_constant :SINK
31
+
20
32
  module_function
21
33
 
22
34
  # Runs the block with run-scoped registrations attributed and returns the file's records — each a run-disclosure
23
35
  # record tagged with its `plugin_id` and carrying the registering plugin's boundary rows as `:reads`, one per
24
36
  # `[plugin id, key]` — frozen and Marshal-clean. A thread variable, as {Plugin::IoBoundary.attributing} uses, so a
25
- # registration from inside an `Enumerator` still lands.
37
+ # registration from inside an `Enumerator` still lands, carrying the installing fiber's typing state as
38
+ # {Plugin::IoBoundary.attributing} does.
26
39
  def collect
27
40
  registrations = []
28
41
  previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
29
- Thread.current.thread_variable_set(ATTRIBUTION_KEY, registrations)
42
+ Thread.current.thread_variable_set(ATTRIBUTION_KEY, [registrations, TypingState.current].freeze)
30
43
  begin
31
44
  yield
32
45
  ensure
@@ -35,9 +48,16 @@ module Rigor
35
48
  credit(registrations)
36
49
  end
37
50
 
38
- # Called by {Plugin::Base#disclose_once} and {Plugin::Base#emit_once} for every call, a repeat included.
51
+ # Called by {Plugin::Base#disclose_once} and {Plugin::Base#emit_once} for every call, a repeat included. Touches
52
+ # {SINK} on the calling fiber's typing state and on the installing fiber's (ADR-129 WD3, #2814).
39
53
  def attribute(plugin, record)
40
- Thread.current.thread_variable_get(ATTRIBUTION_KEY)&.push([plugin, record])
54
+ state = TypingState.current
55
+ state.touch(SINK)
56
+ registrations, owner = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
57
+ return nil if registrations.nil?
58
+
59
+ owner.touch(SINK)
60
+ registrations.push([plugin, record])
41
61
  nil
42
62
  end
43
63
 
@@ -63,9 +83,9 @@ module Rigor
63
83
  end
64
84
 
65
85
  # The boundary's file and listing rows, read without triggering its lazy initializer: a plugin that never
66
- # built one read nothing through it.
86
+ # built one read nothing through it. {Plugin::Base#built_io_boundary} is private to the engine, so it is sent.
67
87
  def boundary_rows(plugin)
68
- boundary = plugin.instance_variable_get(:@io_boundary) or return []
88
+ boundary = plugin.__send__(:built_io_boundary) or return []
69
89
  descriptor = boundary.cache_descriptor
70
90
  descriptor.files + descriptor.globs
71
91
  end