rigortype 0.3.5 → 0.3.7

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 (266) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
  14. data/docs/handbook/02-everyday-types.md +1 -1
  15. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  16. data/docs/handbook/08-understanding-errors.md +1 -1
  17. data/docs/handbook/09-plugins.md +2 -2
  18. data/docs/handbook/10-sorbet.md +1 -1
  19. data/docs/handbook/README.md +2 -2
  20. data/docs/handbook/appendix-go.md +1 -1
  21. data/docs/handbook/appendix-java-csharp.md +2 -2
  22. data/docs/handbook/appendix-mypy.md +1 -1
  23. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  24. data/docs/handbook/appendix-rust.md +1 -1
  25. data/docs/handbook/appendix-type-theory.md +2 -2
  26. data/docs/handbook/appendix-typescript.md +4 -4
  27. data/docs/manual/02-cli-reference.md +29 -16
  28. data/docs/manual/03-configuration.md +1 -1
  29. data/docs/manual/04-diagnostics.md +12 -0
  30. data/docs/manual/05-inspecting-types.md +20 -8
  31. data/docs/manual/07-plugins.md +4 -0
  32. data/docs/manual/08-skills.md +1 -1
  33. data/docs/manual/11-ci.md +1 -1
  34. data/docs/manual/12-caching.md +1 -1
  35. data/docs/manual/15-type-protection-coverage.md +1 -1
  36. data/docs/manual/18-removing-dead-code.md +13 -4
  37. data/docs/manual/19-effect-labels.md +19 -12
  38. data/docs/manual/README.md +2 -2
  39. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  40. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  41. data/docs/manual/plugins/rigor-activejob.md +3 -3
  42. data/docs/manual/plugins/rigor-activerecord.md +78 -5
  43. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  44. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  45. data/docs/manual/plugins/rigor-rspec.md +6 -2
  46. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  47. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  48. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  49. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  50. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  51. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  52. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  53. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  54. data/lib/rigor/analysis/check_rules.rb +383 -48
  55. data/lib/rigor/analysis/crash_signature.rb +116 -0
  56. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  57. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  58. data/lib/rigor/analysis/diagnostic.rb +17 -6
  59. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  60. data/lib/rigor/analysis/incremental.rb +28 -0
  61. data/lib/rigor/analysis/incremental_session.rb +44 -9
  62. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  63. data/lib/rigor/analysis/reachability/graph.rb +44 -16
  64. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  65. data/lib/rigor/analysis/reachability/scan.rb +27 -7
  66. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  67. data/lib/rigor/analysis/result.rb +32 -0
  68. data/lib/rigor/analysis/run_cache_key.rb +53 -1
  69. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  70. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
  71. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  72. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  73. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  74. data/lib/rigor/analysis/runner.rb +298 -38
  75. data/lib/rigor/analysis/worker_session.rb +10 -1
  76. data/lib/rigor/bleeding_edge.rb +7 -6
  77. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  78. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  79. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  80. data/lib/rigor/cache/descriptor.rb +42 -2
  81. data/lib/rigor/cache/file_digest.rb +20 -2
  82. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  83. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  84. data/lib/rigor/cache/store.rb +10 -1
  85. data/lib/rigor/cli/check_command.rb +67 -48
  86. data/lib/rigor/cli/coverage_command.rb +28 -24
  87. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  88. data/lib/rigor/cli/coverage_scan.rb +47 -6
  89. data/lib/rigor/cli/doc_links.rb +100 -0
  90. data/lib/rigor/cli/docs_command.rb +32 -2
  91. data/lib/rigor/cli/effects_command.rb +27 -2
  92. data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
  93. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  94. data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
  95. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  96. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  97. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  98. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  99. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  100. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  101. data/lib/rigor/cli/type_of_command.rb +188 -41
  102. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  103. data/lib/rigor/cli/type_scan_command.rb +10 -1
  104. data/lib/rigor/cli/unused_command.rb +65 -16
  105. data/lib/rigor/cli.rb +22 -9
  106. data/lib/rigor/configuration.rb +7 -4
  107. data/lib/rigor/effects/definition_lines.rb +100 -0
  108. data/lib/rigor/effects/envelope_check.rb +18 -1
  109. data/lib/rigor/effects/liskov_check.rb +17 -8
  110. data/lib/rigor/effects/plugin_facts.rb +1 -1
  111. data/lib/rigor/effects/scanner.rb +2 -3
  112. data/lib/rigor/effects/signature_sources.rb +13 -2
  113. data/lib/rigor/effects/snapshot.rb +53 -21
  114. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  115. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  116. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  117. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  118. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  119. data/lib/rigor/environment/rbs_loader.rb +525 -46
  120. data/lib/rigor/environment.rb +38 -15
  121. data/lib/rigor/inference/acceptance.rb +172 -20
  122. data/lib/rigor/inference/captured_locals.rb +62 -0
  123. data/lib/rigor/inference/content_join.rb +347 -0
  124. data/lib/rigor/inference/def_handle.rb +15 -3
  125. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  126. data/lib/rigor/inference/expression_typer.rb +1020 -141
  127. data/lib/rigor/inference/fork_map.rb +6 -1
  128. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  129. data/lib/rigor/inference/hkt_registry.rb +46 -0
  130. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  131. data/lib/rigor/inference/index_write_widening.rb +48 -0
  132. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  133. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  134. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  135. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  136. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  137. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  138. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  139. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  140. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  141. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  142. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  143. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  144. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  145. data/lib/rigor/inference/mutation_widening.rb +244 -174
  146. data/lib/rigor/inference/narrowing.rb +331 -27
  147. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  148. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  149. data/lib/rigor/inference/precision_scanner.rb +5 -2
  150. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  151. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  152. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  153. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  154. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  155. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  156. data/lib/rigor/inference/version_guard.rb +229 -0
  157. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  158. data/lib/rigor/plugin/base.rb +3 -2
  159. data/lib/rigor/plugin/inflector.rb +14 -5
  160. data/lib/rigor/plugin/io_boundary.rb +107 -4
  161. data/lib/rigor/plugin/loader.rb +14 -0
  162. data/lib/rigor/protection/analysis_guard.rb +60 -0
  163. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  164. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  165. data/lib/rigor/protection/discovery_seed.rb +1 -1
  166. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  167. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  168. data/lib/rigor/protection/mutator.rb +121 -18
  169. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  170. data/lib/rigor/reflection.rb +182 -30
  171. data/lib/rigor/scope/discovery_index.rb +39 -1
  172. data/lib/rigor/scope.rb +385 -4
  173. data/lib/rigor/sig_gen/classification.rb +12 -1
  174. data/lib/rigor/sig_gen/generator.rb +254 -5
  175. data/lib/rigor/sig_gen/renderer.rb +1 -6
  176. data/lib/rigor/sig_gen/writer.rb +3 -0
  177. data/lib/rigor/source/constant_path.rb +79 -2
  178. data/lib/rigor/triage/catalogue.rb +1 -1
  179. data/lib/rigor/type/combinator.rb +10 -0
  180. data/lib/rigor/type/maybe.rb +47 -0
  181. data/lib/rigor/type/refined.rb +1 -2
  182. data/lib/rigor/type/result.rb +53 -0
  183. data/lib/rigor/type.rb +2 -0
  184. data/lib/rigor/version.rb +1 -1
  185. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  186. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  187. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  188. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  189. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  190. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  191. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  192. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  193. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  194. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  195. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  196. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  197. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  198. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  199. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  200. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  201. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  202. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  203. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  204. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  207. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  208. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  209. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  210. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  211. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  212. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  213. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  214. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  215. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  216. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  217. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  218. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  219. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  220. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  221. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  222. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  223. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  224. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  225. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  226. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  227. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  228. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  229. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  230. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  231. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  232. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  233. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  234. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  235. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  236. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  237. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  238. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  239. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  240. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  241. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  242. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  243. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  244. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  245. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  246. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  247. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  248. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  249. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  250. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  251. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  252. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  253. data/sig/rigor/analysis/baseline.rbs +1 -1
  254. data/sig/rigor/environment.rbs +3 -0
  255. data/sig/rigor/inference.rbs +4 -1
  256. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  257. data/sig/rigor/reflection.rbs +3 -1
  258. data/sig/rigor/scope.rbs +25 -1
  259. data/sig/rigor/type.rbs +95 -1
  260. data/sig/rigor.rbs +16 -0
  261. data/skills/rigor-ci-setup/SKILL.md +2 -2
  262. data/skills/rigor-editor-setup/SKILL.md +2 -2
  263. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  264. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  265. data/skills/rigor-plugin-review/SKILL.md +3 -3
  266. metadata +73 -3
@@ -120,23 +120,65 @@ module Rigor
120
120
  Effects::FileCollection.merge_all(effect_collections)
121
121
  end
122
122
 
123
+ # The run's as-written superclass table — what `effect.liskov-widened` reads. Served straight from
124
+ # the summary entry on a warm hit (#482), where merging every collection to recover one of its
125
+ # tables would be the whole cost the split removes.
126
+ def effect_ancestry
127
+ @cached_effect_ancestry || effect_collection.superclasses
128
+ end
129
+
123
130
  # Issue #382 — the same collections keyed by the path that produced each, which is the form the two
124
131
  # caches persist: `{ "lib/a.rb" => FileCollection, … }`. The merged {#effect_collection} cannot be
125
132
  # persisted per file (merging is where the path is deliberately dropped), and per file is what an
126
133
  # ADR-46 recheck needs — it re-collects the changed closure and serves the rest from the snapshot.
127
134
  def effect_collections_by_path
128
135
  pooled = @pool_coordinator.collected_effects
129
- pooled.empty? ? @file_effects.dup : @file_effects.merge(pooled)
136
+ base = forced_file_effects
137
+ pooled.empty? ? base.dup : base.merge(pooled)
138
+ end
139
+
140
+ # #482 — the per-file collections of a run served from the summary entry, loaded on the first
141
+ # question that genuinely needs them. A served run adopts the table, the sources and the ancestry
142
+ # from the small entry and never touches the collections blob; this is the escape hatch that makes
143
+ # that split safe, because a consumer the split did not anticipate loads the blob here instead of
144
+ # silently reading an empty table. Nil loader (an analyzing run) is the identity.
145
+ def forced_file_effects
146
+ loader = @cached_collections_loader
147
+ return @file_effects if loader.nil?
148
+
149
+ @cached_collections_loader = nil
150
+ loaded = loader.call
151
+ @file_effects = loaded if loaded.is_a?(Hash)
152
+ @file_effects
153
+ end
154
+
155
+ # Issue #382 — adopt persisted collections as this run's own. The warm-hit half of the whole-run
156
+ # effects slot: the analysis did not run, so `#assemble_run_diagnostics` never reached
157
+ # {#close_effect_graph}. The slot stores the propagated table BESIDE the collections — the whole-run
158
+ # entry is already invalidated by every analyzed file (the same post-run dependency descriptor as
159
+ # the diagnostics slot) and by every meaning input (the effects identity), which is exactly the
160
+ # invalidation a stored closure needs — so a warm hit adopts both and re-runs nothing. An entry
161
+ # from before the table rode along reads as a miss (`#peek_effect_collections`' shape guard), and
162
+ # the ADR-46 incremental snapshot still persists per-file collections only: a recheck re-collects a
163
+ # partial closure, which is the case a stored table genuinely cannot serve.
164
+ def adopt_effect_collections(collections, table: nil)
165
+ @file_effects = collections
166
+ if table
167
+ @effect_table = table
168
+ else
169
+ close_effect_graph
170
+ end
130
171
  end
131
172
 
132
- # Issue #382adopt persisted collections as this run's own and close the graph over them. The
133
- # warm-hit half of the whole-run effects slot: the analysis did not run, so `#assemble_run_diagnostics`
134
- # never reached {#close_effect_graph} and the fixpoint is run here instead. The fixpoint is ALWAYS
135
- # re-run rather than persisted — it is the cheap half, and a stored table would have to be invalidated
136
- # by every input a summary has.
137
- def adopt_effect_collections(collections)
138
- @file_effects = collections
139
- close_effect_graph
173
+ # #482the warm-hit half after the split: everything a served run reads, with the collections left
174
+ # behind a loader ({#forced_file_effects}). `sources` and `ancestry` are the two derived tables a
175
+ # served run would otherwise have to merge the whole collection set to recover.
176
+ def adopt_effect_summary(table:, sources:, ancestry:, collections_loader:)
177
+ @effect_table = table
178
+ @cached_effect_sources = sources
179
+ @cached_effect_ancestry = ancestry
180
+ @cached_collections_loader = collections_loader
181
+ @file_effects = {}
140
182
  end
141
183
 
142
184
  # Issue #382 — whether this run's effect collections came from the whole-run effects slot rather than
@@ -154,6 +196,8 @@ module Rigor
154
196
  # its entry points are named by *file* globs (`effects.snapshot.reach:`, the `unused --entry-point`
155
197
  # syntax), so the key has to be traced back to the file it was written in.
156
198
  def effect_sources
199
+ return @cached_effect_sources if @cached_effect_sources
200
+
157
201
  effect_collections.each_with_object({}) do |collection, out|
158
202
  path = collection.path
159
203
  next if path.nil?
@@ -248,6 +292,11 @@ module Rigor
248
292
  @file_effects = {}
249
293
  @effect_table = nil
250
294
  @effects_served_from_cache = false
295
+ # #482 — set only by a run served from the summary entry: the two derived tables it carries, and
296
+ # the loader that fetches the collections blob if anything still asks for per-file form.
297
+ @cached_effect_sources = nil
298
+ @cached_effect_ancestry = nil
299
+ @cached_collections_loader = nil
251
300
  @run_dependency_descriptor = nil
252
301
  # Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
253
302
  # See `self_undefined_rule_active?`.
@@ -295,16 +344,31 @@ module Rigor
295
344
  @cached_plugin_prepare_diagnostics = [].freeze
296
345
  @project_discovered_classes = {}.freeze
297
346
  @project_discovered_def_nodes = {}.freeze
347
+ # Issue #681 — the `Module.nesting` recorded per declared `def`, keyed by node identity.
348
+ @project_discovered_def_nestings = {}.compare_by_identity.freeze
298
349
  @project_discovered_singleton_def_nodes = {}.freeze
299
350
  @project_discovered_def_sources = {}.freeze
300
351
  @project_discovered_singleton_def_sources = {}.freeze
301
352
  @project_discovered_superclasses = {}.freeze
353
+ # Issue #682 — the `Module.nesting` each class / module declaration HEADER is written in.
354
+ @project_discovered_header_nestings = {}.freeze
302
355
  @project_discovered_includes = {}.freeze
303
356
  @project_discovered_class_sources = {}.freeze
357
+ # Issue #644 — the cross-file VALUE-constant publication table (`{qualified name => Type::Constant}`,
358
+ # literal writes only) and its per-name write attribution. The first seeds `in_source_constants` on
359
+ # every per-file scope so `FOO = :sym` in one file reads as `:sym` in another; the second seeds
360
+ # `constant_sources` only on an ADR-46 recording run, where it is the positive edge's attribution.
361
+ @project_constant_values = {}.freeze
362
+ @project_constant_sources = {}.freeze
363
+ # ...and the raw per-(name, path) publication census the incremental producer diffs.
364
+ @project_constant_writes = {}.freeze
365
+ @published_constant_name_set = nil
304
366
  @project_discovered_method_visibilities = {}.freeze
305
367
  @project_discovered_methods = {}.freeze
306
368
  @project_data_member_layouts = {}.freeze
307
369
  @project_struct_member_layouts = {}.freeze
370
+ # Issue #684 — set per run by {#project_discovery_expansion}; nil on every path that does not widen.
371
+ @project_discovery_expansion = nil
308
372
  # ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
309
373
  # `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
310
374
  # scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
@@ -355,6 +419,7 @@ module Rigor
355
419
  return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
356
420
 
357
421
  expansion = expand_paths(paths)
422
+ @project_discovery_expansion = project_discovery_expansion(paths, expansion)
358
423
  @snapshots.reset_for_run
359
424
  # Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
360
425
  @project_discovery_done = false
@@ -481,6 +546,22 @@ module Rigor
481
546
  result.transform_values(&:freeze).freeze
482
547
  end
483
548
 
549
+ # Issue #644 — per-file PUBLICATION CENSUS: `{path => {qualified constant name => descriptor}}`, the
550
+ # value-constant twin of {#class_declarations}. The descriptor (`[literal]` or
551
+ # `ScopeIndexer::CONSTANT_UNPUBLISHABLE`) is load-bearing and a bare name set is not: a name's published
552
+ # answer is a function of the WHOLE project's write set for it, so what a recheck has to detect is a
553
+ # change in the census — a value edit, a write becoming unpublishable, a second declarer appearing or
554
+ # going away — not merely a name appearing.
555
+ # {Incremental.changed_constant_publications} diffs it; the reader's `constant:` edge is recorded on
556
+ # every reference, resolved or not, so the diff reaches it either way.
557
+ def constant_declarations
558
+ result = Hash.new { |hash, key| hash[key] = {} }
559
+ @project_constant_writes.each do |name, by_path|
560
+ by_path.each { |path, descriptor| result[path][name] = descriptor }
561
+ end
562
+ result.transform_values(&:freeze).freeze
563
+ end
564
+
484
565
  # ADR-67 WD6c lift — the inferred-param table this run seeded from (frozen; empty when
485
566
  # `parameter_inference:` is off or the pre-pass failed soft). The incremental session reads it back
486
567
  # after a baseline so the snapshot records the seeds the cached diagnostics were computed under.
@@ -588,6 +669,8 @@ module Rigor
588
669
  def evaluate_return_types_setup(paths, specs)
589
670
  expansion = expand_paths(paths || @configuration.paths)
590
671
  @project_discovery_done = false
672
+ # This entry point supplies its own expansion; it never widens (see {#project_discovery_expansion}).
673
+ @project_discovery_expansion = nil
591
674
  @run_generation = Object.new.freeze
592
675
  run_project_pre_passes(expansion: expansion)
593
676
  ensure_project_discovery(expansion)
@@ -674,9 +757,7 @@ module Rigor
674
757
  return nil unless @record_effects
675
758
 
676
759
  descriptor = effects_key_descriptor(key_descriptor)
677
- cached = descriptor && peek_effect_collections(descriptor)
678
- if cached
679
- adopt_effect_collections(cached)
760
+ if descriptor && served_from_effect_entries?(descriptor)
680
761
  @effects_served_from_cache = true
681
762
  return nil
682
763
  end
@@ -686,6 +767,32 @@ module Rigor
686
767
  analysis
687
768
  end
688
769
 
770
+ # The two warm lanes, in cost order (#482).
771
+ #
772
+ # The summary entry is what a warm run wants: the table and the two derived tables, and no
773
+ # collections blob. Its absence is not a miss on its own — a run whose propagation fail-softed
774
+ # deliberately stores no table (see {#storable_effect_table}), and an entry written before the
775
+ # split has only the collections — so the second lane adopts the collections and re-runs the
776
+ # fixpoint over them, which is the pre-split behaviour and the retry the fail-soft posture wants.
777
+ # Only when neither entry answers does the run re-analyse.
778
+ #
779
+ # @return [Boolean] whether this run was served.
780
+ def served_from_effect_entries?(descriptor)
781
+ summary = peek_effect_summary(descriptor)
782
+ if summary
783
+ table, sources, ancestry = summary
784
+ adopt_effect_summary(table: table, sources: sources, ancestry: ancestry,
785
+ collections_loader: -> { peek_effect_collections(descriptor) || {} })
786
+ return true
787
+ end
788
+
789
+ collections = peek_effect_collections(descriptor)
790
+ return false if collections.nil?
791
+
792
+ adopt_effect_collections(collections)
793
+ true
794
+ end
795
+
689
796
  def effects_key_descriptor(key_descriptor)
690
797
  Effects::Identity.descriptor(base: key_descriptor, configuration: @configuration,
691
798
  plugin_facts: effect_plugin_facts)
@@ -693,42 +800,92 @@ module Rigor
693
800
  nil
694
801
  end
695
802
 
696
- # The read half. A miss, a stale dependency, a corrupt entry and a stored value of the wrong shape
697
- # are one answer — nil, "collect it again" — because none of them can be told apart from the outside
698
- # and all of them have the same remedy.
803
+ # The read half of the serving entry (#482). A miss, a stale dependency, a corrupt entry and a
804
+ # stored value of the wrong shape are one answer — nil, "collect it again" — because none of them
805
+ # can be told apart from the outside and all of them have the same remedy. An entry from before the
806
+ # split simply does not exist under this producer id, so an older cache re-collects once.
807
+ def peek_effect_summary(descriptor)
808
+ cached = @cache_store.peek_validated(
809
+ producer_id: RunCacheKey::RUN_EFFECTS_TABLE_PRODUCER_ID, key_descriptor: descriptor
810
+ )
811
+ return nil unless cached.is_a?(Array) && cached.length == 3
812
+ return nil unless cached[0].is_a?(Effects::EffectTable) && cached[1].is_a?(Hash) &&
813
+ cached[2].is_a?(Hash)
814
+
815
+ cached
816
+ rescue StandardError
817
+ nil
818
+ end
819
+
820
+ # The read half of the collections entry — only ever asked by {#forced_file_effects}, on the paths
821
+ # that need per-file form. The stored shape is `[collections]`; an entry from before the split is
822
+ # `[collections, table]` and its first member is the same Hash, so it still reads.
699
823
  def peek_effect_collections(descriptor)
700
824
  cached = @cache_store.peek_validated(
701
825
  producer_id: RunCacheKey::RUN_EFFECTS_PRODUCER_ID, key_descriptor: descriptor
702
826
  )
703
- cached.is_a?(Hash) ? cached : nil
827
+ return nil unless cached.is_a?(Array) && cached.first.is_a?(Hash)
828
+
829
+ cached.first
704
830
  rescue StandardError
705
831
  nil
706
832
  end
707
833
 
708
834
  # The write half, run only after a miss, so the block never recomputes anything: it hands over the
709
- # collections the analysis just produced, validated against the same post-run dependency descriptor
710
- # the diagnostics slot records. Fail-soft a collection that will not Marshal (which nothing in
711
- # {Effects::FileCollection} should be, and the fork pool already proves per file) costs the next run
712
- # its warm start and nothing else.
835
+ # collections the analysis just produced and the table the analysis already closed over them, so
836
+ # a warm hit re-runs neither the merge nor the fixpoint validated against the same post-run
837
+ # dependency descriptor the diagnostics slot records. Fail-soft a collection that will not
838
+ # Marshal (which nothing in {Effects::FileCollection} should be, and the fork pool already proves
839
+ # per file) costs the next run its warm start and nothing else.
840
+ # The write half, run only after a miss, so neither block recomputes anything. Two entries under one
841
+ # key descriptor and one dependency descriptor (#482): the summary a warm run serves from, and the
842
+ # collections the incremental and fail-soft paths read. The summary is written LAST, so a run
843
+ # interrupted between the two leaves a collections entry that the next run's summary miss simply
844
+ # re-collects — never a summary whose collections are absent.
713
845
  def store_effect_collections(descriptor, expansion, rbs_descriptor)
714
846
  return if descriptor.nil?
715
847
 
716
848
  collections = effect_collections_by_path
717
- @cache_store.fetch_or_validate(
718
- producer_id: RunCacheKey::RUN_EFFECTS_PRODUCER_ID, key_descriptor: descriptor,
719
- generation_cap: RunCacheKey::EFFECTS_GENERATION_CAP
720
- ) { [collections, run_dependency_descriptor(expansion, rbs_descriptor)] }
849
+ dependencies = run_dependency_descriptor(expansion, rbs_descriptor)
850
+ write_effect_entry(RunCacheKey::RUN_EFFECTS_PRODUCER_ID, descriptor, [collections], dependencies)
851
+ table = storable_effect_table(collections)
852
+ return nil if table.nil?
853
+
854
+ write_effect_entry(RunCacheKey::RUN_EFFECTS_TABLE_PRODUCER_ID, descriptor,
855
+ [table, effect_sources, effect_collection.superclasses], dependencies)
721
856
  nil
722
857
  rescue StandardError
723
858
  nil
724
859
  end
725
- private :serve_effect_collections, :effects_key_descriptor,
726
- :peek_effect_collections, :store_effect_collections
860
+
861
+ def write_effect_entry(producer_id, descriptor, payload, dependencies)
862
+ @cache_store.fetch_or_validate(
863
+ producer_id: producer_id, key_descriptor: descriptor,
864
+ generation_cap: RunCacheKey::EFFECTS_GENERATION_CAP
865
+ ) { [payload, dependencies] }
866
+ end
867
+
868
+ # An empty table over non-empty summaries is {#close_effect_graph}'s fail-soft answer, not a
869
+ # result — persisting it would freeze one run's propagation failure into every warm hit until the
870
+ # next invalidation. Storing nil instead makes the warm hit re-run the fixpoint, which is exactly
871
+ # the retry the fail-soft posture wants.
872
+ def storable_effect_table(collections)
873
+ table = @effect_table
874
+ return nil if table.nil?
875
+ return nil if table.empty? && collections.any? { |_, collection| !collection.summaries.empty? }
876
+
877
+ table
878
+ end
879
+ private :serve_effect_collections, :served_from_effect_entries?,
880
+ :effects_key_descriptor, :peek_effect_summary,
881
+ :peek_effect_collections, :store_effect_collections, :write_effect_entry,
882
+ :storable_effect_table, :forced_file_effects
727
883
 
728
884
  # ADR-103 WD8 / #383 — the envelope check. Nothing at all without an `effects:` block, and nothing
729
885
  # under `effects.check: false`; with both, one walk of the project's own RBS for `%a{pure}` /
730
886
  # `%a{rigor:v1:effect …}`, and only if that finds an envelope does anything else run (the discovery
731
- # tables the `def` positions come from are forced from inside the pass, lazily, for that reason).
887
+ # tables the `def` positions come from are forced from inside the judgment, on the first finding
888
+ # built — a judged-clean envelope forces no discovery at all).
732
889
  #
733
890
  # The environment is the one the cacheable path already resolved when there is one, so a warm run
734
891
  # reads the envelopes off the loader it built anyway rather than building a second.
@@ -745,7 +902,7 @@ module Rigor
745
902
  # ADR-103 WD1 / #386 — the nominal relation `effect.liskov-widened` reads, from the collector's
746
903
  # own as-written superclass table, so the Liskov check and the closed-world proven lane resolve
747
904
  # the same ancestry. Lazy: only a project that declared an envelope pays the merge.
748
- ancestry: -> { effect_collection.superclasses },
905
+ ancestry: -> { effect_ancestry },
749
906
  apply_tolerated: !@no_tolerated_effects,
750
907
  # #387 — the same compiled plugin tables the collection window scanned under, so an envelope may
751
908
  # name a label a plugin opened and the unknown-label check agrees with the scan.
@@ -836,6 +993,10 @@ module Rigor
836
993
  close_effect_graph
837
994
  diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
838
995
  diagnostics += @diagnostic_aggregator.rbs_environment_build_failed_diagnostics
996
+ # Issue #696 — after its env-wide twin and before the synthesized-namespace notice: the three
997
+ # `rbs.coverage.*` build conditions surface widest-consequence first, and their relative order is
998
+ # the diagnostic output contract.
999
+ diagnostics += @diagnostic_aggregator.rbs_definition_build_failed_diagnostics
839
1000
  diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
840
1001
  diagnostics += @diagnostic_aggregator.conforms_to_diagnostics
841
1002
  diagnostics += @diagnostic_aggregator.rbs_extended_reporter_diagnostics
@@ -958,15 +1119,33 @@ module Rigor
958
1119
  end
959
1120
 
960
1121
  def build_run_dependency_descriptor(expansion, rbs_descriptor)
961
- entries = analyzed_file_entries(expansion) + pre_eval_file_entries + rbs_descriptor.files
1122
+ entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
1123
+ pre_eval_file_entries + rbs_descriptor.files
1124
+ globs = []
962
1125
  @plugin_registry.plugins.each do |plugin|
963
1126
  # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
964
1127
  # are frozen after the run, and a plugin that never built a boundary read no files through it,
965
1128
  # so it contributes no dependencies.
966
1129
  boundary = plugin.instance_variable_get(:@io_boundary)
967
1130
  entries.concat(boundary.cache_descriptor.files) if boundary
1131
+ plugin.class.producers.each_value do |prod|
1132
+ globs.concat(plugin.send(:watch_glob_entries, prod[:watch])) if prod[:watch]
1133
+ end
968
1134
  end
969
- Cache::Descriptor.new(files: entries)
1135
+ Cache::Descriptor.new(files: entries, globs: globs.uniq)
1136
+ end
1137
+
1138
+ # Issue #684 — the project files this run DISCOVERED but did not analyse. A widened run's diagnostics
1139
+ # are a function of them (that is the entire point: the method whose absence would have been reported
1140
+ # is defined in one of them), so the run-result cache must invalidate when one changes. Empty on every
1141
+ # run whose discovery expansion is its analysis expansion, which is every whole-project run.
1142
+ def discovery_file_entries(expansion)
1143
+ return [] if @project_discovery_expansion.nil?
1144
+
1145
+ analyzed = expansion.fetch(:files).to_set
1146
+ analyzed_file_entries(
1147
+ { files: @project_discovery_expansion.fetch(:files).reject { |path| analyzed.include?(path) } }
1148
+ )
970
1149
  end
971
1150
 
972
1151
  def analyzed_file_entries(expansion)
@@ -1062,12 +1241,18 @@ module Rigor
1062
1241
  def apply_discovery_result(discovery)
1063
1242
  @project_discovered_classes = discovery.discovered_classes
1064
1243
  @project_discovered_def_nodes = discovery.discovered_def_nodes
1244
+ @project_discovered_def_nestings = discovery.discovered_def_nestings
1065
1245
  @project_discovered_singleton_def_nodes = discovery.discovered_singleton_def_nodes
1066
1246
  @project_discovered_def_sources = discovery.discovered_def_sources
1067
1247
  @project_discovered_singleton_def_sources = discovery.discovered_singleton_def_sources
1068
1248
  @project_discovered_superclasses = discovery.discovered_superclasses
1249
+ @project_discovered_header_nestings = discovery.discovered_header_nestings
1069
1250
  @project_discovered_includes = discovery.discovered_includes
1070
1251
  @project_discovered_class_sources = discovery.discovered_class_sources
1252
+ @project_constant_values = discovery.constant_values
1253
+ @published_constant_name_set = nil
1254
+ @project_constant_sources = discovery.constant_sources
1255
+ @project_constant_writes = discovery.constant_writes
1071
1256
  @project_discovered_method_visibilities = discovery.discovered_method_visibilities
1072
1257
  @project_discovered_methods = discovery.discovered_methods
1073
1258
  @project_data_member_layouts = discovery.data_member_layouts
@@ -1087,6 +1272,8 @@ module Rigor
1087
1272
  return if @project_discovery_done
1088
1273
 
1089
1274
  @project_discovery_done = true
1275
+ # Issue #684 — the cross-file index spans the PROJECT even when the analysis targets a file list.
1276
+ expansion = @project_discovery_expansion || expansion
1090
1277
  if @collect_seed_bundles
1091
1278
  # ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
1092
1279
  # capture the refreshed bundle set for the session to persist.
@@ -1099,6 +1286,39 @@ module Rigor
1099
1286
  end
1100
1287
  end
1101
1288
 
1289
+ # Issue #684 — the file set the cross-file DISCOVERY pre-pass walks, when that is wider than the set
1290
+ # being analysed. nil means "the same one", which is every whole-project run.
1291
+ #
1292
+ # `rigor check path/to/one_file.rb` — what an editor integration, a pre-commit hook and
1293
+ # `git diff --name-only | xargs rigor check` all do — discovered only the files it was handed, so a
1294
+ # class the project declares in `sig/` WITHOUT declaring every one of its methods drew a false
1295
+ # `call.undefined-method` for any method whose defining file was outside the invocation. The class was
1296
+ # RBS-known, source discovery could not supply the method, and the rule fired on correct code. The
1297
+ # same check over the containing DIRECTORY reported nothing, which is also why per-directory sums
1298
+ # over a tree over-reported against a whole-tree run (14 diagnostics of this shape against 2 during
1299
+ # the #652 work).
1300
+ #
1301
+ # Discovery is a parse pass, not an analysis: 0.55s over Rigor's own 444-file `lib` cold, and under
1302
+ # ADR-85 seed bundles a warm run re-walks only what changed. The alternative — standing the rule down
1303
+ # whenever evidence might be missing — buys the same FP safety by making `rigor check one_file.rb`
1304
+ # answer a different question than `rigor check .`, and the diagnostics for a file should not depend
1305
+ # on what else was on the command line.
1306
+ #
1307
+ # Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
1308
+ # seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file, and
1309
+ # ADR-46's `analyze_only` subset mode (which already passes the full expansion and filters later).
1310
+ def project_discovery_expansion(paths, expansion)
1311
+ return nil unless widen_discovery_to_project?(paths)
1312
+
1313
+ widened = expand_paths(@configuration.paths | paths)
1314
+ widened.fetch(:files).size > expansion.fetch(:files).size ? widened : nil
1315
+ end
1316
+
1317
+ def widen_discovery_to_project?(paths)
1318
+ @prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && @analyze_only.nil? &&
1319
+ paths != @configuration.paths
1320
+ end
1321
+
1102
1322
  # ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
1103
1323
  # analysis assembly (`Runner#symbol_fingerprints` / `#class_declarations`, consumed by
1104
1324
  # {IncrementalSession} after the run), so they force the build eagerly — matching the pre-slice-1
@@ -1109,7 +1329,8 @@ module Rigor
1109
1329
  end
1110
1330
 
1111
1331
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
1112
- :apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?
1332
+ :apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?,
1333
+ :project_discovery_expansion, :widen_discovery_to_project?
1113
1334
 
1114
1335
  # Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
1115
1336
  # `version:`. Prism exposes no version list, so the floor is found empirically — only when a
@@ -1240,6 +1461,7 @@ module Rigor
1240
1461
  synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
1241
1462
  quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
1242
1463
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
1464
+ definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
1243
1465
  conformance_results_snapshot: -> { @snapshots.conformance_results }
1244
1466
  )
1245
1467
  end
@@ -1458,6 +1680,9 @@ module Rigor
1458
1680
  tables[:run_generation] = @run_generation if @run_generation
1459
1681
  tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
1460
1682
  tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
1683
+ unless @project_discovered_def_nestings.empty?
1684
+ tables[:discovered_def_nestings] = @project_discovered_def_nestings
1685
+ end
1461
1686
  unless @project_discovered_singleton_def_nodes.empty?
1462
1687
  tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
1463
1688
  end
@@ -1465,6 +1690,9 @@ module Rigor
1465
1690
  unless @project_discovered_superclasses.empty?
1466
1691
  tables[:discovered_superclasses] = @project_discovered_superclasses
1467
1692
  end
1693
+ unless @project_discovered_header_nestings.empty?
1694
+ tables[:discovered_header_nestings] = @project_discovered_header_nestings
1695
+ end
1468
1696
  tables[:discovered_includes] = @project_discovered_includes unless @project_discovered_includes.empty?
1469
1697
  unless @project_discovered_method_visibilities.empty?
1470
1698
  tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
@@ -1472,14 +1700,22 @@ module Rigor
1472
1700
  tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
1473
1701
  seed_opt_in_pre_pass_tables(tables)
1474
1702
  seed_member_layout_tables(tables)
1475
- # ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
1476
- # dependency recording, so seed it only when recording is on; a normal run never carries it.
1477
- if @record_dependencies && !@project_discovered_class_sources.empty?
1478
- tables[:discovered_class_sources] = @project_discovered_class_sources
1479
- end
1703
+ seed_dependency_attribution_tables(tables)
1480
1704
  tables
1481
1705
  end
1482
1706
 
1707
+ # ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read only by the recording accessors
1708
+ # (`Scope#record_class_dependency` for the class-declaration map, `Scope#record_constant_dependency`
1709
+ # for the constant-write map). A normal run carries neither. Extracted to keep
1710
+ # {#project_scope_seed_tables} under the complexity budget.
1711
+ def seed_dependency_attribution_tables(tables)
1712
+ return unless @record_dependencies
1713
+
1714
+ tables[:discovered_class_sources] = @project_discovered_class_sources unless
1715
+ @project_discovered_class_sources.empty?
1716
+ tables[:constant_sources] = @project_constant_sources unless @project_constant_sources.empty?
1717
+ end
1718
+
1483
1719
  # Issue #260 — the mutable starting point {#project_scope_seed_tables} fills in: a copy of the opt-in
1484
1720
  # `discovery_seed:` when one was supplied, otherwise the empty Hash every other run has always started
1485
1721
  # from. Extracted so the seed costs {#project_scope_seed_tables} no branch of its complexity budget.
@@ -1499,9 +1735,33 @@ module Rigor
1499
1735
  # Extracted to keep {#project_scope_seed_tables} under the complexity budget.
1500
1736
  def seed_opt_in_pre_pass_tables(tables)
1501
1737
  tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
1502
- return if @project_pre_eval_constants.empty?
1738
+ constants = merged_seed_constants
1739
+ return if constants.empty?
1740
+
1741
+ tables[:in_source_constants] = constants
1742
+ names = published_constant_name_set
1743
+ tables[:published_constant_names] = names unless names.empty?
1744
+ end
1745
+
1746
+ # Issue #644 — the seeded `in_source_constants` table, joining the two cross-file constant producers.
1747
+ # The whole-project literal table wins over a `pre_eval:` publication of the same name: both describe
1748
+ # the same assignment, the literal table pins the value the write actually carries while ADR-17's
1749
+ # widener erases it to the class, and listing a file under `pre_eval:` must never LOSE precision. The
1750
+ # per-file table still wins over both (`ScopeIndexer.index`'s merge) — same-file stays the most
1751
+ # specific authority.
1752
+ # Issue #644 — the LAST SEGMENTS of the published table, the run-wide half of
1753
+ # `Scope#published_constant?`. Seeded on EVERY run (unlike `constant_sources`, which only a recording
1754
+ # run needs) because {CheckRules::PublishedConstantGuard} asks the question on every `if` / `unless`.
1755
+ def published_constant_name_set
1756
+ @published_constant_name_set ||=
1757
+ @project_constant_values.keys.to_set { |name| name.split("::").last }.freeze
1758
+ end
1759
+
1760
+ def merged_seed_constants
1761
+ return @project_constant_values if @project_pre_eval_constants.empty?
1762
+ return @project_pre_eval_constants if @project_constant_values.empty?
1503
1763
 
1504
- tables[:in_source_constants] = @project_pre_eval_constants
1764
+ @project_pre_eval_constants.merge(@project_constant_values).freeze
1505
1765
  end
1506
1766
 
1507
1767
  # ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
@@ -235,6 +235,14 @@ module Rigor
235
235
  # Read-once snapshot of the per-worker reporters so the caller (or the eventual Phase 4b pool
236
236
  # aggregator) can merge into a single coordinator-side reporter. Both reporters dedupe at write time,
237
237
  # so a post-hoc concat + de-dup at the entry-key level is sound.
238
+ #
239
+ # Issue #696 — `definition_build_failures` rides this channel rather than a coordinator-side snapshot,
240
+ # and it has to. Under the pool the PARENT never analyses a file, so its loader never demands a
241
+ # definition and never enters the failing build; a diagnostic wired off the parent's loader would
242
+ # appear at `--workers=0` and vanish at `--workers=N`, which is the same "reports less depending on
243
+ # how you ran it" defect the diagnostic exists to end. Draining it out of the workers is what makes
244
+ # the two paths say the same thing. The payload is `[String, String, String, Array<String>]` tuples —
245
+ # Marshal-clean for the fork backend and shareable for the Ractor one.
238
246
  def drain_reporters
239
247
  {
240
248
  rbs_extended: {
@@ -242,7 +250,8 @@ module Rigor
242
250
  lossy_projections: @rbs_extended_reporter.lossy_projections
243
251
  },
244
252
  boundary_cross: @boundary_cross_reporter.entries,
245
- source_rbs_synthesis: @source_rbs_synthesis_reporter.entries
253
+ source_rbs_synthesis: @source_rbs_synthesis_reporter.entries,
254
+ definition_build_failures: @environment&.rbs_loader&.definition_build_failures || []
246
255
  }
247
256
  end
248
257
 
@@ -107,14 +107,15 @@ module Rigor
107
107
  id: "reject-unparseable-signatures",
108
108
  kind: :severity,
109
109
  summary: "A broken `signature_paths:` RBS set fails the run instead of degrading it silently. An " \
110
- "unparseable `.rbs` is otherwise skipped with a warning, and a duplicate-declaration " \
111
- "conflict (a file that parses fine but collides on resolve — typically against Rigor's " \
112
- "own bundled RBS) collapses the whole env with a warning; either way the run gets quieter " \
113
- "rather than cleaner. This treats both as a build error, the way a broken source file " \
114
- "already is.",
110
+ "unparseable `.rbs` is otherwise skipped with a warning; a duplicate-declaration conflict " \
111
+ "(a file that parses fine but collides on resolve — typically against Rigor's own bundled " \
112
+ "RBS) collapses the whole env with a warning; and a duplicate METHOD definition collapses " \
113
+ "one class's method surface the same way. Each leaves the run quieter rather than " \
114
+ "cleaner. This treats all three as a build error, the way a broken source file already is.",
115
115
  severity_overrides: {
116
116
  "rbs.coverage.quarantined-signature" => :error,
117
- "rbs.coverage.environment-build-failed" => :error
117
+ "rbs.coverage.environment-build-failed" => :error,
118
+ "rbs.coverage.definition-build-failed" => :error
118
119
  }.freeze
119
120
  ),
120
121
  Feature.new(