rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -297,11 +297,15 @@ module Rigor
297
297
  record_dependencies: false, record_self_calls: false, analyze_only: nil,
298
298
  seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
299
299
  discovery_seed: nil, no_tolerated_effects: false,
300
- restored_run_level_rows: nil)
300
+ restored_run_level_rows: nil, served_per_file_diagnostics: nil)
301
301
  @configuration = configuration
302
302
  # Issues #796 / #794 — the previous full run's run-level rows, supplied only by
303
303
  # {IncrementalSession} on a narrowed run; see {PoolCoordinator#replay_restored_run_level_rows}.
304
304
  @restored_run_level_rows = restored_run_level_rows
305
+ # Issue #1524 — `{path => rows}` the {IncrementalSession} serves from its per-file cache for the files a
306
+ # narrowed run does not analyse, spliced into this run's per-file stream by
307
+ # {#interleave_served_diagnostics}. Empty on every other run.
308
+ @served_per_file_diagnostics = served_per_file_diagnostics || {}
305
309
  @explain = explain
306
310
  @cache_store = enforce_read_only_cache(cache_store, buffer)
307
311
  @plugin_requirer = plugin_requirer
@@ -353,6 +357,12 @@ module Rigor
353
357
  @cached_effect_ancestry = nil
354
358
  @cached_collections_loader = nil
355
359
  @run_dependency_descriptor = nil
360
+ # ADR-45 WD2 (#1507) — the plugin reads each file's analysis made (`{path => [row]}`), and the boundary
361
+ # rows as they stood around the per-file loop; see {#incremental_slot_rows}. Recorded only by a
362
+ # dependency-recording (incremental) run.
363
+ @analysis_reads = {}
364
+ @pre_analysis_plugin_rows = nil
365
+ @post_analysis_plugin_rows = nil
356
366
  # Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
357
367
  # See `self_undefined_rule_active?`.
358
368
  @self_undefined_rule_active = nil
@@ -414,6 +424,7 @@ module Rigor
414
424
  @project_discovered_prepends = {}.freeze
415
425
  # Issue #898 — the singleton-side twin of the include table (`extend M` / `extend self`).
416
426
  @project_discovered_extends = {}.freeze
427
+ @project_unpositioned_mixins = {}.freeze
417
428
  @project_discovered_deferred_ranges = {}.freeze
418
429
  # Issue #1120 — `{refined class => {method => [refining modules]}}`, every `refine X do … end` body.
419
430
  @project_discovered_refinements = {}.freeze
@@ -433,6 +444,9 @@ module Rigor
433
444
  @constant_shadower_index = nil
434
445
  @project_discovered_method_visibilities = {}.freeze
435
446
  @project_discovered_methods = {}.freeze
447
+ # ADR-119 WD1 — the `possible_*` / `contested_*` siblings of the paired members, ONE Hash keyed by sibling
448
+ # name so no adoption or seed path names a sibling and none can drop one.
449
+ @project_discovery_siblings = Scope::DiscoveryIndex::EMPTY_SIBLINGS
436
450
  # Issue #992 — the cross-file parameter-envelope table `call.wrong-arity` reads for an undeclared `def`.
437
451
  @project_discovered_parameter_envelopes = {}.freeze
438
452
  @project_data_member_layouts = {}.freeze
@@ -508,6 +522,9 @@ module Rigor
508
522
  # #788 — the per-file reader is assigned only on the analysis (miss) path; reset it here so a run the
509
523
  # ADR-45 result cache serves does not answer with the previous run's rows.
510
524
  @per_file_diagnostics = [].freeze
525
+ @analysis_reads = {}
526
+ @pre_analysis_plugin_rows = nil
527
+ @post_analysis_plugin_rows = nil
511
528
  # ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
512
529
  # of THIS run share one memo bucket while entries from any earlier run in this process (stale after
513
530
  # an edit) become unreachable.
@@ -558,6 +575,12 @@ module Rigor
558
575
  @in_memory_sources = nil
559
576
  end
560
577
 
578
+ # Issue #1700 — the sources the required-feature scan reads in place of a file on disk: an in-memory
579
+ # source ({#run_source}) and an editor buffer (`--tmp-file` read for its `--instead-of` path).
580
+ def required_feature_sources
581
+ (@in_memory_sources || {}).merge(ProjectEnvironment.buffer_sources(@buffer))
582
+ end
583
+
561
584
  # ADR-46 — the project file set that a run over `paths` would analyze, computed by globbing only (no
562
585
  # RBS environment build), so the incremental fingerprint can be derived cheaply on the warm path
563
586
  # before deciding whether to build the env at all.
@@ -619,11 +642,16 @@ module Rigor
619
642
  # detect a class that *appeared* in an edit so a subclass whose ancestor was previously undefined
620
643
  # (and so recorded a negative class edge) is re-checked. Inverts the project class-source attribution
621
644
  # (class → declaring files).
645
+ #
646
+ # Issue #1820 — each file's set also holds a `[class, superclass as written]` pair for every class it declares
647
+ # with a superclass, so a subclass appearing or vanishing re-checks the consumers that enumerated the parent's
648
+ # subclasses (`subclass_of:<last segment>`).
622
649
  def class_declarations
623
650
  result = Hash.new { |hash, key| hash[key] = Set.new }
624
651
  @project_discovered_class_sources.each do |class_name, files|
625
652
  files.each { |file| result[file] << class_name }
626
653
  end
654
+ Incremental.add_superclass_edges(result, @project_discovered_class_sources, @project_discovered_superclasses)
627
655
  result.transform_values(&:freeze).freeze
628
656
  end
629
657
 
@@ -828,6 +856,11 @@ module Rigor
828
856
  # then hit — the stats it would otherwise suppress were all gathered.
829
857
  @run_served_from_cache = !computed && analysis.nil?
830
858
  diagnostics
859
+ rescue Inference::DeclarationWalk::ContractError
860
+ # A broken walk contract in the project pre-pass (a shadow-harness divergence, an unknown variant) is
861
+ # not a cache failure: the fallback below would re-run the analysis with the pre-pass marked done and
862
+ # no project index, and report the resulting false positives instead of the failure.
863
+ raise
831
864
  rescue StandardError
832
865
  # The result cache must never break a run. If anything in the cache path fails, fall back to a
833
866
  # direct, uncached analysis.
@@ -1175,19 +1208,55 @@ module Rigor
1175
1208
  # `#per_file_diagnostics`: the `analyze_files` return, stamped with the same severity profile the run's
1176
1209
  # own stream gets (`SeverityStamp` drops `:off` rows and re-stamps overrides, and the per-file cache never
1177
1210
  # re-stamps), sliced to the rows positioned at the targets. Returns the RAW return for the run's stream,
1178
- # which is stamped once, at the end of `#run_analysis`.
1211
+ # which is stamped once, at the end of `#run_analysis` — on a narrowed incremental run with the served
1212
+ # files' rows spliced in (#1524), which `#per_file_diagnostics` never carries.
1179
1213
  def analyze_targets(targets, environment:, project_files:)
1180
1214
  raw = template_units.remap(
1181
1215
  @pool_coordinator.analyze_files(targets, environment: environment, project_files: project_files)
1182
1216
  )
1217
+ # ADR-45 WD2 (#1507) — the boundaries as the per-file loop left them; with the snapshot the first
1218
+ # analysed file took, the rows {#incremental_slot_rows} checks every analysis-time read against.
1219
+ @post_analysis_plugin_rows = plugin_boundary_rows if @record_dependencies
1183
1220
  analysed = targets.to_set
1184
1221
  @per_file_diagnostics = @diagnostic_aggregator.apply_severity_profile(raw)
1185
1222
  .select { |diagnostic| analysed.include?(diagnostic.path) }
1186
1223
  .freeze
1187
- raw
1224
+ interleave_served_diagnostics(raw, analysed, project_files | targets)
1188
1225
  end
1189
1226
  private :analyze_targets
1190
1227
 
1228
+ # Issue #1524 — splices the rows {IncrementalSession} serves from its per-file cache into this narrowed
1229
+ # run's per-file stream, each file's rows where a full run of the same tree lists them. A full run
1230
+ # analyses `order` (the project's files, then any buffer and template-unit targets) one file after
1231
+ # another and a narrowed run a subsequence of it, so a served file's rows go just before the first row
1232
+ # positioned at an analysed file later in `order`, or at the end when there is none. Only such a row
1233
+ # marks a place, so the rows this run produced keep their relative order and a pool backend's
1234
+ # `.rigor.yml` rows stay at the head. Splicing here rather than into the finished result keeps the
1235
+ # run-level streams on either side of the per-file stream, where a full run has them. The served rows
1236
+ # are already severity-resolved and the run's final stamp leaves them unchanged: a resolved severity
1237
+ # resolves to itself, and a row the profile turns `:off` is never cached.
1238
+ def interleave_served_diagnostics(raw, analysed, order)
1239
+ served = @served_per_file_diagnostics
1240
+ return raw if served.empty?
1241
+
1242
+ rank = order.each_with_index.to_h
1243
+ # A served path the expansion no longer lists (a file deleted between the session's listing and this
1244
+ # run) keeps its rows, after every other file's, as the concatenation this replaced did.
1245
+ known, unknown = served.keys.partition { |path| rank.key?(path) }
1246
+ pending = known.sort_by { |path| rank.fetch(path) }
1247
+ merged = []
1248
+ raw.each do |diagnostic|
1249
+ # `order` lists every target, so an analysed path always has a rank; one that did not would only
1250
+ # mark no place, since a misplaced row must never abort the run (ADR-5).
1251
+ position = analysed.include?(diagnostic.path) ? rank.fetch(diagnostic.path, -1) : -1
1252
+ merged.concat(served.fetch(pending.shift)) while pending.any? && rank.fetch(pending.first) < position
1253
+ merged << diagnostic
1254
+ end
1255
+ (pending + unknown).each { |path| merged.concat(served.fetch(path)) }
1256
+ merged
1257
+ end
1258
+ private :interleave_served_diagnostics
1259
+
1191
1260
  # ADR-67 WD6a — the check-walk parameter-inference pre-pass. Populates `@project_param_inferred_types`
1192
1261
  # (read by `project_scope_seed_tables`) with the call-site union of every undeclared parameter, running
1193
1262
  # ONE round (a single hop of call-site → param typing; the protection scan's three-round fixpoint stays a
@@ -1310,24 +1379,204 @@ module Rigor
1310
1379
  # `.rbs` files that existed while the run read them, so without these a signature file written after
1311
1380
  # the run left the slot validating fresh and the warm run answered without it.
1312
1381
  globs = rbs_descriptor.globs + template_unit_glob_entries
1382
+ boundary = plugin_boundary_rows
1383
+ entries.concat(boundary.files)
1384
+ globs.concat(boundary.globs).concat(producer_watch_glob_entries)
1385
+ Cache::Descriptor.new(files: entries, globs: globs.uniq)
1386
+ end
1387
+
1388
+ # Every file a plugin read or probed through its `IoBoundary` (#577 / #613), and every directory it listed
1389
+ # (#954), as the boundaries stand when asked.
1390
+ def plugin_boundary_rows
1391
+ files = []
1392
+ globs = []
1313
1393
  @plugin_registry.plugins.each do |plugin|
1314
1394
  # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
1315
1395
  # are frozen after the run, and a plugin that never built a boundary read no files through it,
1316
1396
  # so it contributes no dependencies.
1317
1397
  boundary = plugin.instance_variable_get(:@io_boundary)
1318
- if boundary
1319
- # #629 — a boundary carries GlobEntry listing rows too (`IoBoundary#list_directory`), and they
1320
- # are the only edge back to a file the run decided was ABSENT from a directory it consulted.
1321
- boundary_descriptor = boundary.cache_descriptor
1322
- entries.concat(boundary_descriptor.files)
1323
- globs.concat(boundary_descriptor.globs)
1398
+ next unless boundary
1399
+
1400
+ # #629 — a boundary carries GlobEntry listing rows too (`IoBoundary#list_directory`), and they
1401
+ # are the only edge back to a file the run decided was ABSENT from a directory it consulted.
1402
+ boundary_descriptor = boundary.cache_descriptor
1403
+ files.concat(boundary_descriptor.files)
1404
+ globs.concat(boundary_descriptor.globs)
1405
+ end
1406
+ Cache::Descriptor.new(files: files, globs: globs)
1407
+ end
1408
+
1409
+ # One glob row per producer `watch:` pattern (ADR-60 WD3), recomputed against the tree as it stands.
1410
+ def producer_watch_glob_entries
1411
+ @plugin_registry.plugins.flat_map do |plugin|
1412
+ plugin.class.producers.each_value.flat_map do |prod|
1413
+ prod[:watch] ? plugin.send(:watch_glob_entries, prod[:watch]) : []
1324
1414
  end
1325
- plugin.class.producers.each_value do |prod|
1326
- globs.concat(plugin.send(:watch_glob_entries, prod[:watch])) if prod[:watch]
1415
+ end
1416
+ end
1417
+
1418
+ # ADR-45 WD2 (#1507) — the rows `rigor check --incremental`'s run-result slot records beside the analysed
1419
+ # files (whose digests {IncrementalSession} holds itself). Every run reads these inputs again and re-derives
1420
+ # its answer from them; they are split by when the row was taken, which decides whether the session's
1421
+ # {IncrementalRunSlot::WriteGuard} must vouch for it:
1422
+ #
1423
+ # - `observed` — every plugin row (`#prepare`, producers, template synthesis, and this run's per-file reads
1424
+ # too), recorded by the {Plugin::IoBoundary} as the plugin read. It includes each producer's `watch:`
1425
+ # globs, which a producer replays into its boundary whether it computed or was served (#1558). A save
1426
+ # after the read leaves the row stale, so no guard is needed.
1427
+ # - `derived` — rows taken when the run ENDS: the template files and their claimed globs (#392, re-analysed
1428
+ # every run), and one existence row per analysis root and per `pre_eval:` entry. A run's path errors (`no
1429
+ # such file or directory`, `pre-eval.file-not-found`) answer to those last rows; the plain slot records
1430
+ # neither, and the incremental path regenerates the errors every run, so a slot that served one past the
1431
+ # edit that retracts it would print what no run of that tree prints. Existence rows carry ADR-45 WD1b's
1432
+ # bound: a directory replaced by a file of the same name stays fresh.
1433
+ # - `by_file` — `{path => Descriptor}`, the plugin reads each file's analysis made
1434
+ # ({Plugin::IoBoundary.attributing}). A narrowed run analyses only its closure, so the session carries the
1435
+ # other files' reads forward from the previous slot.
1436
+ #
1437
+ # The template rows stand in for the `template-units` slot the plain key carries, which only a run can
1438
+ # compute. That digest is a function of each unit's compiled Ruby (the template's bytes, and whatever else
1439
+ # the transform read, through its boundary), its transform (the plugin, whose identity and configuration
1440
+ # the key carries) and the synthesis version (the engine's): each of those has a row or a key slot.
1441
+ #
1442
+ # The inputs a recheck does NOT re-derive are {#baseline_dependency_rows}. nil when this run cannot vouch for
1443
+ # its rows — a pool run, whose workers' reads never reach this process; a run that recorded no dependencies,
1444
+ # whose analysis no session owns; or a run in which a boundary row changed during the per-file loop that no
1445
+ # file's analysis was credited with, such as a read from a thread the plugin started.
1446
+ IncrementalSlotRows = Data.define(:observed, :derived, :by_file)
1447
+
1448
+ # @param roots — the analysis roots the run was given.
1449
+ def incremental_slot_rows(roots:)
1450
+ return nil if pool_mode? || !@record_dependencies
1451
+ return nil unless analysis_reads_attributed?
1452
+
1453
+ IncrementalSlotRows.new(
1454
+ observed: plugin_boundary_rows,
1455
+ derived: Cache::Descriptor.new(
1456
+ files: template_unit_file_entries + existence_entries(Array(roots) + @configuration.pre_eval),
1457
+ globs: template_unit_glob_entries
1458
+ ),
1459
+ by_file: @analysis_reads.transform_values do |reads|
1460
+ Cache::Descriptor.new(
1461
+ files: reads.grep(Cache::Descriptor::FileEntry).uniq,
1462
+ globs: reads.grep(Cache::Descriptor::GlobEntry).uniq
1463
+ )
1464
+ end
1465
+ )
1466
+ end
1467
+
1468
+ # Whether every boundary row that appeared or changed during the per-file loop is one some file's analysis
1469
+ # was credited with. A row outside every file's attribution would ride this run's `run` rows only, and the
1470
+ # next narrowed run — which does not repeat the read — would drop it.
1471
+ def analysis_reads_attributed?
1472
+ post = @post_analysis_plugin_rows || plugin_boundary_rows
1473
+ pre = @pre_analysis_plugin_rows || post
1474
+ credited = @analysis_reads.each_value.flat_map(&:itself).to_set
1475
+ (post.files - pre.files).all? { |row| credited.include?(row) } &&
1476
+ (post.globs - pre.globs).all? { |row| credited.include?(row) }
1477
+ end
1478
+
1479
+ # ADR-45 WD2 (#1507) — the rows a FULL run records for the inputs an incremental recheck does not re-derive,
1480
+ # which the session therefore carries unchanged from the full run that recorded them to every later slot:
1481
+ #
1482
+ # - the signature tree the plain slot records (`RbsDescriptor.file_entries` / `.glob_entries` over the loader
1483
+ # roots this run's analysis resolved), and an existence row per signature root, configured or the
1484
+ # auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty, and the snapshot
1485
+ # fingerprint digests the same roots (configured, or the auto-detected `sig`), so a change to one drops the
1486
+ # snapshot and forces a full run rather than reaching a recheck;
1487
+ # - the discovered-not-analysed files (#684) and a listing row per discovery root — a narrowed recheck does
1488
+ # not widen discovery at all;
1489
+ # - every `pre_eval:` file outside `files` (a stat row, or an absence row) — the session tracks only analysed
1490
+ # files, and one that is analysed is guarded by its own row.
1491
+ #
1492
+ # Recomputing any of these on a recheck would vouch for the tree the recheck saw while its answer still
1493
+ # carries rows computed before the change, so a change to one keeps the probe declining until the next full
1494
+ # run. nil when this process analysed no file itself — a pool run, or a run with no files.
1495
+ #
1496
+ # Split by what identifies the input. `pinned` holds the signature files outside the project's own signature
1497
+ # roots, which the slot's key identifies already: the engine's own (`data/`, a bundled plugin's `sig/`, a
1498
+ # gem overlay) by the engine slot, a gem's `sig/` and an `rbs collection` directory by the lockfiles. `owned`
1499
+ # holds the rest, which only their rows identify. The session's write guard refuses an `owned` row whose
1500
+ # change time it cannot read, and passes over a `pinned` one.
1501
+ BaselineRows = Data.define(:owned, :pinned)
1502
+
1503
+ SignatureRoots = Struct.new(:signature_paths)
1504
+ private_constant :SignatureRoots
1505
+
1506
+ # @param files — the run's analysed-path set.
1507
+ def baseline_dependency_rows(files:)
1508
+ return nil if pool_mode? || !@collect_stats || @analyzed_files.empty?
1509
+
1510
+ own, pinned = signature_rows
1511
+ BaselineRows.new(
1512
+ owned: Cache::Descriptor.new(
1513
+ files: own.files + existence_entries(signature_root_candidates) +
1514
+ discovery_file_entries({ files: files }) + unanalysed_pre_eval_entries(files),
1515
+ globs: (own.globs + discovery_glob_entries).uniq
1516
+ ),
1517
+ pinned: pinned
1518
+ )
1519
+ end
1520
+
1521
+ # The signature tree's rows, the project's own and the rest.
1522
+ def signature_rows
1523
+ roots = SignatureRoots.new(@snapshots.signature_paths)
1524
+ own = signature_root_candidates.map { |root| File.join(File.absolute_path(root.to_s), "") }
1525
+ own_files, other_files = Cache::RbsDescriptor.file_entries(roots, comparator: :stat)
1526
+ .partition { |row| within?(row.path, own) }
1527
+ own_globs, other_globs = Cache::RbsDescriptor.glob_entries(roots).partition { |row| within?(row.root, own) }
1528
+ [Cache::Descriptor.new(files: own_files, globs: own_globs.uniq),
1529
+ Cache::Descriptor.new(files: other_files, globs: other_globs.uniq)]
1530
+ end
1531
+
1532
+ def within?(path, roots)
1533
+ directory = File.join(File.absolute_path(path.to_s), "")
1534
+ roots.any? { |root| directory.start_with?(root) }
1535
+ end
1536
+
1537
+ # The project's own signature roots, whether or not they exist: the configured `signature_paths:`, or the
1538
+ # `sig` directory `Environment.default_signature_paths` auto-detects when none are configured. The
1539
+ # signature rows' listing rows cover a file appearing under a root that existed; only an existence row
1540
+ # covers the root itself appearing.
1541
+ def signature_root_candidates
1542
+ @configuration.signature_paths || ["sig"]
1543
+ end
1544
+
1545
+ # One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
1546
+ # as well as an edit to one that existed.
1547
+ def discovery_glob_entries
1548
+ return [] if @project_discovery_expansion.nil?
1549
+
1550
+ @configuration.paths.select { |root| File.directory?(root) }.map do |root|
1551
+ Cache::Descriptor::GlobEntry.compute(root: root, pattern: PathExpansion::RUBY_GLOB, mode: :names)
1552
+ end
1553
+ end
1554
+
1555
+ def unanalysed_pre_eval_entries(files)
1556
+ analysed = files.to_set { |path| File.absolute_path(path) }
1557
+ outside = @configuration.pre_eval.reject { |path| analysed.include?(File.absolute_path(path)) }
1558
+ outside.map do |path|
1559
+ if File.file?(path)
1560
+ Cache::Descriptor::FileEntry.stat(path: path, digest: Cache::FileDigest.hexdigest(path))
1561
+ else
1562
+ existence_entries([path]).first
1563
+ end
1564
+ end
1565
+ end
1566
+
1567
+ def existence_entries(paths)
1568
+ paths.uniq.map do |path|
1569
+ physical = @buffer ? @buffer.resolve(path) : path
1570
+ if File.exist?(physical)
1571
+ Cache::Descriptor::FileEntry.present(path: physical)
1572
+ else
1573
+ Cache::Descriptor::FileEntry.absent(path: physical)
1327
1574
  end
1328
1575
  end
1329
- Cache::Descriptor.new(files: entries, globs: globs.uniq)
1330
1576
  end
1577
+ private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries,
1578
+ :analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
1579
+ :unanalysed_pre_eval_entries, :signature_rows, :within?
1331
1580
 
1332
1581
  # Issue #684 — the project files this run DISCOVERED but did not analyse. A widened run's diagnostics
1333
1582
  # are a function of them (that is the entire point: the method whose absence would have been reported
@@ -1483,8 +1732,10 @@ module Rigor
1483
1732
  end
1484
1733
 
1485
1734
  # The tables only check rules read beyond dispatch — the issue #1120 refinements and the issue #1367
1486
- # global-write census. Extracted to keep {#apply_discovery_result} under its ABC budget.
1735
+ # global-write census — and the ADR-119 WD1 siblings of the paired members. Extracted to keep
1736
+ # {#apply_discovery_result} under its ABC budget.
1487
1737
  def apply_discovery_call_surface_tables(discovery)
1738
+ @project_discovery_siblings = discovery.siblings
1488
1739
  @project_discovered_refinements = discovery.discovered_refinements
1489
1740
  @project_discovered_global_write_census = discovery.discovered_global_write_census
1490
1741
  end
@@ -1496,6 +1747,7 @@ module Rigor
1496
1747
  @project_discovered_includes = discovery.discovered_includes
1497
1748
  @project_discovered_prepends = discovery.discovered_prepends
1498
1749
  @project_discovered_extends = discovery.discovered_extends
1750
+ @project_unpositioned_mixins = discovery.unpositioned_mixins
1499
1751
  end
1500
1752
 
1501
1753
  # Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
@@ -1512,7 +1764,7 @@ module Rigor
1512
1764
 
1513
1765
  @project_discovery_done = true
1514
1766
  # Issue #684 — the cross-file index spans the PROJECT even when the analysis targets a file list.
1515
- expansion = @project_discovery_expansion || expansion
1767
+ expansion = @project_discovery_expansion || once_each(expansion)
1516
1768
  if @collect_seed_bundles
1517
1769
  # ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
1518
1770
  # capture the refreshed bundle set for the session to persist.
@@ -1546,11 +1798,42 @@ module Rigor
1546
1798
  # Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
1547
1799
  # seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file, and
1548
1800
  # ADR-46's `analyze_only` subset mode (which already passes the full expansion and filters later).
1801
+ #
1802
+ # Issue #1556 — the widened set holds each FILE once. `.rigor.yml`'s `paths:` load as absolute paths and a
1803
+ # path argument usually does not, so `rigor check lib` widened discovery over both spellings of every file:
1804
+ # the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
1805
+ # and every file was a discovered-not-analysed dependency of the run's cache entry.
1806
+ #
1807
+ # The widening decision itself still compares the paths as spelled. `exclude:` patterns match a path as it
1808
+ # is spelled, so until #1576 is fixed the configured absolute `lib` can discover files the argument `lib`
1809
+ # excludes (a relative `lib/gen/**` pattern never matches `/abs/lib/gen/x.rb`), and treating the two as the
1810
+ # same root would drop those files from discovery.
1549
1811
  def project_discovery_expansion(paths, expansion)
1550
1812
  return nil unless widen_discovery_to_project?(paths)
1551
1813
 
1552
- widened = expand_paths(@configuration.paths | paths)
1553
- widened.fetch(:files).size > expansion.fetch(:files).size ? widened : nil
1814
+ analysed = expansion.fetch(:files)
1815
+ files = discovery_files(expand_paths(@configuration.paths | paths).fetch(:files), analysed)
1816
+ # Overlapping arguments (`lib lib/user.rb`) name an analysed file twice; the widened set names it once.
1817
+ files.size > analysed.uniq { |path| File.absolute_path(path) }.size ? { files: files, errors: [] } : nil
1818
+ end
1819
+
1820
+ # The expansion with each file once, first spelling kept: overlapping arguments (`lib ./lib`) that do not
1821
+ # widen discovery still name a file twice, and the pre-pass would take every constant in it for one written
1822
+ # twice (#1556). Symlinked aliases (`src -> lib`) are not folded; that needs a realpath and stays out of scope.
1823
+ def once_each(expansion)
1824
+ files = expansion.fetch(:files)
1825
+ unique = files.uniq { |path| File.absolute_path(path) }
1826
+ unique.size == files.size ? expansion : expansion.merge(files: unique)
1827
+ end
1828
+
1829
+ # Each file once, in project order, under the spelling the analysis uses when it is analysed.
1830
+ def discovery_files(project_files, analysed_files)
1831
+ spelling = analysed_files.to_h { |path| [File.absolute_path(path), path] }
1832
+ seen = Set.new
1833
+ project_files.filter_map do |path|
1834
+ absolute = File.absolute_path(path)
1835
+ spelling.fetch(absolute, path) if seen.add?(absolute)
1836
+ end
1554
1837
  end
1555
1838
 
1556
1839
  def widen_discovery_to_project?(paths)
@@ -1569,7 +1852,7 @@ module Rigor
1569
1852
 
1570
1853
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
1571
1854
  :apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?,
1572
- :project_discovery_expansion, :widen_discovery_to_project?
1855
+ :project_discovery_expansion, :discovery_files, :once_each, :widen_discovery_to_project?
1573
1856
 
1574
1857
  # Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
1575
1858
  # `version:`. Prism exposes no version list, so the floor is found empirically — only when a
@@ -1733,7 +2016,8 @@ module Rigor
1733
2016
  project_scope_seed: -> { project_scope_seed_tables },
1734
2017
  template_units: -> { template_units },
1735
2018
  analyze_file: ->(path, environment) { analyze_file(path, environment) },
1736
- restored_run_level_rows: @restored_run_level_rows
2019
+ restored_run_level_rows: @restored_run_level_rows,
2020
+ feature_sources: -> { required_feature_sources }
1737
2021
  )
1738
2022
  @diagnostic_aggregator = DiagnosticAggregator.new(
1739
2023
  configuration: @configuration,
@@ -1748,6 +2032,7 @@ module Rigor
1748
2032
  synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
1749
2033
  quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
1750
2034
  signature_standdowns_snapshot: -> { @snapshots.signature_standdowns },
2035
+ vendored_standdowns_snapshot: -> { @snapshots.vendored_standdowns },
1751
2036
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
1752
2037
  definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
1753
2038
  hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
@@ -1832,7 +2117,7 @@ module Rigor
1832
2117
  end
1833
2118
 
1834
2119
  def shadow_rule_walk?
1835
- ENV.fetch("RIGOR_SHADOW_RULE_WALK", nil)
2120
+ ShadowHarness.enabled?
1836
2121
  end
1837
2122
 
1838
2123
  def collect_plugin_diagnostics(plugin, path, root, scope, node_result)
@@ -1977,13 +2262,9 @@ module Rigor
1977
2262
  # bucket identically to the sequential path.
1978
2263
  tables[:run_generation] = @run_generation if @run_generation
1979
2264
  tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
1980
- tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
1981
2265
  unless @project_discovered_def_nestings.empty?
1982
2266
  tables[:discovered_def_nestings] = @project_discovered_def_nestings
1983
2267
  end
1984
- unless @project_discovered_singleton_def_nodes.empty?
1985
- tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
1986
- end
1987
2268
  seed_def_source_tables(tables)
1988
2269
  unless @project_discovered_superclasses.empty?
1989
2270
  tables[:discovered_superclasses] = @project_discovered_superclasses
@@ -1992,34 +2273,65 @@ module Rigor
1992
2273
  tables[:discovered_header_nestings] = @project_discovered_header_nestings
1993
2274
  end
1994
2275
  seed_mixin_tables(tables)
1995
- unless @project_discovered_method_visibilities.empty?
1996
- tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
1997
- end
1998
- tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
1999
- unless @project_discovered_deferred_ranges.empty?
2000
- tables[:discovered_deferred_ranges] = @project_discovered_deferred_ranges
2001
- end
2276
+ seed_paired_tables(tables)
2002
2277
  seed_call_surface_tables(tables)
2003
2278
  seed_opt_in_pre_pass_tables(tables)
2004
2279
  seed_member_layout_tables(tables)
2005
2280
  seed_dependency_attribution_tables(tables)
2006
- tables
2281
+ # Only a base seed can bring in half a pair (`seed_paired_tables` writes both halves), so a run without
2282
+ # one skips the copy this costs per file.
2283
+ @discovery_seed ? Scope::DiscoveryIndex.compact_pairs(tables) : tables
2284
+ end
2285
+
2286
+ # ADR-119 WD1 — the members `Scope::DiscoveryIndex::SIBLINGS` pairs, each with its sibling. A pair rides the
2287
+ # seed when either half is non-empty and is left out when both are, which keeps an `discovery_seed:` base's
2288
+ # table for a pair this run computed nothing for. {Scope::DiscoveryIndex#with} takes the two together, and
2289
+ # `compact_pairs` completes a half pair a base seed brought in.
2290
+ PAIRED_MEMBER_IVARS = Scope::DiscoveryIndex::SIBLINGS.keys.to_h do |member|
2291
+ [member, :"@project_#{member}"]
2292
+ end.freeze
2293
+ private_constant :PAIRED_MEMBER_IVARS
2294
+
2295
+ def seed_paired_tables(tables)
2296
+ PAIRED_MEMBER_IVARS.each do |member, ivar|
2297
+ sibling = Scope::DiscoveryIndex::SIBLINGS.fetch(member)
2298
+ table = instance_variable_get(ivar)
2299
+ own = @project_discovery_siblings.fetch(sibling)
2300
+ next if table.empty? && own.empty?
2301
+
2302
+ tables[member] = table
2303
+ tables[sibling] = own
2304
+ end
2007
2305
  end
2008
2306
 
2009
- # The tables only check rules read: the issue #992 parameter envelopes (`call.wrong-arity`), the issue #1120
2010
- # refinements (`call.undefined-method`), and the issue #1367 global-write census (the `global.*` write
2011
- # rules). Split out of {#project_scope_seed_tables} to keep it under the complexity budget.
2307
+ # The tables only check rules read: the issue #1120 refinements (`call.undefined-method`) and the issue #1367
2308
+ # global-write census (the `global.*` write rules); the issue #992 envelopes ride {#seed_paired_tables}. Split
2309
+ # out of {#project_scope_seed_tables} to keep it under the complexity budget.
2012
2310
  def seed_call_surface_tables(tables)
2013
- unless @project_discovered_parameter_envelopes.empty?
2014
- tables[:discovered_parameter_envelopes] = @project_discovered_parameter_envelopes
2015
- end
2016
- tables[:discovered_refinements] = @project_discovered_refinements unless @project_discovered_refinements.empty?
2311
+ refinements = seed_refinements(tables)
2312
+ tables[:discovered_refinements] = refinements unless refinements.nil? || refinements.empty?
2017
2313
  # Issue #1367 — read by the `global.*` write rules.
2018
2314
  return if @project_discovered_global_write_census.empty?
2019
2315
 
2020
2316
  tables[:discovered_global_write_census] = @project_discovered_global_write_census
2021
2317
  end
2022
2318
 
2319
+ # Issue #1672 — the project's refinements (this run's discovery, else an opt-in `discovery_seed:` base's)
2320
+ # unioned with the opt-in gems' ({DependencySourceInference::Index#refinements}). The gem half comes from
2321
+ # the pre-passes every run takes, warm or cold, so a file re-analysed warm is seeded what a cold run seeds.
2322
+ # The seed is rebuilt per file, so the union is kept while its two inputs are the same objects.
2323
+ def seed_refinements(tables)
2324
+ project = @project_discovered_refinements
2325
+ project = tables[:discovered_refinements] if project.empty?
2326
+ gems = @dependency_source_index.refinements
2327
+ memo = @seed_refinements
2328
+ return memo[2] if memo && memo[0].equal?(project) && memo[1].equal?(gems)
2329
+
2330
+ merged = Inference::ScopeIndexer.merge_refinement_tables(project, gems)
2331
+ @seed_refinements = [project, gems, merged]
2332
+ merged
2333
+ end
2334
+
2023
2335
  # The three mixin tables: the ADR-24 instance-side `include` map, its issue #1123 `prepend` twin
2024
2336
  # (which carries the instance-ancestor order the include table's set-shaped value cannot), and the
2025
2337
  # #898 singleton-side `extend` table. Grouped here because they are read as one picture of a class's
@@ -2028,12 +2340,14 @@ module Rigor
2028
2340
  tables[:discovered_includes] = @project_discovered_includes unless @project_discovered_includes.empty?
2029
2341
  tables[:discovered_prepends] = @project_discovered_prepends unless @project_discovered_prepends.empty?
2030
2342
  tables[:discovered_extends] = @project_discovered_extends unless @project_discovered_extends.empty?
2343
+ tables[:unpositioned_mixins] = @project_unpositioned_mixins unless @project_unpositioned_mixins.empty?
2031
2344
  end
2032
2345
 
2033
- # ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read only by the recording accessors
2346
+ # ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read by the recording accessors
2034
2347
  # (`Scope#record_class_dependency` for the class-declaration map, `Scope#record_constant_dependency`
2035
- # for the constant-write map). A normal run carries neither of the two. Extracted to keep
2036
- # {#project_scope_seed_tables} under the complexity budget.
2348
+ # for the constant-write map). The class-declaration map rides every run: an ancestor-order reader also
2349
+ # asks whether a class is declared in more than one file. The constant-write map rides only a recording
2350
+ # run. Extracted to keep {#project_scope_seed_tables} under the complexity budget.
2037
2351
  #
2038
2352
  # Issue #617 — the census's binding writes, grouped by last segment, ride every run instead: a constant
2039
2353
  # compound write whose plain read resolves nothing reads its binding off them, a TYPE rather than an edge.
@@ -2043,10 +2357,10 @@ module Rigor
2043
2357
  tables[:constant_writers] = writers unless writers.empty?
2044
2358
  shadowers = constant_shadower_index
2045
2359
  tables[:constant_shadowers] = shadowers unless shadowers.empty?
2046
- return unless @record_dependencies
2047
-
2048
2360
  tables[:discovered_class_sources] = @project_discovered_class_sources unless
2049
2361
  @project_discovered_class_sources.empty?
2362
+ return unless @record_dependencies
2363
+
2050
2364
  tables[:constant_sources] = @project_constant_sources unless @project_constant_sources.empty?
2051
2365
  end
2052
2366
 
@@ -2108,7 +2422,7 @@ module Rigor
2108
2422
  def memo_files_by_segment
2109
2423
  @project_constant_writes.each_with_object({}) do |(name, by_path), files|
2110
2424
  set = (files[name.split("::").last] ||= Set.new)
2111
- by_path.each { |path, descriptor| set << File.expand_path(path) if memo_descriptor?(descriptor) }
2425
+ by_path.each { |path, descriptor| set << File.absolute_path(path) if memo_descriptor?(descriptor) }
2112
2426
  end
2113
2427
  end
2114
2428
 
@@ -2166,14 +2480,14 @@ module Rigor
2166
2480
  writers = @project_constant_sources[name]
2167
2481
  wildcard = @project_constant_sources["#{Inference::ScopeIndexer::DYNAMIC_TARGET_PREFIX}#{segment}"]
2168
2482
  [writers, wildcard].any? do |paths|
2169
- paths&.any? { |path| !listed.include?(File.expand_path(path)) }
2483
+ paths&.any? { |path| !listed.include?(File.absolute_path(path)) }
2170
2484
  end
2171
2485
  end
2172
2486
 
2173
2487
  # The listed `pre_eval:` files, expanded so a census path and a configured one compare as the same
2174
2488
  # file whichever spelling each arrived in.
2175
2489
  def pre_eval_path_set
2176
- @pre_eval_path_set ||= @configuration.pre_eval.to_set { |path| File.expand_path(path) }
2490
+ @pre_eval_path_set ||= @configuration.pre_eval.to_set { |path| File.absolute_path(path) }
2177
2491
  end
2178
2492
 
2179
2493
  # ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
@@ -2203,11 +2517,18 @@ module Rigor
2203
2517
  def analyze_file(path, environment)
2204
2518
  return analyze_with_effects(path, environment) unless @record_dependencies
2205
2519
 
2520
+ # ADR-45 WD2 (#1507) — every plugin read this file's analysis makes is attributed to it, so the
2521
+ # incremental session can validate them on a later run that serves this file's answer from cache.
2522
+ @pre_analysis_plugin_rows ||= plugin_boundary_rows
2523
+ reads = []
2206
2524
  diagnostics = nil
2207
- record = DependencyRecorder.record_for(path) do
2208
- diagnostics = analyze_with_effects(path, environment)
2525
+ record = Plugin::IoBoundary.attributing(reads) do
2526
+ DependencyRecorder.record_for(path) do
2527
+ diagnostics = analyze_with_effects(path, environment)
2528
+ end
2209
2529
  end
2210
2530
  @file_dependencies[path] = record
2531
+ @analysis_reads[path] = reads unless reads.empty?
2211
2532
  diagnostics
2212
2533
  end
2213
2534
 
@@ -2312,7 +2633,8 @@ module Rigor
2312
2633
  self_call_misses: self_call_misses,
2313
2634
  comments: parse_result.comments,
2314
2635
  disabled_rules: @configuration.disabled_rules,
2315
- node_collectors: node_collectors
2636
+ node_collectors: node_collectors,
2637
+ stated_runtime_ruby: @configuration.stated_runtime_ruby
2316
2638
  )
2317
2639
  diagnostics + plugin_emitted_diagnostics(path, root, scope, node_results)
2318
2640
  end