rigortype 0.4.5 → 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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/handbook/11-sig-gen.md +11 -2
  4. data/docs/manual/02-cli-reference.md +46 -7
  5. data/docs/manual/03-configuration.md +2 -1
  6. data/docs/manual/04-diagnostics.md +1 -1
  7. data/docs/manual/10-mcp-server.md +9 -1
  8. data/docs/manual/12-caching.md +8 -1
  9. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  10. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  11. data/docs/manual/plugins/rigor-devise.md +15 -0
  12. data/docs/manual/plugins/rigor-rspec.md +2 -1
  13. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  14. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  15. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  16. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
  17. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  18. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  19. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  20. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  21. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
  22. data/lib/rigor/analysis/check_rules.rb +93 -8
  23. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  24. data/lib/rigor/analysis/edit_replay.rb +67 -0
  25. data/lib/rigor/analysis/incremental.rb +37 -0
  26. data/lib/rigor/analysis/incremental_session.rb +175 -27
  27. data/lib/rigor/analysis/path_expansion.rb +41 -0
  28. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  29. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  30. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  31. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  32. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  36. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  37. data/lib/rigor/analysis/runner.rb +156 -49
  38. data/lib/rigor/analysis/worker_session.rb +14 -2
  39. data/lib/rigor/cache/incremental_snapshot.rb +30 -7
  40. data/lib/rigor/cli/check_command.rb +93 -4
  41. data/lib/rigor/cli/diff_command.rb +2 -1
  42. data/lib/rigor/cli/lens_command.rb +5 -5
  43. data/lib/rigor/cli/mcp_command.rb +2 -1
  44. data/lib/rigor/cli/options.rb +20 -3
  45. data/lib/rigor/cli/plugins_command.rb +5 -0
  46. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  48. data/lib/rigor/cli/sig_gen_command.rb +46 -7
  49. data/lib/rigor/cli/type_of_command.rb +2 -1
  50. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  51. data/lib/rigor/config_audit.rb +16 -5
  52. data/lib/rigor/configuration.rb +14 -4
  53. data/lib/rigor/environment/rbs_loader.rb +115 -6
  54. data/lib/rigor/environment/required_features.rb +136 -29
  55. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  56. data/lib/rigor/environment.rb +4 -2
  57. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  58. data/lib/rigor/inference/deep_erasure.rb +138 -0
  59. data/lib/rigor/inference/def_return_typer.rb +159 -9
  60. data/lib/rigor/inference/definer_resolution.rb +17 -3
  61. data/lib/rigor/inference/error_info.rb +17 -3
  62. data/lib/rigor/inference/expression_typer.rb +62 -13
  63. data/lib/rigor/inference/global_write_census.rb +36 -1
  64. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  65. data/lib/rigor/inference/loop_value_record.rb +53 -0
  66. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
  68. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  69. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  70. data/lib/rigor/inference/method_dispatcher.rb +46 -16
  71. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  72. data/lib/rigor/inference/module_function_state.rb +12 -0
  73. data/lib/rigor/inference/mutation_widening.rb +5 -2
  74. data/lib/rigor/inference/narrowing.rb +66 -5
  75. data/lib/rigor/inference/object_mixins.rb +13 -1
  76. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  77. data/lib/rigor/inference/project_yielder.rb +128 -0
  78. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  79. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  80. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  81. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  82. data/lib/rigor/inference/scope_indexer.rb +480 -103
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
  84. data/lib/rigor/inference/statement_evaluator.rb +658 -90
  85. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  86. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  87. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  88. data/lib/rigor/language_server/buffer_table.rb +6 -0
  89. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  90. data/lib/rigor/language_server/project_context.rb +59 -3
  91. data/lib/rigor/language_server/server.rb +5 -3
  92. data/lib/rigor/mcp/server.rb +9 -1
  93. data/lib/rigor/plugin/base.rb +26 -11
  94. data/lib/rigor/plugin/manifest.rb +29 -3
  95. data/lib/rigor/plugin/registry.rb +38 -0
  96. data/lib/rigor/protection/discovery_seed.rb +57 -2
  97. data/lib/rigor/rooted_glob.rb +10 -1
  98. data/lib/rigor/scope.rb +14 -5
  99. data/lib/rigor/sig_gen/generator.rb +308 -37
  100. data/lib/rigor/sig_gen/observation_collector.rb +4 -1
  101. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  102. data/lib/rigor/sig_gen/renderer.rb +6 -3
  103. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  104. data/lib/rigor/type/combinator.rb +4 -3
  105. data/lib/rigor/type/constant.rb +45 -3
  106. data/lib/rigor/type/hash_shape.rb +14 -9
  107. data/lib/rigor/type/struct_instance.rb +3 -2
  108. data/lib/rigor/version.rb +1 -1
  109. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
  110. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  111. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  112. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  113. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  114. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
  115. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  116. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  117. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  118. data/sig/rigor/analysis/baseline.rbs +3 -2
  119. data/sig/rigor/effects/file_collection.rbs +5 -6
  120. data/sig/rigor/effects/registry.rbs +8 -6
  121. data/sig/rigor/effects/summary.rbs +6 -5
  122. data/sig/rigor/environment.rbs +5 -3
  123. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  124. data/sig/rigor/inference.rbs +6 -0
  125. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  126. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  127. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  128. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  129. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  130. data/sig/rigor/plugin/io_boundary.rbs +4 -4
  131. data/sig/rigor/plugin/manifest.rbs +16 -18
  132. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  133. data/sig/rigor/plugin/registry.rbs +1 -1
  134. data/sig/rigor/scope.rbs +31 -50
  135. data/sig/rigor/type.rbs +0 -6
  136. metadata +20 -1
@@ -56,7 +56,7 @@ module Rigor
56
56
  :hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
57
57
  :source_rbs_synthesizer, :additional_initializers,
58
58
  :effect_root, :effect_labels, :effect_attributions, :effect_edges, :effect_entry_points,
59
- :effect_ancestry
59
+ :effect_ancestry, :per_invocation_classes, :per_invocation_non_action_macros
60
60
 
61
61
  # #392 — the project-relative globs whose files this plugin compiles into {TemplateUnit}s. Purely a
62
62
  # CLAIM: the engine globs these, reads each match's bytes and offers them to
@@ -74,7 +74,8 @@ module Rigor
74
74
  hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
75
75
  source_rbs_synthesizer: nil, additional_initializers: [],
76
76
  effect_root: nil, effect_labels: [], effect_attributions: [], effect_edges: [],
77
- effect_entry_points: [], effect_ancestry: [], template_globs: []
77
+ effect_entry_points: [], effect_ancestry: [], template_globs: [], per_invocation_classes: [],
78
+ per_invocation_non_action_macros: []
78
79
  )
79
80
  validate_id!(id)
80
81
  validate_version!(version)
@@ -109,6 +110,7 @@ module Rigor
109
110
  assign_additional_initializers(additional_initializers)
110
111
  assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
111
112
  effect_entry_points, effect_ancestry, template_globs)
113
+ assign_per_invocation_classes(per_invocation_classes, per_invocation_non_action_macros)
112
114
  freeze
113
115
  end
114
116
 
@@ -202,6 +204,28 @@ module Rigor
202
204
  @template_globs = globs.map { |glob| glob.to_s.dup.freeze }.uniq.freeze
203
205
  end
204
206
  private :assign_template_globs
207
+
208
+ # ADR-125 — `per_invocation_classes:` names the framework classes whose every instance serves exactly one
209
+ # entry point, so ivar writes in two distinct entry points of a descendant are never live together.
210
+ # `per_invocation_non_action_macros:` names the class-body macros whose Symbol, String or inline-`def`
211
+ # arguments name a public method that is NOT an entry point (`before_action :load`, `helper_method
212
+ # :current_user`); they apply to this manifest's own `per_invocation_classes:`, so a list without classes
213
+ # is refused. Validated and assigned in one call, because `#initialize` is at its line budget.
214
+ def assign_per_invocation_classes(names, macros)
215
+ validate_array_of!("per_invocation_classes", names, "non-empty String") do |name|
216
+ name.is_a?(String) && !name.empty?
217
+ end
218
+ validate_array_of!("per_invocation_non_action_macros", macros, "Symbol/non-empty String") do |macro|
219
+ macro.is_a?(Symbol) || (macro.is_a?(String) && !macro.empty?)
220
+ end
221
+ if names.empty? && !macros.empty?
222
+ raise ArgumentError, "per_invocation_non_action_macros needs per_invocation_classes to apply to"
223
+ end
224
+
225
+ @per_invocation_classes = names.map { |name| name.to_s.delete_prefix("::").dup.freeze }.uniq.freeze
226
+ @per_invocation_non_action_macros = macros.map(&:to_sym).uniq.freeze
227
+ end
228
+ private :assign_per_invocation_classes
205
229
  # rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
206
230
 
207
231
  public
@@ -258,7 +282,9 @@ module Rigor
258
282
  "effect_edges" => effect_edges.map(&:to_h),
259
283
  "effect_entry_points" => effect_entry_points.map(&:to_h),
260
284
  "effect_ancestry" => effect_ancestry.map(&:to_h),
261
- "template_globs" => template_globs
285
+ "template_globs" => template_globs,
286
+ "per_invocation_classes" => per_invocation_classes,
287
+ "per_invocation_non_action_macros" => per_invocation_non_action_macros.map(&:to_s)
262
288
  }
263
289
  end
264
290
 
@@ -397,6 +397,28 @@ module Rigor
397
397
  @rbs_complete_extends_set.include?(class_name.to_s)
398
398
  end
399
399
 
400
+ # ADR-125 — the aggregate class names declared across loaded plugins as per-invocation (manifest
401
+ # `per_invocation_classes:`): each instance of a descendant serves exactly one public-method entry point.
402
+ # `per_invocation_class?` is the exact-name membership predicate; `def.ivar-write-mismatch` walks a
403
+ # class's ancestry and asks it of each ancestor.
404
+ attr_reader :per_invocation_classes
405
+
406
+ def per_invocation_class?(class_name)
407
+ return false if class_name.nil?
408
+
409
+ @per_invocation_classes_set.include?(class_name.to_s.delete_prefix("::"))
410
+ end
411
+
412
+ # ADR-125 — the macros whose arguments name a non-entry-point method on a descendant of the declared
413
+ # per-invocation class `class_name`: the union of `per_invocation_non_action_macros:` over the manifests
414
+ # that declare that class. An empty Set for a class no manifest declares.
415
+ def per_invocation_non_action_macros(class_name)
416
+ @per_invocation_macros.fetch(class_name.to_s.delete_prefix("::"), EMPTY_MACROS)
417
+ end
418
+
419
+ EMPTY_MACROS = Set.new.freeze
420
+ private_constant :EMPTY_MACROS
421
+
400
422
  # ADR-28 — flat, ordered list of every loaded plugin's path-scoped method-protocol contracts, in
401
423
  # plugin registration order. Read from each plugin's `#protocol_contracts` (which the manifest backs
402
424
  # by default but a plugin MAY override to fold in per-project config). Consumed by
@@ -465,12 +487,28 @@ module Rigor
465
487
  @rbs_complete_ancestors_set = @rbs_complete_ancestors.to_set.freeze
466
488
  @rbs_complete_extends = aggregate_class_names(manifests, :rbs_complete_extends)
467
489
  @rbs_complete_extends_set = @rbs_complete_extends.to_set.freeze
490
+ @per_invocation_classes = aggregate_class_names(manifests, :per_invocation_classes)
491
+ @per_invocation_classes_set = @per_invocation_classes.to_set.freeze
492
+ @per_invocation_macros = compile_per_invocation_macros(manifests)
468
493
  @type_node_resolvers = manifests.flat_map { |m| m&.type_node_resolvers || [] }.freeze
469
494
  @protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
470
495
  @contracts_by_path = {}
471
496
  @effect_memo = {}
472
497
  end
473
498
 
499
+ # ADR-125 — declared class name => frozen Set of the non-action macros its declaring manifests list.
500
+ def compile_per_invocation_macros(manifests)
501
+ table = Hash.new { |hash, key| hash[key] = Set.new }
502
+ manifests.each do |manifest|
503
+ next if manifest.nil?
504
+
505
+ manifest.per_invocation_classes.each do |name|
506
+ table[name].merge(manifest.per_invocation_non_action_macros)
507
+ end
508
+ end
509
+ table.to_h { |name, macros| [name, macros.freeze] }.freeze
510
+ end
511
+
474
512
  # The shared shape of `open_receivers:` / `rbs_complete_ancestors:` — a flat uniq'd class-name list
475
513
  # aggregated across manifests, each entry normalised to String.
476
514
  def aggregate_class_names(manifests, field)
@@ -3,6 +3,9 @@
3
3
  require_relative "../inference/parameter_inference_collector"
4
4
  require_relative "../inference/scope_indexer"
5
5
  require_relative "../inference/project_seed_tables"
6
+ require_relative "../cache/incremental_snapshot"
7
+ require_relative "../analysis/path_expansion"
8
+ require_relative "../analysis/buffer_binding"
6
9
 
7
10
  module Rigor
8
11
  module Protection
@@ -73,12 +76,64 @@ module Rigor
73
76
  # @param paths — the measured file set, in the same order {#bundles} was built from.
74
77
  # @param bundles — that bundle set.
75
78
  # @param buffer — the mutant binding (logical path → mutant bytes).
79
+ # @param constant_sources — ADR-124 — also carry the per-name writer paths (`constant_sources`), which a
80
+ # `prebuilt:` runner needs to retract a `pre_eval:` constant another file also assigns (#663).
76
81
  # @return frozen seed tables.
77
- def tables_for_buffer(paths:, bundles:, buffer:)
82
+ def tables_for_buffer(paths:, bundles:, buffer:, constant_sources: false)
78
83
  index = Inference::ScopeIndexer.discovered_project_index_incremental(
79
84
  paths, seed_bundles: bundles, buffer: buffer
80
85
  )
81
- seed_tables(index).freeze
86
+ tables = seed_tables(index)
87
+ sources = index.fetch(:def_index).fetch(:constant_sources)
88
+ tables = tables.merge(constant_sources: sources) if constant_sources && !sources.empty?
89
+ tables.freeze
90
+ end
91
+
92
+ # Issue #512 — the seed a one-shot probe (`rigor type-of`) can afford: the cross-file discovery tables for
93
+ # the configured paths, folded from the per-file bundles a prior `rigor check --incremental` left in the
94
+ # persistent snapshot, with `buffer`'s file (and any file whose bytes moved since) re-walked. A probe has
95
+ # no business re-parsing the project — that is what made the first attempt cost twice a single-file probe —
96
+ # so when there is no reusable snapshot this answers nil and the caller keeps its single-file scope. It
97
+ # never writes the snapshot.
98
+ #
99
+ # The snapshot is reused only under the fingerprint a bare `rigor check --incremental` writes (roots = the
100
+ # configured paths, the plain run mode): any other fingerprint, a different engine, configuration,
101
+ # lockfile or `sig/` included, is a miss.
102
+ #
103
+ # @param configuration — the probe's loaded configuration.
104
+ # @param buffer — the editor binding, or nil.
105
+ # @return frozen seed tables, or nil when the snapshot cannot seed this project.
106
+ def from_snapshot(configuration:, buffer: nil)
107
+ paths = Analysis::PathExpansion.configured_files(configuration)
108
+ return nil if paths.empty?
109
+
110
+ bundles = snapshot_bundles(configuration)
111
+ return nil if bundles.nil? || bundles.empty?
112
+
113
+ tables_for_buffer(paths: respell_for_buffer(paths, buffer), bundles: bundles, buffer: buffer,
114
+ constant_sources: true)
115
+ end
116
+
117
+ # The configured file list with the entry that reaches `buffer`'s file respelled as the buffer's logical
118
+ # path: the binding substitutes bytes only for a path spelled exactly as its logical path, while the
119
+ # configured paths may be relative or reach the file through a symlink. `real_paths` is the memoised
120
+ # `BufferBinding.real_path` of each entry when the caller keeps one.
121
+ def respell_for_buffer(paths, buffer, real_paths: nil)
122
+ return paths if buffer.nil?
123
+
124
+ real = Analysis::BufferBinding.real_path(buffer.logical_path)
125
+ paths.each_with_index.map do |path, index|
126
+ entry_real = real_paths ? real_paths[index] : Analysis::BufferBinding.real_path(path)
127
+ entry_real == real ? buffer.logical_path : path
128
+ end
129
+ end
130
+
131
+ def snapshot_bundles(configuration)
132
+ fingerprint = Cache::IncrementalSnapshot.fingerprint(configuration: configuration, roots: configuration.paths)
133
+ return nil if fingerprint.nil?
134
+
135
+ loaded = Cache::IncrementalSnapshot.new(root: configuration.cache_path).load_any(fingerprints: [fingerprint])
136
+ loaded&.last&.seed_bundles
82
137
  end
83
138
 
84
139
  # The whole-project discovery tables, from the single-walk combined pass (one parse per file) that
@@ -12,6 +12,9 @@ module Rigor
12
12
  # would not do that: it reads an absolute pattern, or an absolute alternative, outside `base`. A root without
13
13
  # metacharacters is joined unchanged, so the results are the joined glob's strings in its order, `root` kept as
14
14
  # given (relative, symlinked or with a trailing `/`).
15
+ #
16
+ # `literal(name)` is the same escape for a single name (a file's basename, a configured path) that is joined into
17
+ # a pattern as a literal: `literal("a[1].rb")` matches `a[1].rb` and not `a1.rb`.
15
18
  module RootedGlob
16
19
  METACHARACTERS = /[\[\]{}*?\\]/
17
20
 
@@ -21,7 +24,13 @@ module Rigor
21
24
  # @param pattern — a `Dir.glob` pattern relative to `root`
22
25
  # @return the matches spelled under `root`, in `Dir.glob` order; empty when `root` is not a directory
23
26
  def glob(root, pattern)
24
- Dir.glob(File.join(root.to_s.gsub(METACHARACTERS, "\\\\\\0"), pattern.to_s))
27
+ Dir.glob(File.join(literal(root), pattern.to_s))
28
+ end
29
+
30
+ # @param name — a file or directory name, taken literally
31
+ # @return the pattern that matches exactly `name` under `Dir.glob`, with its metacharacters escaped
32
+ def literal(name)
33
+ name.to_s.gsub(METACHARACTERS, "\\\\\\0")
25
34
  end
26
35
  end
27
36
  end
data/lib/rigor/scope.rb CHANGED
@@ -1563,7 +1563,8 @@ module Rigor
1563
1563
  #
1564
1564
  # This is EVIDENCE FOR a singleton ancestor, never against one: the walk sees only what a constant
1565
1565
  # argument to a receiverless `extend` (or, since #915, a receiverless `include` / `prepend` inside a
1566
- # `class << self` body) written inside a declaration body spells, so a runtime `Widget.extend(m)` and an
1566
+ # `class << self` body) written inside a declaration body spells, or, since #2094, `Widget.extend(M)` and
1567
+ # `Widget.singleton_class.include(M)` with a constant receiver, so a runtime `Widget.extend(m)` and an
1567
1568
  # `extend` in a file outside the analysed set are both absent from it, as is an `extend` declared only
1568
1569
  # in RBS — that one reaches the same consumer through `Environment#singleton_extended_modules` instead.
1569
1570
  # Its one consumer ({Inference::Narrowing.narrow_class}) therefore reads it
@@ -1941,8 +1942,9 @@ module Rigor
1941
1942
  # declaration-level fact of the whole file, which is also the granularity
1942
1943
  # `ScopeIndexer#append_constant_signature` moves the declaration signature at.
1943
1944
  #
1944
- # `constant_sources` is seeded only when dependency recording is on, so on every ordinary run the table
1945
- # is empty and this is one Hash miss; the caller additionally gates on the recorder being active.
1945
+ # `constant_sources` is seeded when dependency recording is on and in the editor publish's discovery seed
1946
+ # (ADR-124), where the recorder is inactive; elsewhere the table is empty and this is one Hash miss. The caller
1947
+ # gates on the recorder being active.
1946
1948
  def record_constant_dependency(name)
1947
1949
  sites = @discovery.constant_sources[name]
1948
1950
  return if sites.nil?
@@ -1973,11 +1975,18 @@ module Rigor
1973
1975
  # a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
1974
1976
  # modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
1975
1977
  # `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method surface a diagnostic.
1976
- def discovered_method_visibility(class_name, method_name)
1978
+ # `singleton: true` reads the class's singleton slot for the name (`[:singleton, name]`).
1979
+ #
1980
+ # Issue #2134 — every answer is an ADR-46 read of the slot, so it records `visibility:<class>#<method_name>`
1981
+ # whichever way it answers ({Analysis::DependencyRecorder.read_visibility}). A `private :foo` can be written from
1982
+ # a reopening or a `class_eval` block in a file the reader has no other edge to, and adding or removing it must
1983
+ # re-check the reader.
1984
+ def discovered_method_visibility(class_name, method_name, singleton: false)
1985
+ Analysis::DependencyRecorder.read_visibility(class_name, method_name) if Analysis::DependencyRecorder.active?
1977
1986
  table = @discovery.discovered_method_visibilities[class_name.to_s]
1978
1987
  return nil unless table
1979
1988
 
1980
- table[method_name.to_sym]
1989
+ table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
1981
1990
  end
1982
1991
 
1983
1992
  # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap (ROADMAP § Type-language / engine —