rigortype 0.4.4 → 0.4.6

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 (233) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/docs/handbook/11-sig-gen.md +100 -18
  4. data/docs/manual/02-cli-reference.md +85 -16
  5. data/docs/manual/03-configuration.md +12 -9
  6. data/docs/manual/04-diagnostics.md +17 -6
  7. data/docs/manual/05-inspecting-types.md +2 -2
  8. data/docs/manual/06-baseline.md +1 -1
  9. data/docs/manual/07-plugins.md +2 -1
  10. data/docs/manual/08-skills.md +7 -0
  11. data/docs/manual/10-mcp-server.md +17 -2
  12. data/docs/manual/12-caching.md +8 -1
  13. data/docs/manual/18-removing-dead-code.md +18 -7
  14. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  15. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  16. data/docs/manual/plugins/rigor-devise.md +15 -0
  17. data/docs/manual/plugins/rigor-rspec.md +102 -1
  18. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  19. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  20. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  21. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  22. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
  23. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
  24. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  25. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  26. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  27. data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
  28. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  29. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
  30. data/lib/rigor/analysis/check_rules.rb +319 -21
  31. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/edit_replay.rb +67 -0
  34. data/lib/rigor/analysis/incremental.rb +86 -0
  35. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  36. data/lib/rigor/analysis/incremental_session.rb +336 -46
  37. data/lib/rigor/analysis/path_expansion.rb +44 -1
  38. data/lib/rigor/analysis/reachability/graph.rb +90 -13
  39. data/lib/rigor/analysis/reachability/scan.rb +279 -39
  40. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  41. data/lib/rigor/analysis/rule_catalog.rb +22 -5
  42. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  43. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  44. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  48. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  49. data/lib/rigor/analysis/runner.rb +222 -111
  50. data/lib/rigor/analysis/severity_stamp.rb +11 -0
  51. data/lib/rigor/analysis/worker_session.rb +24 -1
  52. data/lib/rigor/cache/descriptor.rb +40 -7
  53. data/lib/rigor/cache/engine_source.rb +3 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +49 -8
  55. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  56. data/lib/rigor/cache/store.rb +4 -3
  57. data/lib/rigor/cli/check_command.rb +93 -4
  58. data/lib/rigor/cli/command.rb +3 -1
  59. data/lib/rigor/cli/diff_command.rb +2 -1
  60. data/lib/rigor/cli/docs_command.rb +3 -2
  61. data/lib/rigor/cli/doctor_command.rb +1 -1
  62. data/lib/rigor/cli/explain_command.rb +3 -2
  63. data/lib/rigor/cli/lens_command.rb +5 -5
  64. data/lib/rigor/cli/mcp_command.rb +2 -1
  65. data/lib/rigor/cli/options.rb +20 -3
  66. data/lib/rigor/cli/plugin_command.rb +2 -1
  67. data/lib/rigor/cli/plugins_command.rb +7 -1
  68. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  69. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  70. data/lib/rigor/cli/sig_gen_command.rb +73 -13
  71. data/lib/rigor/cli/skill_command.rb +2 -1
  72. data/lib/rigor/cli/skill_describe.rb +3 -2
  73. data/lib/rigor/cli/type_of_command.rb +2 -1
  74. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  75. data/lib/rigor/cli/unused_command.rb +10 -7
  76. data/lib/rigor/cli.rb +3 -2
  77. data/lib/rigor/config_audit.rb +72 -16
  78. data/lib/rigor/configuration/severity_profile.rb +20 -2
  79. data/lib/rigor/configuration.rb +43 -7
  80. data/lib/rigor/effects/signature_sources.rb +3 -1
  81. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  82. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  83. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  84. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  85. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  86. data/lib/rigor/environment/rbs_loader.rb +136 -7
  87. data/lib/rigor/environment/required_features.rb +136 -29
  88. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  89. data/lib/rigor/environment.rb +4 -2
  90. data/lib/rigor/inference/block_call_timing.rb +7 -1
  91. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  92. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  93. data/lib/rigor/inference/captured_locals.rb +44 -13
  94. data/lib/rigor/inference/clause_widening.rb +90 -0
  95. data/lib/rigor/inference/deep_erasure.rb +138 -0
  96. data/lib/rigor/inference/def_return_typer.rb +159 -9
  97. data/lib/rigor/inference/definer_resolution.rb +101 -20
  98. data/lib/rigor/inference/error_info.rb +17 -3
  99. data/lib/rigor/inference/expression_typer.rb +97 -18
  100. data/lib/rigor/inference/global_write_census.rb +36 -1
  101. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  102. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  103. data/lib/rigor/inference/loop_value_record.rb +53 -0
  104. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  105. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  106. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
  107. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  108. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  109. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  110. data/lib/rigor/inference/method_dispatcher.rb +113 -19
  111. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  112. data/lib/rigor/inference/module_function_state.rb +149 -42
  113. data/lib/rigor/inference/mutation_widening.rb +5 -2
  114. data/lib/rigor/inference/narrowing.rb +116 -11
  115. data/lib/rigor/inference/object_mixins.rb +13 -1
  116. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  117. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  118. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  119. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  120. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  121. data/lib/rigor/inference/project_seed_tables.rb +121 -0
  122. data/lib/rigor/inference/project_yielder.rb +128 -0
  123. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  124. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  125. data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
  126. data/lib/rigor/inference/return_barrier.rb +16 -0
  127. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  128. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  129. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  130. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  131. data/lib/rigor/inference/scope_indexer.rb +1144 -197
  132. data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
  133. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  134. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  135. data/lib/rigor/inference/statement_evaluator.rb +1088 -213
  136. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  137. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  138. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  139. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  140. data/lib/rigor/language_server/buffer_table.rb +6 -0
  141. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  142. data/lib/rigor/language_server/project_context.rb +59 -3
  143. data/lib/rigor/language_server/server.rb +5 -3
  144. data/lib/rigor/lens/contract_reader.rb +45 -2
  145. data/lib/rigor/lens/document.rb +2 -1
  146. data/lib/rigor/lens/file_reader.rb +12 -2
  147. data/lib/rigor/lens/members.rb +16 -2
  148. data/lib/rigor/lens/run.rb +34 -7
  149. data/lib/rigor/mcp/server.rb +11 -3
  150. data/lib/rigor/plugin/base.rb +29 -13
  151. data/lib/rigor/plugin/io_boundary.rb +42 -6
  152. data/lib/rigor/plugin/isolation.rb +4 -3
  153. data/lib/rigor/plugin/manifest.rb +29 -3
  154. data/lib/rigor/plugin/registry.rb +38 -0
  155. data/lib/rigor/protection/discovery_seed.rb +64 -36
  156. data/lib/rigor/protection/mutation_cache.rb +8 -2
  157. data/lib/rigor/rooted_glob.rb +36 -0
  158. data/lib/rigor/scope/discovery_index.rb +5 -0
  159. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  160. data/lib/rigor/scope/resolution_chain.rb +40 -5
  161. data/lib/rigor/scope.rb +59 -7
  162. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  163. data/lib/rigor/sig_gen/classification.rb +4 -1
  164. data/lib/rigor/sig_gen/generator.rb +465 -49
  165. data/lib/rigor/sig_gen/layout_index.rb +5 -2
  166. data/lib/rigor/sig_gen/observation_collector.rb +141 -12
  167. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  168. data/lib/rigor/sig_gen/renderer.rb +16 -155
  169. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  170. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  171. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  172. data/lib/rigor/sig_gen/write_result.rb +3 -1
  173. data/lib/rigor/sig_gen/writer.rb +5 -3
  174. data/lib/rigor/signature_path_audit.rb +4 -2
  175. data/lib/rigor/type/combinator.rb +4 -3
  176. data/lib/rigor/type/constant.rb +54 -3
  177. data/lib/rigor/type/hash_shape.rb +14 -9
  178. data/lib/rigor/type/struct_instance.rb +3 -2
  179. data/lib/rigor/version.rb +1 -1
  180. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  181. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
  182. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  183. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  184. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  185. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  186. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  187. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  188. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  189. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  190. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  191. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  192. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  193. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  194. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  195. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  196. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  197. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
  198. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  199. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  200. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  201. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  202. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  203. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  204. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  205. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  206. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  207. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  208. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  209. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  210. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  211. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  212. data/sig/rigor/analysis/baseline.rbs +3 -2
  213. data/sig/rigor/effects/file_collection.rbs +5 -6
  214. data/sig/rigor/effects/registry.rbs +8 -6
  215. data/sig/rigor/effects/summary.rbs +6 -5
  216. data/sig/rigor/environment.rbs +5 -3
  217. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  218. data/sig/rigor/inference.rbs +6 -0
  219. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  220. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  221. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  222. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  223. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  224. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  225. data/sig/rigor/plugin/manifest.rbs +16 -18
  226. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  227. data/sig/rigor/plugin/registry.rbs +1 -1
  228. data/sig/rigor/rooted_glob.rbs +7 -0
  229. data/sig/rigor/scope.rbs +34 -52
  230. data/sig/rigor/type.rbs +0 -6
  231. data/skills/rigor-doctor/references/01-checks.md +23 -11
  232. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  233. metadata +32 -1
@@ -12,6 +12,8 @@ require_relative "../cache/file_digest"
12
12
  require_relative "../cache/incremental_snapshot"
13
13
  require_relative "run_cache_key"
14
14
  require_relative "path_expansion"
15
+ require_relative "require_relative_closure"
16
+ require_relative "run_disclosure_credit"
15
17
  require_relative "../plugin"
16
18
  require_relative "../plugin/source_rbs_synthesis_reporter"
17
19
  require_relative "../rbs_extended/reporter"
@@ -26,6 +28,7 @@ require_relative "../effects/identity"
26
28
  require_relative "../effects/propagator"
27
29
  require_relative "../inference/parameter_inference_collector"
28
30
  require_relative "../inference/pre_eval_constants"
31
+ require_relative "../inference/project_seed_tables"
29
32
  require_relative "../inference/scope_indexer"
30
33
  require_relative "../inference/synthetic_method_scanner"
31
34
  require_relative "../inference/project_patched_scanner"
@@ -74,7 +77,10 @@ module Rigor
74
77
  # this and serves reused files from it without re-stamping: a run-level row is regenerated
75
78
  # every run wherever it is positioned, and slicing the run's full stream by path cached the
76
79
  # file-positioned ones (`effect.annotations-unchecked`, `source-rbs-*`).
77
- :per_file_diagnostics
80
+ :per_file_diagnostics,
81
+ # PR #2286 review M1 — the ADR-16 index this run's pre-pass built; `IncrementalSession` digests it
82
+ # to tell when a class-body DSL edit moved what a served file's dispatch read.
83
+ :synthetic_method_index
78
84
 
79
85
  # Issues #796 / #794 — the two run-level rows a NARROWED run cannot re-derive from the files it
80
86
  # analyses: the `definition-build-failed` set (whose producer is the analysis's own demand, and #696
@@ -98,6 +104,23 @@ module Rigor
98
104
  pooled.empty? ? @file_dependencies : @file_dependencies.merge(pooled)
99
105
  end
100
106
 
107
+ # Issue #1553 — the plugin reads ({Plugin::IoBoundary.attributing}) each file's analysis made this run,
108
+ # `{path => [row]}`, reconciled from the sequential and pooled halves exactly as {#file_dependencies} is.
109
+ # Empty unless `record_dependencies: true`; a file whose analysis read nothing has no key. The incremental
110
+ # session keeps them beside each file's cached rows so a later recheck re-analyses the file once one of
111
+ # them goes stale.
112
+ def analysis_reads
113
+ pooled = @pool_coordinator.collected_analysis_reads
114
+ pooled.empty? ? @analysis_reads : @analysis_reads.merge(pooled)
115
+ end
116
+
117
+ # Issue #2020 — `{path => [run-disclosure record]}`, the run-scoped rows each analysed file's plugin hooks
118
+ # registered ({RunDisclosureCredit}), sequential and pooled alike; empty unless `record_dependencies:`.
119
+ def analysis_disclosures
120
+ pooled = @pool_coordinator.collected_analysis_disclosures
121
+ pooled.empty? ? @analysis_disclosures : @analysis_disclosures.merge(pooled)
122
+ end
123
+
101
124
  # ADR-103 — the run's propagated effect graph, or {Effects::EffectTable.empty} when collection did
102
125
  # not run. Reconciled from the sequential and pooled halves exactly as {#file_dependencies} is, then
103
126
  # closed by the post-pool fixpoint in {#assemble_run_diagnostics}.
@@ -283,10 +306,11 @@ module Rigor
283
306
  # worker continues to build its own Environment.
284
307
  # @param discovery_seed — issue #260 — opt-in cross-file discovery tables, keyed by
285
308
  # {Scope::DiscoveryIndex} slot name, seeded onto every per-file scope through
286
- # `project_scope_seed_tables`. The ONE deliberate exception to "a `prebuilt:` runner carries no
287
- # discovery tables": {Protection::DiagnosticOracle} threads the table set Tier 2's site filter already
288
- # judges anchors against, so a site admitted because a sibling-file class resolved is also a site the
289
- # oracle can kill at. nil (the default) leaves the prebuilt/LSP contract byte-identical.
309
+ # `project_scope_seed_tables`. A `prebuilt:` runner computes no discovery tables of its own, so this is
310
+ # how one learns the project: {Protection::DiagnosticOracle} threads the table set Tier 2's site filter
311
+ # already judges anchors against, and (issue #2147, ADR-124) the LSP's per-buffer publish threads
312
+ # `LanguageServer::ProjectContext#discovery_seed_for`, so a class another file declares is not an
313
+ # external ancestor there. nil (the default) seeds nothing.
290
314
  # @param no_tolerated_effects — ADR-103 WD1 / #385 — `rigor check --no-tolerated-effects`.
291
315
  # Judges effect envelopes as if `effects.tolerated:` were empty. A judgment-time switch only: the
292
316
  # run, its collection and its cache identity are unchanged.
@@ -298,7 +322,7 @@ module Rigor
298
322
  seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
299
323
  discovery_seed: nil, no_tolerated_effects: false,
300
324
  restored_run_level_rows: nil, served_per_file_diagnostics: nil, served_effect_collections: nil,
301
- on_file_indexed: nil)
325
+ served_run_disclosures: nil, on_file_indexed: nil)
302
326
  @configuration = configuration
303
327
  # ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
304
328
  # index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
@@ -316,6 +340,9 @@ module Rigor
316
340
  # effect graph this run closes (and the envelope pass judges) spans the whole project, not just the closure.
317
341
  # Never part of {#effect_collections_by_path}, which stays what THIS run collected. Empty on every other run.
318
342
  @served_effect_collections = served_effect_collections || {}
343
+ # Issue #2020 — the run-scoped plugin rows ({RunDisclosureCredit}) those unanalysed files registered when
344
+ # they were last analysed, emitted beside this run's own registrations. Empty on every other run.
345
+ @served_run_disclosures = served_run_disclosures || []
319
346
  @explain = explain
320
347
  @cache_store = enforce_read_only_cache(cache_store, buffer)
321
348
  @plugin_requirer = plugin_requirer
@@ -371,6 +398,7 @@ module Rigor
371
398
  # rows as they stood around the per-file loop; see {#incremental_slot_rows}. Recorded only by a
372
399
  # dependency-recording (incremental) run.
373
400
  @analysis_reads = {}
401
+ @analysis_disclosures = {}
374
402
  @pre_analysis_plugin_rows = nil
375
403
  @post_analysis_plugin_rows = nil
376
404
  # Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
@@ -464,6 +492,9 @@ module Rigor
464
492
  @project_struct_member_layouts = {}.freeze
465
493
  # Issue #684 — set per run by {#project_discovery_expansion}; nil on every path that does not widen.
466
494
  @project_discovery_expansion = nil
495
+ # Issue #1583 — whether this run widened its discovery to the configured paths, even when the widening
496
+ # added no file; {#discovery_glob_entries} lists the configured roots for every such run.
497
+ @discovery_widened = false
467
498
  # ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
468
499
  # `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
469
500
  # scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
@@ -507,6 +538,16 @@ module Rigor
507
538
  end
508
539
  end
509
540
 
541
+ # ADR-45 WD2 (#1507) / #2020 — the per-run reset of what each file's analysis is credited with: its plugin
542
+ # reads, its run-scoped plugin registrations, and the boundary rows around the per-file loop.
543
+ def reset_analysis_attribution
544
+ @analysis_reads = {}
545
+ @analysis_disclosures = {}
546
+ @pre_analysis_plugin_rows = nil
547
+ @post_analysis_plugin_rows = nil
548
+ end
549
+ private :reset_analysis_attribution
550
+
510
551
  def run_analysis(paths)
511
552
  Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths =
512
553
  @configuration.fold_platform_specific_paths
@@ -518,6 +559,7 @@ module Rigor
518
559
 
519
560
  expansion = expand_paths(paths)
520
561
  @project_discovery_expansion = project_discovery_expansion(paths, expansion)
562
+ @discovery_widened = widen_discovery_to_project?(paths)
521
563
  @snapshots.reset_for_run
522
564
  # Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
523
565
  @project_discovery_done = false
@@ -533,9 +575,7 @@ module Rigor
533
575
  # #788 — the per-file reader is assigned only on the analysis (miss) path; reset it here so a run the
534
576
  # ADR-45 result cache serves does not answer with the previous run's rows.
535
577
  @per_file_diagnostics = [].freeze
536
- @analysis_reads = {}
537
- @pre_analysis_plugin_rows = nil
538
- @post_analysis_plugin_rows = nil
578
+ reset_analysis_attribution
539
579
  # ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
540
580
  # of THIS run share one memo bucket while entries from any earlier run in this process (stale after
541
581
  # an edit) become unreachable.
@@ -599,6 +639,19 @@ module Rigor
599
639
  expand_paths(paths).fetch(:files)
600
640
  end
601
641
 
642
+ # Issue #1583 — the project files a run over `paths` would DISCOVER without analysing them (#684's
643
+ # widening), by globbing only. Empty whenever the run would not widen, which includes every run over
644
+ # the configured `paths:`. {IncrementalSession} digests them, because an edit to one moves the
645
+ # diagnostics of the files it does analyse.
646
+ def discovery_only_file_set(paths = @configuration.paths)
647
+ expansion = expand_paths(paths)
648
+ widened = project_discovery_expansion(paths, expansion)
649
+ return [] if widened.nil?
650
+
651
+ analysed = expansion.fetch(:files).to_set
652
+ widened.fetch(:files).reject { |path| analysed.include?(path) }
653
+ end
654
+
602
655
  # ADR-46 §2 — inverts {#file_dependencies} into the reverse edge the incremental step walks:
603
656
  # `dependents[X] = { A : A read a declaration / body from X }`. On an edit to X, the body tier
604
657
  # (slice 2) re-analyses `{X} ∪ dependents[X]` and serves every other file from the per-file cache.
@@ -791,6 +844,7 @@ module Rigor
791
844
  @project_discovery_done = false
792
845
  # This entry point supplies its own expansion; it never widens (see {#project_discovery_expansion}).
793
846
  @project_discovery_expansion = nil
847
+ @discovery_widened = false
794
848
  @run_generation = Object.new.freeze
795
849
  run_project_pre_passes(expansion: expansion)
796
850
  ensure_project_discovery(expansion)
@@ -1385,13 +1439,13 @@ module Rigor
1385
1439
  end
1386
1440
 
1387
1441
  def build_run_dependency_descriptor(expansion, rbs_descriptor)
1388
- entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
1442
+ entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) + required_missing_entries +
1389
1443
  pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files +
1390
1444
  root_presence_entries(expansion)
1391
1445
  # #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
1392
1446
  # `.rbs` files that existed while the run read them, so without these a signature file written after
1393
1447
  # the run left the slot validating fresh and the warm run answered without it.
1394
- globs = rbs_descriptor.globs + template_unit_glob_entries
1448
+ globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries
1395
1449
  boundary = plugin_boundary_rows
1396
1450
  entries.concat(boundary.files)
1397
1451
  globs.concat(boundary.globs).concat(producer_watch_glob_entries)
@@ -1406,7 +1460,7 @@ module Rigor
1406
1460
  # auto-detected `sig`: the loader reads a directory and a file differently, and #979's listing rows cover a
1407
1461
  # file appearing under a root that existed, not the root appearing. A kind row rather than an existence row,
1408
1462
  # because a directory replaced by a file of the same name leaves an existence row fresh. The `--incremental`
1409
- # slot records existence rows for the same paths ({#incremental_slot_rows}, {#baseline_dependency_rows}).
1463
+ # slot records kind rows for the same paths ({#incremental_slot_rows}, {#baseline_dependency_rows}, #1977).
1410
1464
  def root_presence_entries(expansion)
1411
1465
  (expansion.fetch(:roots) + @configuration.pre_eval + signature_root_candidates)
1412
1466
  .uniq.map { |path| Cache::Descriptor::FileEntry.kind(path: path) }
@@ -1452,11 +1506,11 @@ module Rigor
1452
1506
  # globs, which a producer replays into its boundary whether it computed or was served (#1558). A save
1453
1507
  # after the read leaves the row stale, so no guard is needed.
1454
1508
  # - `derived` — rows taken when the run ENDS: the template files and their claimed globs (#392, re-analysed
1455
- # every run), and one existence row per analysis root and per `pre_eval:` entry. A run's path errors (`no
1456
- # such file or directory`, `pre-eval.file-not-found`) answer to those last rows; the plain slot records
1457
- # neither, and the incremental path regenerates the errors every run, so a slot that served one past the
1458
- # edit that retracts it would print what no run of that tree prints. Existence rows carry ADR-45 WD1b's
1459
- # bound: a directory replaced by a file of the same name stays fresh.
1509
+ # every run), and one `:kind` row per analysis root and per `pre_eval:` entry. A run's path errors (`no
1510
+ # such file or directory`, `not a Ruby file`, `pre-eval.file-not-found`) answer to those last rows, and the
1511
+ # incremental path regenerates the errors every run, so a slot that served one past the edit that retracts
1512
+ # it would print what no run of that tree prints. A kind row rather than an existence row (#1977): an
1513
+ # existence row reads a directory replaced by a file of the same name as fresh, ADR-45 WD1b's bound.
1460
1514
  # - `by_file` — `{path => Descriptor}`, the plugin reads each file's analysis made
1461
1515
  # ({Plugin::IoBoundary.attributing}). A narrowed run analyses only its closure, so the session carries the
1462
1516
  # other files' reads forward from the previous slot.
@@ -1480,7 +1534,8 @@ module Rigor
1480
1534
  IncrementalSlotRows.new(
1481
1535
  observed: plugin_boundary_rows,
1482
1536
  derived: Cache::Descriptor.new(
1483
- files: template_unit_file_entries + existence_entries(Array(roots) + @configuration.pre_eval),
1537
+ files: template_unit_file_entries + kind_entries(Array(roots) + @configuration.pre_eval) +
1538
+ required_missing_entries,
1484
1539
  globs: template_unit_glob_entries
1485
1540
  ),
1486
1541
  by_file: @analysis_reads.transform_values do |reads|
@@ -1507,13 +1562,14 @@ module Rigor
1507
1562
  # which the session therefore carries unchanged from the full run that recorded them to every later slot:
1508
1563
  #
1509
1564
  # - the signature tree the plain slot records (`RbsDescriptor.file_entries` / `.glob_entries` over the loader
1510
- # roots this run's analysis resolved), and an existence row per signature root, configured or the
1511
- # auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty, and the snapshot
1512
- # fingerprint digests the same roots (configured, or the auto-detected `sig`), so a change to one drops the
1513
- # snapshot and forces a full run rather than reaching a recheck;
1565
+ # roots this run's analysis resolved), and a `:kind` row per signature root, configured or the
1566
+ # auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty. The snapshot
1567
+ # fingerprint digests the same roots, so a change to one makes the next full-path run a full run rather
1568
+ # than a recheck, but the engine-free slot probe serves before any fingerprint is consulted: a root that
1569
+ # turns from a file into a directory (or back) is declined there by this `:kind` row alone (#1977);
1514
1570
  # - the discovered-not-analysed files (#684) and a listing row per discovery root — a narrowed recheck does
1515
1571
  # not widen discovery at all;
1516
- # - every `pre_eval:` file outside `files` (a stat row, or an absence row) — the session tracks only analysed
1572
+ # - every `pre_eval:` file outside `files` (a stat row, or a kind row) — the session tracks only analysed
1517
1573
  # files, and one that is analysed is guarded by its own row.
1518
1574
  #
1519
1575
  # Recomputing any of these on a recheck would vouch for the tree the recheck saw while its answer still
@@ -1537,8 +1593,9 @@ module Rigor
1537
1593
  own, pinned = signature_rows
1538
1594
  BaselineRows.new(
1539
1595
  owned: Cache::Descriptor.new(
1540
- files: own.files + existence_entries(signature_root_candidates) +
1541
- discovery_file_entries({ files: files }) + unanalysed_pre_eval_entries(files),
1596
+ files: own.files + kind_entries(signature_root_candidates) +
1597
+ discovery_file_entries({ files: files }) + required_missing_entries +
1598
+ unanalysed_pre_eval_entries(files),
1542
1599
  globs: (own.globs + discovery_glob_entries).uniq
1543
1600
  ),
1544
1601
  pinned: pinned
@@ -1563,16 +1620,18 @@ module Rigor
1563
1620
 
1564
1621
  # The project's own signature roots, whether or not they exist: the configured `signature_paths:`, or the
1565
1622
  # `sig` directory `Environment.default_signature_paths` auto-detects when none are configured. The
1566
- # signature rows' listing rows cover a file appearing under a root that existed; only an existence row
1567
- # covers the root itself appearing.
1623
+ # signature rows' listing rows cover a file appearing under a root that existed; only a row for the root
1624
+ # itself covers it appearing.
1568
1625
  def signature_root_candidates
1569
1626
  @configuration.signature_paths || ["sig"]
1570
1627
  end
1571
1628
 
1572
1629
  # One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
1573
- # as well as an edit to one that existed.
1630
+ # as well as an edit to one that existed. Issue #1583 — keyed on the widening decision, not on what it
1631
+ # added: a run over `lib/b.rb` alone in `lib` widens to nothing, and a `lib/a.rb` created afterwards must
1632
+ # still move the answer of the run-result cache and the `--incremental` slot.
1574
1633
  def discovery_glob_entries
1575
- return [] if @project_discovery_expansion.nil?
1634
+ return [] unless @discovery_widened
1576
1635
 
1577
1636
  @configuration.paths.select { |root| File.directory?(root) }.map do |root|
1578
1637
  Cache::Descriptor::GlobEntry.compute(root: root, pattern: PathExpansion::RUBY_GLOB, mode: :names)
@@ -1586,22 +1645,18 @@ module Rigor
1586
1645
  if File.file?(path)
1587
1646
  Cache::Descriptor::FileEntry.stat(path: path, digest: Cache::FileDigest.hexdigest(path))
1588
1647
  else
1589
- existence_entries([path]).first
1648
+ kind_entries([path]).first
1590
1649
  end
1591
1650
  end
1592
1651
  end
1593
1652
 
1594
- def existence_entries(paths)
1653
+ # #1977 — one `:kind` row per path, the row {#root_presence_entries} records in the plain slot.
1654
+ def kind_entries(paths)
1595
1655
  paths.uniq.map do |path|
1596
- physical = @buffer ? @buffer.resolve(path) : path
1597
- if File.exist?(physical)
1598
- Cache::Descriptor::FileEntry.present(path: physical)
1599
- else
1600
- Cache::Descriptor::FileEntry.absent(path: physical)
1601
- end
1656
+ Cache::Descriptor::FileEntry.kind(path: @buffer ? @buffer.resolve(path) : path)
1602
1657
  end
1603
1658
  end
1604
- private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries, :root_presence_entries,
1659
+ private :plugin_boundary_rows, :producer_watch_glob_entries, :kind_entries, :root_presence_entries,
1605
1660
  :analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
1606
1661
  :unanalysed_pre_eval_entries, :signature_rows, :within?
1607
1662
 
@@ -1780,20 +1835,19 @@ module Rigor
1780
1835
  end
1781
1836
 
1782
1837
  # Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
1783
- # Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt` — the
1784
- # LSP path deliberately seeds an empty project scope from a snapshot that carries no discovery tables,
1785
- # so forcing a build there would change that contract. A caller that DOES want cross-file knowledge under
1786
- # `prebuilt:` supplies it explicitly through `discovery_seed:` (issue #260) rather than by re-walking the
1787
- # project here. Called eagerly from `#run` for the recording /
1788
- # subset (ADR-46) modes and lazily from `#assemble_run_diagnostics` on the analysis path, so a warm
1789
- # cache HIT (which never assembles) never pays the double parse.
1838
+ # Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt`: the
1839
+ # snapshot carries no discovery tables, and a per-request re-walk of the project is what `prebuilt:` exists
1840
+ # to avoid. A caller that wants cross-file knowledge under `prebuilt:` supplies it through `discovery_seed:`
1841
+ # (issue #260), as the LSP publish does since issue #2147 (ADR-124). Called eagerly from `#run` for the
1842
+ # recording / subset (ADR-46) modes and lazily from `#assemble_run_diagnostics` on the analysis path, so a
1843
+ # warm cache HIT (which never assembles) never pays the double parse.
1790
1844
  def ensure_project_discovery(expansion)
1791
1845
  return if @prebuilt
1792
1846
  return if @project_discovery_done
1793
1847
 
1794
1848
  @project_discovery_done = true
1795
1849
  # Issue #684 — the cross-file index spans the PROJECT even when the analysis targets a file list.
1796
- expansion = @project_discovery_expansion || once_each(expansion)
1850
+ expansion = buffer_spelled(@project_discovery_expansion || once_each(expansion))
1797
1851
  if @collect_seed_bundles
1798
1852
  # ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
1799
1853
  # capture the refreshed bundle set for the session to persist.
@@ -1825,23 +1879,71 @@ module Rigor
1825
1879
  # on what else was on the command line.
1826
1880
  #
1827
1881
  # Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
1828
- # seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file, and
1829
- # ADR-46's `analyze_only` subset mode (which already passes the full expansion and filters later).
1882
+ # seeds discovery from a snapshot, ADR-124) and `run_source`'s in-memory single file.
1883
+ #
1884
+ # Issue #2165 — editor `buffer:` mode (`rigor check --tmp-file T --instead-of F F`) widens like a plain run.
1885
+ # It was excluded on the reading that its expansion is already the one it means, but an editor passes the
1886
+ # buffer's own path, so discovery saw that file alone: every class another file declares read as an external
1887
+ # ancestor, and the run reported what `rigor check F` does not. The discovery pre-pass reads through the
1888
+ # binding, so the buffer's file is walked from the buffer's bytes.
1889
+ #
1890
+ # Issue #1583 — ADR-46's `analyze_only` subset mode widens like a full run. It was excluded on the
1891
+ # reading that it "already passes the full expansion", which holds only when the incremental session
1892
+ # runs over the configured `paths:`. `rigor check --incremental lib/b.rb` passes the argument's
1893
+ # expansion, so its rechecks (and `--verify-incremental`'s partitions) discovered the argument alone
1894
+ # while its baseline discovered the project, and `lib/b.rb`'s report on a sibling's constant came and
1895
+ # went with the run kind.
1830
1896
  #
1831
1897
  # Issue #1556 — the widened set holds each FILE once. `.rigor.yml`'s `paths:` load as absolute paths and a
1832
1898
  # path argument usually does not, so `rigor check lib` widened discovery over both spellings of every file:
1833
1899
  # the pre-pass then saw two writers for every constant a file declares and withheld its publication (#644),
1834
1900
  # and every file was a discovered-not-analysed dependency of the run's cache entry.
1835
1901
  #
1902
+ # Issue #2021 — an analysed file outside the configured paths has no project there, so the widened set also
1903
+ # holds what it loads with a literal `require_relative` ({#required_outside_paths}). A plugin file checked on
1904
+ # its own otherwise missed its sibling's nested `Effects` and resolved the project's `Rigor::Effects`.
1905
+ #
1836
1906
  # The widening decision itself compares the paths as spelled. `exclude:` patterns apply to both spellings
1837
1907
  # alike (#1576), so the configured absolute `lib` and the argument `lib` exclude the same files.
1838
1908
  def project_discovery_expansion(paths, expansion)
1839
1909
  return nil unless widen_discovery_to_project?(paths)
1840
1910
 
1841
1911
  analysed = expansion.fetch(:files)
1842
- files = discovery_files(expand_paths(@configuration.paths | paths).fetch(:files), analysed)
1912
+ project = expand_paths(@configuration.paths | paths).fetch(:files)
1913
+ walked = required_outside_paths(analysed)
1914
+ files = discovery_files(project + walked.found, analysed)
1843
1915
  # Overlapping arguments (`lib lib/user.rb`) name an analysed file twice; the widened set names it once.
1844
- files.size > analysed.uniq { |path| File.absolute_path(path) }.size ? { files: files, errors: [] } : nil
1916
+ return nil unless files.size > analysed.uniq { |path| File.absolute_path(path) }.size || walked.missing.any?
1917
+
1918
+ { files: files, errors: [], uncertain: walked.found.to_set.freeze, missing: walked.missing }
1919
+ end
1920
+
1921
+ # Issue #2021 — what the analysed files outside the configured `paths:` load with a literal `require_relative`,
1922
+ # through targets that are themselves outside them ({RequireRelativeClosure}). A file under a configured path
1923
+ # is discovered already, so the walk adds nothing for a run whose files all lie inside them. The comparison is
1924
+ # by real path, so a symlinked spelling of a configured file does not join discovery a second time (two
1925
+ # writers would withhold its constants, #644). `exclude:` drops a target from discovery as it drops a
1926
+ # configured file; the walk still follows what an excluded target requires.
1927
+ #
1928
+ # The found files are discovered as `uncertain` ({Analysis::Runner::ProjectPrePasses#uncertain_paths}): the walk
1929
+ # sees one entry point's load graph, and a file it does not reach may redefine what they define.
1930
+ def required_outside_paths(analysed)
1931
+ roots = @configuration.paths.map { |root| RequireRelativeClosure.real_path(root) }
1932
+ covered = ->(real) { roots.any? { |root| real == root || real.start_with?(File.join(root, "")) } }
1933
+ seeds = analysed.reject { |path| covered.call(RequireRelativeClosure.real_path(path)) }
1934
+ return RequireRelativeClosure::Result.new(found: [], missing: []) if seeds.empty?
1935
+
1936
+ read = ->(path) { File.read(@buffer ? @buffer.resolve(path) : path) }
1937
+ walked = RequireRelativeClosure.closure(seeds, covered: covered, read: read)
1938
+ walked.with(found: PathExpansion.reject_excluded(walked.found, @configuration.exclude_patterns,
1939
+ base: @configuration.exclude_base))
1940
+ end
1941
+
1942
+ # Issue #2021 — a kind row per `require_relative` target the walk named and found absent: a file appearing
1943
+ # there joins the discovery, so it moves the answer of the run-result cache and the `--incremental` slot.
1944
+ def required_missing_entries
1945
+ missing = @project_discovery_expansion&.fetch(:missing, nil)
1946
+ missing.nil? || missing.empty? ? [] : kind_entries(missing)
1845
1947
  end
1846
1948
 
1847
1949
  # The expansion with each file once, first spelling kept: overlapping arguments (`lib ./lib`) that do not
@@ -1853,6 +1955,23 @@ module Rigor
1853
1955
  unique.size == files.size ? expansion : expansion.merge(files: unique)
1854
1956
  end
1855
1957
 
1958
+ # Issue #2165 — the discovery set with the buffer's file under the binding's own spelling, once. The binding
1959
+ # substitutes bytes only for a path spelled exactly as its logical path, while the file can reach discovery as
1960
+ # the argument's spelling, the configured one, or through a symlink; each of those read it from disk, and a
1961
+ # second spelling made it a second declarer. Every member whose real path is the buffer's file is replaced by
1962
+ # the logical path, and the duplicates that leaves are dropped (`IncrementalSession#rebind_buffer` is option
1963
+ # B's counterpart).
1964
+ def buffer_spelled(expansion)
1965
+ return expansion if @buffer.nil?
1966
+
1967
+ target = Analysis::BufferBinding.real_path(@buffer.logical_path)
1968
+ files = expansion.fetch(:files)
1969
+ spelled = files.map do |path|
1970
+ Analysis::BufferBinding.real_path(path) == target ? @buffer.logical_path : path
1971
+ end.uniq
1972
+ spelled == files ? expansion : expansion.merge(files: spelled)
1973
+ end
1974
+
1856
1975
  # Each file once, in project order, under the spelling the analysis uses when it is analysed.
1857
1976
  def discovery_files(project_files, analysed_files)
1858
1977
  spelling = analysed_files.to_h { |path| [File.absolute_path(path), path] }
@@ -1864,8 +1983,7 @@ module Rigor
1864
1983
  end
1865
1984
 
1866
1985
  def widen_discovery_to_project?(paths)
1867
- @prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && @analyze_only.nil? &&
1868
- paths != @configuration.paths
1986
+ @prebuilt.nil? && @in_memory_sources.nil? && paths != @configuration.paths
1869
1987
  end
1870
1988
 
1871
1989
  # ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
@@ -1879,7 +1997,8 @@ module Rigor
1879
1997
 
1880
1998
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
1881
1999
  :apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?,
1882
- :project_discovery_expansion, :discovery_files, :once_each, :widen_discovery_to_project?
2000
+ :project_discovery_expansion, :discovery_files, :once_each, :widen_discovery_to_project?,
2001
+ :required_outside_paths, :required_missing_entries
1883
2002
 
1884
2003
  # Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
1885
2004
  # `version:`. Prism exposes no version list, so the floor is found empirically — only when a
@@ -2069,6 +2188,7 @@ module Rigor
2069
2188
  hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
2070
2189
  conformance_results_snapshot: -> { @snapshots.conformance_results },
2071
2190
  pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures },
2191
+ served_run_disclosures: -> { @served_run_disclosures },
2072
2192
  pooled_trust_refusals: -> { @pool_coordinator.collected_trust_refusals }
2073
2193
  )
2074
2194
  end
@@ -2216,10 +2336,8 @@ module Rigor
2216
2336
  # client editing a brand-new file.
2217
2337
  elsif accept_as_ruby_file?(path)
2218
2338
  files << path
2219
- elsif File.exist?(path)
2220
- bad << [path, "not a Ruby file (expected `.rb` or a directory)"]
2221
2339
  else
2222
- bad << [path, "no such file or directory"]
2340
+ bad << [path, PathExpansion.unusable_message(path)]
2223
2341
  end
2224
2342
  end
2225
2343
  { files: files, roots: Array(paths).map(&:to_s), errors: path_expansion_errors(bad, any_files: files.any?) }
@@ -2229,27 +2347,24 @@ module Rigor
2229
2347
  # app lib` with no `lib/` (the 20260620 field trial's strap case) analyses `app` instead of aborting
2230
2348
  # on exit 1. A bad path that leaves NOTHING to analyse stays an error so a lone typo (`rigor check
2231
2349
  # typo.rb`) is not silently masked (the regression the path-error check was added to catch in the
2232
- # first place).
2350
+ # first place). The wording and grading live in {PathExpansion.diagnostics}, shared with `sig-gen` (#1991).
2233
2351
  def path_expansion_errors(bad, any_files:)
2234
- severity = any_files ? :warning : :error
2235
- suffix = any_files ? " (skipped)" : ""
2236
- bad.map { |path, message| path_error(path, "#{message}#{suffix}", severity: severity) }
2352
+ PathExpansion.diagnostics(bad, any_files: any_files)
2237
2353
  end
2238
2354
 
2355
+ # #2191 — the buffer's logical path is accepted under any spelling that names the same file (relative, absolute,
2356
+ # or through a symlinked root), since the discovery set respells it to the logical path (`buffer_spelled`).
2239
2357
  def accept_as_ruby_file?(path)
2240
2358
  (File.file?(path) && path.end_with?(".rb")) ||
2241
- (@buffer && path == @buffer.logical_path) ||
2359
+ buffer_path?(path) ||
2242
2360
  @in_memory_sources&.key?(path)
2243
2361
  end
2244
2362
 
2245
- def path_error(path, message, severity: :error)
2246
- Diagnostic.new(
2247
- path: path,
2248
- line: 1,
2249
- column: 1,
2250
- message: message,
2251
- severity: severity
2252
- )
2363
+ def buffer_path?(path)
2364
+ return false if @buffer.nil?
2365
+
2366
+ path == @buffer.logical_path ||
2367
+ Analysis::BufferBinding.real_path(path) == Analysis::BufferBinding.real_path(@buffer.logical_path)
2253
2368
  end
2254
2369
 
2255
2370
  # Reads + parses the source at `path`. Under editor mode (`@buffer` set) reads bytes from
@@ -2425,50 +2540,21 @@ module Rigor
2425
2540
  tables[:published_constant_names] = names unless names.empty?
2426
2541
  end
2427
2542
 
2428
- # Issue #617 — the censused names that bind, grouped by last segment (`Scope#bound_constant_names`). A
2429
- # name some write other than a memo `||=` touches always binds. A memo-only name binds once memos of its
2430
- # segment span two files: either may load first and set the constant the other then reads, so only a
2431
- # memo no other file shares keeps the idiom's reading. Files are counted expanded, because discovery can
2432
- # walk one file under two spellings (`lib/b.rb`, `./lib/b.rb`).
2543
+ # Issue #617 / #1290 — the binding and shadowing constant names, by last segment; derived by
2544
+ # {Inference::ProjectSeedTables}, which the seed paths that run no `check` share.
2433
2545
  def constant_writer_index
2434
- @constant_writer_index ||= begin
2435
- memo_files = memo_files_by_segment
2436
- @project_constant_writes.each_with_object({}) do |(name, by_path), index|
2437
- segment = name.split("::").last
2438
- next if memo_only?(by_path) && memo_files.fetch(segment).size < 2
2439
-
2440
- (index[segment] ||= []) << name
2441
- end.each_value(&:freeze).freeze
2442
- end
2546
+ @constant_writer_index ||= Inference::ProjectSeedTables.constant_writers(@project_constant_writes)
2443
2547
  end
2444
2548
 
2445
- # Issue #1290 — the censused names some write other than a memo `||=` assigns, grouped by last segment
2446
- # (`Scope#shadowing_constant_names`): the ones that exist whether or not an outer constant of the name
2447
- # does, and so stop the lexical ladder above it. A memo-only name is left out however many files memoize
2448
- # it, unlike in {#constant_writer_index}: `X ||= v` resolves `X` through the very lookup whose lower rung
2449
- # the stop would replace, so wherever that rung answers, the memo finds it and never creates the name.
2450
2549
  def constant_shadower_index
2451
- @constant_shadower_index ||= @project_constant_writes.each_with_object({}) do |(name, by_path), index|
2452
- (index[name.split("::").last] ||= []) << name unless memo_only?(by_path)
2453
- end.each_value(&:freeze).freeze
2550
+ @constant_shadower_index ||= Inference::ProjectSeedTables.constant_shadowers(@project_constant_writes)
2454
2551
  end
2455
2552
 
2456
- def memo_files_by_segment
2457
- @project_constant_writes.each_with_object({}) do |(name, by_path), files|
2458
- set = (files[name.split("::").last] ||= Set.new)
2459
- by_path.each { |path, descriptor| set << File.absolute_path(path) if memo_descriptor?(descriptor) }
2460
- end
2461
- end
2462
-
2463
- def memo_only?(by_path) = by_path.each_value.all? { |descriptor| memo_descriptor?(descriptor) }
2464
- def memo_descriptor?(descriptor) = descriptor == Inference::ScopeIndexer::CONSTANT_MEMO
2465
-
2466
2553
  # Issue #644 — the LAST SEGMENTS of the published table, the run-wide half of
2467
2554
  # `Scope#published_constant?`. Seeded on EVERY run (unlike `constant_sources`, which only a recording
2468
2555
  # run needs) because {CheckRules::PublishedConstantGuard} asks the question on every `if` / `unless`.
2469
2556
  def published_constant_name_set
2470
- @published_constant_name_set ||=
2471
- @project_constant_values.keys.to_set { |name| name.split("::").last }.freeze
2557
+ @published_constant_name_set ||= Inference::ProjectSeedTables.published_constant_names(@project_constant_values)
2472
2558
  end
2473
2559
 
2474
2560
  # Issue #644 — the seeded `in_source_constants` table, joining the two cross-file constant producers.
@@ -2477,12 +2563,31 @@ module Rigor
2477
2563
  # widener erases it to the class, and listing a file under `pre_eval:` must never LOSE precision. The
2478
2564
  # per-file table still wins over both (`ScopeIndexer.index`'s merge) — same-file stays the most
2479
2565
  # specific authority.
2566
+ #
2567
+ # ADR-124 — a `prebuilt:` runner computes no literal table of its own; a `discovery_seed:` (the editor
2568
+ # publish, ADR-124) brings the project's, which then plays the same part.
2480
2569
  def merged_seed_constants
2481
2570
  pre_eval = publishable_pre_eval_constants
2482
- return @project_constant_values if pre_eval.empty?
2483
- return pre_eval if @project_constant_values.empty?
2571
+ literal = literal_seed_constants
2572
+ return literal if pre_eval.empty?
2573
+ return pre_eval if literal.empty?
2574
+
2575
+ pre_eval.merge(literal).freeze
2576
+ end
2577
+
2578
+ # The project's literal constant table: this run's, or the one a `discovery_seed:` carries (ADR-124).
2579
+ def literal_seed_constants
2580
+ return @project_constant_values unless @project_constant_values.empty? && @discovery_seed
2581
+
2582
+ @discovery_seed.fetch(:in_source_constants, @project_constant_values)
2583
+ end
2584
+
2585
+ # The per-name writer paths the #663 retraction reads: this run's census, or the one a `discovery_seed:` carries
2586
+ # (ADR-124).
2587
+ def constant_writer_sources
2588
+ return @project_constant_sources unless @project_constant_sources.empty? && @discovery_seed
2484
2589
 
2485
- pre_eval.merge(@project_constant_values).freeze
2590
+ @discovery_seed.fetch(:constant_sources, @project_constant_sources)
2486
2591
  end
2487
2592
 
2488
2593
  # Issue #663 — the `pre_eval:` publication with every name an UNLISTED file also assigns retracted.
@@ -2511,8 +2616,9 @@ module Rigor
2511
2616
  # name of its own for the wildcard's paths to have been folded into.
2512
2617
  def unlisted_constant_writer?(name, listed)
2513
2618
  segment = name.split("::").last
2514
- writers = @project_constant_sources[name]
2515
- wildcard = @project_constant_sources["#{Inference::ScopeIndexer::DYNAMIC_TARGET_PREFIX}#{segment}"]
2619
+ sources = constant_writer_sources
2620
+ writers = sources[name]
2621
+ wildcard = sources["#{Inference::ScopeIndexer::DYNAMIC_TARGET_PREFIX}#{segment}"]
2516
2622
  [writers, wildcard].any? do |paths|
2517
2623
  paths&.any? { |path| !listed.include?(File.absolute_path(path)) }
2518
2624
  end
@@ -2556,13 +2662,18 @@ module Rigor
2556
2662
  @pre_analysis_plugin_rows ||= plugin_boundary_rows
2557
2663
  reads = []
2558
2664
  diagnostics = nil
2559
- record = Plugin::IoBoundary.attributing(reads) do
2560
- DependencyRecorder.record_for(path) do
2561
- diagnostics = analyze_with_effects(path, environment)
2665
+ record = nil
2666
+ # Issue #2020 — and every run-scoped row its analysis registered, with the registering plugins' reads.
2667
+ disclosures = RunDisclosureCredit.collect do
2668
+ record = Plugin::IoBoundary.attributing(reads) do
2669
+ DependencyRecorder.record_for(path) do
2670
+ diagnostics = analyze_with_effects(path, environment)
2671
+ end
2562
2672
  end
2563
2673
  end
2564
2674
  @file_dependencies[path] = record
2565
2675
  @analysis_reads[path] = reads unless reads.empty?
2676
+ @analysis_disclosures[path] = disclosures unless disclosures.empty?
2566
2677
  diagnostics
2567
2678
  end
2568
2679