rigortype 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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 +409 -11
  14. data/docs/handbook/11-sig-gen.md +24 -14
  15. data/docs/manual/02-cli-reference.md +23 -7
  16. data/docs/manual/04-diagnostics.md +1 -1
  17. data/docs/manual/05-inspecting-types.md +20 -8
  18. data/docs/manual/07-plugins.md +4 -0
  19. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  20. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  21. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  22. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  24. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  25. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  29. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  30. data/lib/rigor/analysis/check_rules.rb +383 -48
  31. data/lib/rigor/analysis/crash_signature.rb +188 -0
  32. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  33. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  34. data/lib/rigor/analysis/incremental.rb +28 -0
  35. data/lib/rigor/analysis/incremental_session.rb +63 -16
  36. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  37. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  38. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  39. data/lib/rigor/analysis/result.rb +32 -0
  40. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  41. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +233 -16
  42. data/lib/rigor/analysis/runner/pool_coordinator.rb +266 -47
  43. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  44. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -2
  45. data/lib/rigor/analysis/runner.rb +212 -16
  46. data/lib/rigor/analysis/worker_session.rb +37 -3
  47. data/lib/rigor/bleeding_edge.rb +7 -6
  48. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  49. data/lib/rigor/builtins/imported_refinements.rb +11 -9
  50. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  51. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  52. data/lib/rigor/cache/annotation_location.rb +72 -0
  53. data/lib/rigor/cache/descriptor.rb +42 -2
  54. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  55. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +70 -8
  56. data/lib/rigor/cache/store.rb +17 -1
  57. data/lib/rigor/cli/coverage_command.rb +23 -18
  58. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  59. data/lib/rigor/cli/coverage_scan.rb +47 -6
  60. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  61. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  62. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  63. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  64. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  65. data/lib/rigor/cli/sig_gen_command.rb +47 -0
  66. data/lib/rigor/cli/type_of_command.rb +188 -41
  67. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  68. data/lib/rigor/cli/type_scan_command.rb +10 -1
  69. data/lib/rigor/cli.rb +1 -1
  70. data/lib/rigor/configuration.rb +7 -4
  71. data/lib/rigor/effects/plugin_facts.rb +1 -1
  72. data/lib/rigor/effects/scanner.rb +2 -3
  73. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  74. data/lib/rigor/environment/failure_slot.rb +28 -0
  75. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  76. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  77. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  78. data/lib/rigor/environment/rbs_loader.rb +743 -93
  79. data/lib/rigor/environment.rb +125 -25
  80. data/lib/rigor/inference/acceptance.rb +172 -20
  81. data/lib/rigor/inference/captured_locals.rb +62 -0
  82. data/lib/rigor/inference/content_join.rb +347 -0
  83. data/lib/rigor/inference/def_handle.rb +15 -3
  84. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  85. data/lib/rigor/inference/expression_typer.rb +1020 -141
  86. data/lib/rigor/inference/fork_map.rb +6 -1
  87. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  88. data/lib/rigor/inference/hkt_registry.rb +61 -3
  89. data/lib/rigor/inference/hkt_sugar_translator.rb +87 -0
  90. data/lib/rigor/inference/index_write_widening.rb +48 -0
  91. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  92. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  93. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  94. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  95. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  96. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  97. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  98. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  99. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  100. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  101. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  103. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  104. data/lib/rigor/inference/mutation_widening.rb +244 -174
  105. data/lib/rigor/inference/narrowing.rb +331 -27
  106. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  107. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  108. data/lib/rigor/inference/precision_scanner.rb +5 -2
  109. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  110. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  111. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  112. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  113. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  114. data/lib/rigor/inference/version_guard.rb +229 -0
  115. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  116. data/lib/rigor/plugin/base.rb +3 -2
  117. data/lib/rigor/plugin/inflector.rb +14 -5
  118. data/lib/rigor/plugin/io_boundary.rb +107 -4
  119. data/lib/rigor/plugin/loader.rb +14 -0
  120. data/lib/rigor/plugin/registry.rb +25 -2
  121. data/lib/rigor/protection/analysis_guard.rb +60 -0
  122. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  123. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  124. data/lib/rigor/protection/discovery_seed.rb +1 -1
  125. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  126. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  127. data/lib/rigor/protection/mutator.rb +121 -18
  128. data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
  129. data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
  130. data/lib/rigor/rbs_extended/reporter.rb +93 -13
  131. data/lib/rigor/rbs_extended.rb +6 -1
  132. data/lib/rigor/reflection.rb +182 -30
  133. data/lib/rigor/scope/discovery_index.rb +39 -1
  134. data/lib/rigor/scope.rb +385 -4
  135. data/lib/rigor/sig_gen/classification.rb +12 -1
  136. data/lib/rigor/sig_gen/generator.rb +320 -85
  137. data/lib/rigor/sig_gen/renderer.rb +14 -12
  138. data/lib/rigor/sig_gen/writer.rb +3 -0
  139. data/lib/rigor/source/constant_path.rb +79 -2
  140. data/lib/rigor/triage/catalogue.rb +1 -1
  141. data/lib/rigor/type/combinator.rb +10 -0
  142. data/lib/rigor/type/maybe.rb +47 -0
  143. data/lib/rigor/type/refined.rb +1 -2
  144. data/lib/rigor/type/result.rb +53 -0
  145. data/lib/rigor/type.rb +2 -0
  146. data/lib/rigor/version.rb +1 -1
  147. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  148. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  149. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  151. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  152. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  157. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  159. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  161. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  162. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  163. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  164. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  165. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  166. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  167. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  168. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  169. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  170. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  171. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  172. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  173. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  174. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  175. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  176. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  177. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  178. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  179. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  180. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  181. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  182. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  183. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  184. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  185. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  186. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  187. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  188. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  189. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  190. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  191. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  192. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  193. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  194. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  195. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  196. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  197. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  198. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  199. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  200. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  201. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  202. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  203. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  204. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  205. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  206. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  207. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  208. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  209. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  210. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  211. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  212. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  213. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  214. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  215. data/sig/rigor/analysis/baseline.rbs +1 -1
  216. data/sig/rigor/environment.rbs +9 -0
  217. data/sig/rigor/inference.rbs +4 -1
  218. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  219. data/sig/rigor/reflection.rbs +3 -1
  220. data/sig/rigor/scope.rbs +25 -1
  221. data/sig/rigor/type.rbs +95 -1
  222. data/sig/rigor.rbs +16 -0
  223. metadata +71 -3
@@ -29,6 +29,7 @@ require_relative "../inference/scope_indexer"
29
29
  require_relative "../inference/synthetic_method_scanner"
30
30
  require_relative "../inference/project_patched_scanner"
31
31
  require_relative "../inference/method_dispatcher/file_folding"
32
+ require_relative "crash_signature"
32
33
  require_relative "buffer_binding"
33
34
  require_relative "check_rules"
34
35
  require_relative "dependency_recorder"
@@ -64,7 +65,14 @@ module Rigor
64
65
 
65
66
  attr_reader :cache_store, :plugin_registry, :dependency_source_index,
66
67
  :rbs_extended_reporter, :boundary_cross_reporter,
67
- :analyzed_files, :unresolved_self_calls, :seed_bundles
68
+ :analyzed_files, :unresolved_self_calls, :seed_bundles,
69
+ # #788 rounds 6–7 — the rows per-file analysis produced this run, severity-resolved exactly
70
+ # as the run's stream is and sliced to this run's targets (a pool backend folds `.rigor.yml`
71
+ # prepare / degraded rows into `analyze_files`'s return). `IncrementalSession` caches ONLY
72
+ # this and serves reused files from it without re-stamping: a run-level row is regenerated
73
+ # every run wherever it is positioned, and slicing the run's full stream by path cached the
74
+ # file-positioned ones (`effect.annotations-unchecked`, `source-rbs-*`).
75
+ :per_file_diagnostics
68
76
 
69
77
  # ADR-46 — the per-file cross-file read records this run captured (empty unless
70
78
  # `record_dependencies: true`). Sequential analysis records into `@file_dependencies` via
@@ -302,6 +310,7 @@ module Rigor
302
310
  # See `self_undefined_rule_active?`.
303
311
  @self_undefined_rule_active = nil
304
312
  @analyzed_files = [].freeze
313
+ @per_file_diagnostics = [].freeze
305
314
  # In-memory source map for `#run_source` — `{ logical_path => source String }`. When set,
306
315
  # `parse_source` reads bytes from here instead of disk and `expand_paths` accepts the (possibly
307
316
  # non-existent) logical path. nil on a normal disk-backed run.
@@ -344,16 +353,31 @@ module Rigor
344
353
  @cached_plugin_prepare_diagnostics = [].freeze
345
354
  @project_discovered_classes = {}.freeze
346
355
  @project_discovered_def_nodes = {}.freeze
356
+ # Issue #681 — the `Module.nesting` recorded per declared `def`, keyed by node identity.
357
+ @project_discovered_def_nestings = {}.compare_by_identity.freeze
347
358
  @project_discovered_singleton_def_nodes = {}.freeze
348
359
  @project_discovered_def_sources = {}.freeze
349
360
  @project_discovered_singleton_def_sources = {}.freeze
350
361
  @project_discovered_superclasses = {}.freeze
362
+ # Issue #682 — the `Module.nesting` each class / module declaration HEADER is written in.
363
+ @project_discovered_header_nestings = {}.freeze
351
364
  @project_discovered_includes = {}.freeze
352
365
  @project_discovered_class_sources = {}.freeze
366
+ # Issue #644 — the cross-file VALUE-constant publication table (`{qualified name => Type::Constant}`,
367
+ # literal writes only) and its per-name write attribution. The first seeds `in_source_constants` on
368
+ # every per-file scope so `FOO = :sym` in one file reads as `:sym` in another; the second seeds
369
+ # `constant_sources` only on an ADR-46 recording run, where it is the positive edge's attribution.
370
+ @project_constant_values = {}.freeze
371
+ @project_constant_sources = {}.freeze
372
+ # ...and the raw per-(name, path) publication census the incremental producer diffs.
373
+ @project_constant_writes = {}.freeze
374
+ @published_constant_name_set = nil
353
375
  @project_discovered_method_visibilities = {}.freeze
354
376
  @project_discovered_methods = {}.freeze
355
377
  @project_data_member_layouts = {}.freeze
356
378
  @project_struct_member_layouts = {}.freeze
379
+ # Issue #684 — set per run by {#project_discovery_expansion}; nil on every path that does not widen.
380
+ @project_discovery_expansion = nil
357
381
  # ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
358
382
  # `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
359
383
  # scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
@@ -404,12 +428,16 @@ module Rigor
404
428
  return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
405
429
 
406
430
  expansion = expand_paths(paths)
431
+ @project_discovery_expansion = project_discovery_expansion(paths, expansion)
407
432
  @snapshots.reset_for_run
408
433
  # Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
409
434
  @project_discovery_done = false
410
435
  # Per-run reset of the environment the cacheable path resolves, reused by the envelope pass so a
411
436
  # run never builds two.
412
437
  @run_environment = nil
438
+ # #788 — the per-file reader is assigned only on the analysis (miss) path; reset it here so a run the
439
+ # ADR-45 result cache serves does not answer with the previous run's rows.
440
+ @per_file_diagnostics = [].freeze
413
441
  # ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
414
442
  # of THIS run share one memo bucket while entries from any earlier run in this process (stale after
415
443
  # an edit) become unreachable.
@@ -530,6 +558,22 @@ module Rigor
530
558
  result.transform_values(&:freeze).freeze
531
559
  end
532
560
 
561
+ # Issue #644 — per-file PUBLICATION CENSUS: `{path => {qualified constant name => descriptor}}`, the
562
+ # value-constant twin of {#class_declarations}. The descriptor (`[literal]` or
563
+ # `ScopeIndexer::CONSTANT_UNPUBLISHABLE`) is load-bearing and a bare name set is not: a name's published
564
+ # answer is a function of the WHOLE project's write set for it, so what a recheck has to detect is a
565
+ # change in the census — a value edit, a write becoming unpublishable, a second declarer appearing or
566
+ # going away — not merely a name appearing.
567
+ # {Incremental.changed_constant_publications} diffs it; the reader's `constant:` edge is recorded on
568
+ # every reference, resolved or not, so the diff reaches it either way.
569
+ def constant_declarations
570
+ result = Hash.new { |hash, key| hash[key] = {} }
571
+ @project_constant_writes.each do |name, by_path|
572
+ by_path.each { |path, descriptor| result[path][name] = descriptor }
573
+ end
574
+ result.transform_values(&:freeze).freeze
575
+ end
576
+
533
577
  # ADR-67 WD6c lift — the inferred-param table this run seeded from (frozen; empty when
534
578
  # `parameter_inference:` is off or the pre-pass failed soft). The incremental session reads it back
535
579
  # after a baseline so the snapshot records the seeds the cached diagnostics were computed under.
@@ -637,10 +681,19 @@ module Rigor
637
681
  def evaluate_return_types_setup(paths, specs)
638
682
  expansion = expand_paths(paths || @configuration.paths)
639
683
  @project_discovery_done = false
684
+ # This entry point supplies its own expansion; it never widens (see {#project_discovery_expansion}).
685
+ @project_discovery_expansion = nil
640
686
  @run_generation = Object.new.freeze
641
687
  run_project_pre_passes(expansion: expansion)
642
688
  ensure_project_discovery(expansion)
643
- environment = @pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
689
+ # Issue #795 — `source_files:` is the WHOLE project's file list, never `target_files(expansion)`.
690
+ # `IncrementalSession#build_runner` threads its own `@buffer` into this Runner, so in editor-buffer
691
+ # mode `target_files` narrows to the buffer's single logical path; an environment built over just
692
+ # that path drops every OTHER file's plugin-synthesized virtual RBS, which can flip a re-evaluated
693
+ # return descriptor and wrongly declare a caller's return type unstable (or stable) relative to the
694
+ # full-project answer. This probe never analyzes a file, so it carries none of #788's per-file-cache
695
+ # duplication concern either.
696
+ environment = @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files))
644
697
  specs.to_h do |spec|
645
698
  [[spec[:class_name], spec[:method_name], spec[:singleton]], evaluate_spec_returns(spec, environment)]
646
699
  end
@@ -893,9 +946,19 @@ module Rigor
893
946
  ).diagnostics
894
947
  end
895
948
 
949
+ # Issue #795 — `source_files:` is the WHOLE project's file list (`expansion.fetch(:files)`), never
950
+ # `target_files(expansion)`. A subset or editor-buffer run's `target_files` narrows to the
951
+ # `analyze_only` closure or the buffer's single logical path; building the envelope walk's
952
+ # environment over that subset drops every plugin-synthesized virtual RBS from an excluded file, so
953
+ # an envelope declared only in that file's inline annotation is invisible to the walk — the same #793
954
+ # gap #788 closed for per-file analysis, left open here because #788's binding sentences scoped
955
+ # themselves to "the environments the per-file analysis builds". This path is exempt from #788's
956
+ # per-file-cache duplication concern: `effect.unknown-label` (and its envelope-pass siblings) are
957
+ # produced by this pass, never by `Runner#per_file_diagnostics`, so `IncrementalSession` never caches
958
+ # them and widening this source list cannot duplicate a row on recheck.
896
959
  def envelope_rbs_loader(expansion)
897
960
  environment = @run_environment ||
898
- @pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
961
+ @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files))
899
962
  environment&.rbs_loader
900
963
  rescue StandardError
901
964
  nil
@@ -951,7 +1014,17 @@ module Rigor
951
1014
  # per-file cache, so it needs the full analyzed set to subtract the affected closure from.
952
1015
  targets = target_files(expansion)
953
1016
  @analyzed_files = targets
954
- diagnostics += @pool_coordinator.analyze_files(targets, environment: environment)
1017
+ # Issue #784 — the whole project's file list rides along so an EMPTY `targets` (a narrowed run whose
1018
+ # closure is empty) can still resolve the environment a full run would — same files, same
1019
+ # synthesized RBS — to demand the HKT registry once, while a project with no files resolves nothing.
1020
+ # #788 rounds 6–7 — the per-file stream is kept apart from the run-level streams appended below,
1021
+ # because `IncrementalSession` must cache ONLY what per-file analysis produced (#per_file_diagnostics),
1022
+ # and the reader is exposed SEVERITY-RESOLVED and sliced to this run's targets. The cache serves a
1023
+ # reused file without re-stamping, so a raw row would resurrect a rule the profile resolves to `:off`
1024
+ # (`static.value-use.void` ships off on the default profile) and serve the authored severity where an
1025
+ # override re-stamps it; and a pool backend folds `.rigor.yml`-positioned prepare / pool-degraded rows
1026
+ # into the same return, which the slice drops. The run's own stream is stamped once, at the end.
1027
+ diagnostics += analyze_targets(targets, environment: environment, project_files: expansion.fetch(:files))
955
1028
  # ADR-103 WD12 — the effect fixpoint, in the post-pool aggregation slot beside the conformance
956
1029
  # results. Graph-only over a finite lattice, so it is a plain worklist to a true fixpoint; it
957
1030
  # contributes NO diagnostics and its result leaves through `#effect_table`, never through the
@@ -959,6 +1032,14 @@ module Rigor
959
1032
  close_effect_graph
960
1033
  diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
961
1034
  diagnostics += @diagnostic_aggregator.rbs_environment_build_failed_diagnostics
1035
+ # Issue #696 — after its env-wide twin and before the synthesized-namespace notice: the four
1036
+ # `rbs.coverage.*` build conditions surface widest-consequence first, and their relative order is
1037
+ # the diagnostic output contract.
1038
+ diagnostics += @diagnostic_aggregator.rbs_definition_build_failed_diagnostics
1039
+ # Issue #784 — last of the four, deliberately: it loses only the IMPLICIT HKT registrations a
1040
+ # `type` alias would have contributed, never a class's own declared method surface, so it is the
1041
+ # narrowest-consequence rung on the same ladder.
1042
+ diagnostics += @diagnostic_aggregator.rbs_hkt_scan_failed_diagnostics
962
1043
  diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
963
1044
  diagnostics += @diagnostic_aggregator.conforms_to_diagnostics
964
1045
  diagnostics += @diagnostic_aggregator.rbs_extended_reporter_diagnostics
@@ -966,6 +1047,21 @@ module Rigor
966
1047
  diagnostics + @diagnostic_aggregator.source_rbs_synthesis_diagnostics
967
1048
  end
968
1049
 
1050
+ # #788 round 7 — runs per-file analysis over `targets` and exposes what it produced as
1051
+ # `#per_file_diagnostics`: the `analyze_files` return, stamped with the same severity profile the run's
1052
+ # own stream gets (`SeverityStamp` drops `:off` rows and re-stamps overrides, and the per-file cache never
1053
+ # re-stamps), sliced to the rows positioned at the targets. Returns the RAW return for the run's stream,
1054
+ # which is stamped once, at the end of `#run_analysis`.
1055
+ def analyze_targets(targets, environment:, project_files:)
1056
+ raw = @pool_coordinator.analyze_files(targets, environment: environment, project_files: project_files)
1057
+ analysed = targets.to_set
1058
+ @per_file_diagnostics = @diagnostic_aggregator.apply_severity_profile(raw)
1059
+ .select { |diagnostic| analysed.include?(diagnostic.path) }
1060
+ .freeze
1061
+ raw
1062
+ end
1063
+ private :analyze_targets
1064
+
969
1065
  # ADR-67 WD6a — the check-walk parameter-inference pre-pass. Populates `@project_param_inferred_types`
970
1066
  # (read by `project_scope_seed_tables`) with the call-site union of every undeclared parameter, running
971
1067
  # ONE round (a single hop of call-site → param typing; the protection scan's three-round fixpoint stays a
@@ -1081,15 +1177,33 @@ module Rigor
1081
1177
  end
1082
1178
 
1083
1179
  def build_run_dependency_descriptor(expansion, rbs_descriptor)
1084
- entries = analyzed_file_entries(expansion) + pre_eval_file_entries + rbs_descriptor.files
1180
+ entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
1181
+ pre_eval_file_entries + rbs_descriptor.files
1182
+ globs = []
1085
1183
  @plugin_registry.plugins.each do |plugin|
1086
1184
  # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
1087
1185
  # are frozen after the run, and a plugin that never built a boundary read no files through it,
1088
1186
  # so it contributes no dependencies.
1089
1187
  boundary = plugin.instance_variable_get(:@io_boundary)
1090
1188
  entries.concat(boundary.cache_descriptor.files) if boundary
1189
+ plugin.class.producers.each_value do |prod|
1190
+ globs.concat(plugin.send(:watch_glob_entries, prod[:watch])) if prod[:watch]
1191
+ end
1091
1192
  end
1092
- Cache::Descriptor.new(files: entries)
1193
+ Cache::Descriptor.new(files: entries, globs: globs.uniq)
1194
+ end
1195
+
1196
+ # Issue #684 — the project files this run DISCOVERED but did not analyse. A widened run's diagnostics
1197
+ # are a function of them (that is the entire point: the method whose absence would have been reported
1198
+ # is defined in one of them), so the run-result cache must invalidate when one changes. Empty on every
1199
+ # run whose discovery expansion is its analysis expansion, which is every whole-project run.
1200
+ def discovery_file_entries(expansion)
1201
+ return [] if @project_discovery_expansion.nil?
1202
+
1203
+ analyzed = expansion.fetch(:files).to_set
1204
+ analyzed_file_entries(
1205
+ { files: @project_discovery_expansion.fetch(:files).reject { |path| analyzed.include?(path) } }
1206
+ )
1093
1207
  end
1094
1208
 
1095
1209
  def analyzed_file_entries(expansion)
@@ -1185,12 +1299,18 @@ module Rigor
1185
1299
  def apply_discovery_result(discovery)
1186
1300
  @project_discovered_classes = discovery.discovered_classes
1187
1301
  @project_discovered_def_nodes = discovery.discovered_def_nodes
1302
+ @project_discovered_def_nestings = discovery.discovered_def_nestings
1188
1303
  @project_discovered_singleton_def_nodes = discovery.discovered_singleton_def_nodes
1189
1304
  @project_discovered_def_sources = discovery.discovered_def_sources
1190
1305
  @project_discovered_singleton_def_sources = discovery.discovered_singleton_def_sources
1191
1306
  @project_discovered_superclasses = discovery.discovered_superclasses
1307
+ @project_discovered_header_nestings = discovery.discovered_header_nestings
1192
1308
  @project_discovered_includes = discovery.discovered_includes
1193
1309
  @project_discovered_class_sources = discovery.discovered_class_sources
1310
+ @project_constant_values = discovery.constant_values
1311
+ @published_constant_name_set = nil
1312
+ @project_constant_sources = discovery.constant_sources
1313
+ @project_constant_writes = discovery.constant_writes
1194
1314
  @project_discovered_method_visibilities = discovery.discovered_method_visibilities
1195
1315
  @project_discovered_methods = discovery.discovered_methods
1196
1316
  @project_data_member_layouts = discovery.data_member_layouts
@@ -1210,6 +1330,8 @@ module Rigor
1210
1330
  return if @project_discovery_done
1211
1331
 
1212
1332
  @project_discovery_done = true
1333
+ # Issue #684 — the cross-file index spans the PROJECT even when the analysis targets a file list.
1334
+ expansion = @project_discovery_expansion || expansion
1213
1335
  if @collect_seed_bundles
1214
1336
  # ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
1215
1337
  # capture the refreshed bundle set for the session to persist.
@@ -1222,6 +1344,39 @@ module Rigor
1222
1344
  end
1223
1345
  end
1224
1346
 
1347
+ # Issue #684 — the file set the cross-file DISCOVERY pre-pass walks, when that is wider than the set
1348
+ # being analysed. nil means "the same one", which is every whole-project run.
1349
+ #
1350
+ # `rigor check path/to/one_file.rb` — what an editor integration, a pre-commit hook and
1351
+ # `git diff --name-only | xargs rigor check` all do — discovered only the files it was handed, so a
1352
+ # class the project declares in `sig/` WITHOUT declaring every one of its methods drew a false
1353
+ # `call.undefined-method` for any method whose defining file was outside the invocation. The class was
1354
+ # RBS-known, source discovery could not supply the method, and the rule fired on correct code. The
1355
+ # same check over the containing DIRECTORY reported nothing, which is also why per-directory sums
1356
+ # over a tree over-reported against a whole-tree run (14 diagnostics of this shape against 2 during
1357
+ # the #652 work).
1358
+ #
1359
+ # Discovery is a parse pass, not an analysis: 0.55s over Rigor's own 444-file `lib` cold, and under
1360
+ # ADR-85 seed bundles a warm run re-walks only what changed. The alternative — standing the rule down
1361
+ # whenever evidence might be missing — buys the same FP safety by making `rigor check one_file.rb`
1362
+ # answer a different question than `rigor check .`, and the diagnostics for a file should not depend
1363
+ # on what else was on the command line.
1364
+ #
1365
+ # Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
1366
+ # seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file, and
1367
+ # ADR-46's `analyze_only` subset mode (which already passes the full expansion and filters later).
1368
+ def project_discovery_expansion(paths, expansion)
1369
+ return nil unless widen_discovery_to_project?(paths)
1370
+
1371
+ widened = expand_paths(@configuration.paths | paths)
1372
+ widened.fetch(:files).size > expansion.fetch(:files).size ? widened : nil
1373
+ end
1374
+
1375
+ def widen_discovery_to_project?(paths)
1376
+ @prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && @analyze_only.nil? &&
1377
+ paths != @configuration.paths
1378
+ end
1379
+
1225
1380
  # ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
1226
1381
  # analysis assembly (`Runner#symbol_fingerprints` / `#class_declarations`, consumed by
1227
1382
  # {IncrementalSession} after the run), so they force the build eagerly — matching the pre-slice-1
@@ -1232,7 +1387,8 @@ module Rigor
1232
1387
  end
1233
1388
 
1234
1389
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
1235
- :apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?
1390
+ :apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?,
1391
+ :project_discovery_expansion, :widen_discovery_to_project?
1236
1392
 
1237
1393
  # Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
1238
1394
  # `version:`. Prism exposes no version list, so the floor is found empirically — only when a
@@ -1329,7 +1485,7 @@ module Rigor
1329
1485
  # indexes, prepare-diagnostic snapshot, and the four end-of-pass snapshots) is reached through reader
1330
1486
  # procs so each collaborator observes the live ivar value at call time without a back-reference
1331
1487
  # cycle. The reporter accumulators and the {RunSnapshots} sink are shared mutable instances.
1332
- def build_collaborators # rubocop:disable Metrics/MethodLength
1488
+ def build_collaborators # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
1333
1489
  @pre_passes = ProjectPrePasses.new(
1334
1490
  configuration: @configuration, cache_store: @cache_store, buffer: @buffer,
1335
1491
  plugin_requirer: @plugin_requirer, pool_mode: -> { pool_mode? }
@@ -1363,6 +1519,8 @@ module Rigor
1363
1519
  synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
1364
1520
  quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
1365
1521
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
1522
+ definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
1523
+ hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
1366
1524
  conformance_results_snapshot: -> { @snapshots.conformance_results }
1367
1525
  )
1368
1526
  end
@@ -1581,6 +1739,9 @@ module Rigor
1581
1739
  tables[:run_generation] = @run_generation if @run_generation
1582
1740
  tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
1583
1741
  tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
1742
+ unless @project_discovered_def_nestings.empty?
1743
+ tables[:discovered_def_nestings] = @project_discovered_def_nestings
1744
+ end
1584
1745
  unless @project_discovered_singleton_def_nodes.empty?
1585
1746
  tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
1586
1747
  end
@@ -1588,6 +1749,9 @@ module Rigor
1588
1749
  unless @project_discovered_superclasses.empty?
1589
1750
  tables[:discovered_superclasses] = @project_discovered_superclasses
1590
1751
  end
1752
+ unless @project_discovered_header_nestings.empty?
1753
+ tables[:discovered_header_nestings] = @project_discovered_header_nestings
1754
+ end
1591
1755
  tables[:discovered_includes] = @project_discovered_includes unless @project_discovered_includes.empty?
1592
1756
  unless @project_discovered_method_visibilities.empty?
1593
1757
  tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
@@ -1595,14 +1759,22 @@ module Rigor
1595
1759
  tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
1596
1760
  seed_opt_in_pre_pass_tables(tables)
1597
1761
  seed_member_layout_tables(tables)
1598
- # ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
1599
- # dependency recording, so seed it only when recording is on; a normal run never carries it.
1600
- if @record_dependencies && !@project_discovered_class_sources.empty?
1601
- tables[:discovered_class_sources] = @project_discovered_class_sources
1602
- end
1762
+ seed_dependency_attribution_tables(tables)
1603
1763
  tables
1604
1764
  end
1605
1765
 
1766
+ # ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read only by the recording accessors
1767
+ # (`Scope#record_class_dependency` for the class-declaration map, `Scope#record_constant_dependency`
1768
+ # for the constant-write map). A normal run carries neither. Extracted to keep
1769
+ # {#project_scope_seed_tables} under the complexity budget.
1770
+ def seed_dependency_attribution_tables(tables)
1771
+ return unless @record_dependencies
1772
+
1773
+ tables[:discovered_class_sources] = @project_discovered_class_sources unless
1774
+ @project_discovered_class_sources.empty?
1775
+ tables[:constant_sources] = @project_constant_sources unless @project_constant_sources.empty?
1776
+ end
1777
+
1606
1778
  # Issue #260 — the mutable starting point {#project_scope_seed_tables} fills in: a copy of the opt-in
1607
1779
  # `discovery_seed:` when one was supplied, otherwise the empty Hash every other run has always started
1608
1780
  # from. Extracted so the seed costs {#project_scope_seed_tables} no branch of its complexity budget.
@@ -1622,9 +1794,33 @@ module Rigor
1622
1794
  # Extracted to keep {#project_scope_seed_tables} under the complexity budget.
1623
1795
  def seed_opt_in_pre_pass_tables(tables)
1624
1796
  tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
1625
- return if @project_pre_eval_constants.empty?
1797
+ constants = merged_seed_constants
1798
+ return if constants.empty?
1799
+
1800
+ tables[:in_source_constants] = constants
1801
+ names = published_constant_name_set
1802
+ tables[:published_constant_names] = names unless names.empty?
1803
+ end
1804
+
1805
+ # Issue #644 — the seeded `in_source_constants` table, joining the two cross-file constant producers.
1806
+ # The whole-project literal table wins over a `pre_eval:` publication of the same name: both describe
1807
+ # the same assignment, the literal table pins the value the write actually carries while ADR-17's
1808
+ # widener erases it to the class, and listing a file under `pre_eval:` must never LOSE precision. The
1809
+ # per-file table still wins over both (`ScopeIndexer.index`'s merge) — same-file stays the most
1810
+ # specific authority.
1811
+ # Issue #644 — the LAST SEGMENTS of the published table, the run-wide half of
1812
+ # `Scope#published_constant?`. Seeded on EVERY run (unlike `constant_sources`, which only a recording
1813
+ # run needs) because {CheckRules::PublishedConstantGuard} asks the question on every `if` / `unless`.
1814
+ def published_constant_name_set
1815
+ @published_constant_name_set ||=
1816
+ @project_constant_values.keys.to_set { |name| name.split("::").last }.freeze
1817
+ end
1818
+
1819
+ def merged_seed_constants
1820
+ return @project_constant_values if @project_pre_eval_constants.empty?
1821
+ return @project_pre_eval_constants if @project_constant_values.empty?
1626
1822
 
1627
- tables[:in_source_constants] = @project_pre_eval_constants
1823
+ @project_pre_eval_constants.merge(@project_constant_values).freeze
1628
1824
  end
1629
1825
 
1630
1826
  # ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
@@ -1739,7 +1935,7 @@ module Rigor
1739
1935
  path: path,
1740
1936
  line: 1,
1741
1937
  column: 1,
1742
- message: "internal analyzer error: #{e.class}: #{e.message}",
1938
+ message: CrashSignature.check_rule_message(e),
1743
1939
  severity: :error
1744
1940
  )
1745
1941
  ]
@@ -16,6 +16,7 @@ require_relative "../effects/envelope_index"
16
16
  require_relative "../inference/scope_indexer"
17
17
  require_relative "../inference/method_dispatcher/file_folding"
18
18
  require_relative "check_rules"
19
+ require_relative "crash_signature"
19
20
  require_relative "dependency_recorder"
20
21
  require_relative "dependency_source_inference"
21
22
  require_relative "diagnostic"
@@ -228,21 +229,54 @@ module Rigor
228
229
  rescue Errno::ENOENT => e
229
230
  [analyzer_error(path, e.message)]
230
231
  rescue StandardError => e
231
- [analyzer_error(path, "internal analyzer error: #{e.class}: #{e.message}")]
232
+ [analyzer_error(path, CrashSignature.check_rule_message(e))]
232
233
  end
233
234
  private :analyze_body
234
235
 
235
236
  # Read-once snapshot of the per-worker reporters so the caller (or the eventual Phase 4b pool
236
237
  # aggregator) can merge into a single coordinator-side reporter. Both reporters dedupe at write time,
237
238
  # so a post-hoc concat + de-dup at the entry-key level is sound.
239
+ #
240
+ # Issue #696 — `definition_build_failures` rides this channel rather than a coordinator-side snapshot,
241
+ # and it has to. Under the pool the PARENT never analyses a file, so its loader never demands a
242
+ # definition and never enters the failing build; a diagnostic wired off the parent's loader would
243
+ # appear at `--workers=0` and vanish at `--workers=N`, which is the same "reports less depending on
244
+ # how you ran it" defect the diagnostic exists to end. Draining it out of the workers is what makes
245
+ # the two paths say the same thing. The payload is `[String, String, String, Array<String>]` tuples —
246
+ # Marshal-clean for the fork backend and shareable for the Ractor one.
247
+ #
248
+ # Issue #784 — `hkt_scan_failure` rides the same channel for the same reason: the PARENT's own
249
+ # Environment never demands `#hkt_registry` under the pool, so its slot is always nil, and draining it
250
+ # out of the workers is the only way `--workers=N` says what `--workers=0` says.
251
+ #
252
+ # Issue #805 — the Marshal-clean requirement binds every stream here, not just the ones written for it.
253
+ # The `RbsExtended::Reporter`'s unresolved / lossy-projection entries used to carry the `RBS::Location`
254
+ # itself, and `Marshal.dump` on one raises `TypeError` (a C-extension object with no `_dump`), so a
255
+ # project whose `sig/` produced a single such event killed each worker HERE — after its files were
256
+ # analysed — and the run degraded to in-process re-analysis with a `pool-degraded` row. All three
257
+ # streams now carry `(path, line, column)` primitives.
238
258
  def drain_reporters
259
+ # Issue #784 — demand the registry once per worker before reading the slot, so a worker whose share
260
+ # of files happened to contain no `Klass.method` call still reports the run's outcome (the same
261
+ # reason `PoolCoordinator#hkt_scan_outcome` demands it on the sequential path). The Environment
262
+ # object is built in the constructor, but its RBS env is lazy: on a worker that analysed a file this
263
+ # is a memoised read; on one that demanded nothing it is the same cache-served env load that file
264
+ # would have paid. The fork backend never hands a worker an empty slice, so no idle worker pays it.
265
+ @environment&.hkt_registry
239
266
  {
240
267
  rbs_extended: {
241
268
  unresolved_payloads: @rbs_extended_reporter.unresolved_payloads,
242
- lossy_projections: @rbs_extended_reporter.lossy_projections
269
+ lossy_projections: @rbs_extended_reporter.lossy_projections,
270
+ # Issue #785 — the HKT directive stream rides the same channel for the same reason
271
+ # `hkt_scan_failure` does: the directives are read by the registry scan, and under the pool the
272
+ # PARENT never demands that scan, so a diagnostic wired off the parent's reporter would appear
273
+ # at `--workers=0` and vanish at `--workers=N`. The demand above is what fills this.
274
+ hkt_directive_errors: @rbs_extended_reporter.hkt_directive_errors
243
275
  },
244
276
  boundary_cross: @boundary_cross_reporter.entries,
245
- source_rbs_synthesis: @source_rbs_synthesis_reporter.entries
277
+ source_rbs_synthesis: @source_rbs_synthesis_reporter.entries,
278
+ definition_build_failures: @environment&.rbs_loader&.definition_build_failures || [],
279
+ hkt_scan_failure: @environment&.hkt_scan_failure
246
280
  }
247
281
  end
248
282
 
@@ -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(
@@ -52,6 +52,15 @@ module Rigor
52
52
  Rigor::Inference::HktBodyParser.parse(CSV_PARSED_BODY, params: [:K])
53
53
  end
54
54
 
55
+ # `csv::row[K]` — `Array[K | nil]` (CSV.parse_line's shape:
56
+ # an Array of optionally-nil cell values for a single row).
57
+ CSV_ROW_BODY = "Array[K | nil]"
58
+ private_constant :CSV_ROW_BODY
59
+
60
+ def csv_row_body_tree
61
+ Rigor::Inference::HktBodyParser.parse(CSV_ROW_BODY, params: [:K])
62
+ end
63
+
55
64
  def json_value_registration
56
65
  Rigor::Inference::HktRegistry::Registration.new(
57
66
  uri: :"json::value",
@@ -90,6 +99,25 @@ module Rigor
90
99
  )
91
100
  end
92
101
 
102
+ def csv_row_registration
103
+ Rigor::Inference::HktRegistry::Registration.new(
104
+ uri: :"csv::row",
105
+ arity: 1,
106
+ variance: [:out],
107
+ bound: Rigor::Type::Combinator.untyped
108
+ )
109
+ end
110
+
111
+ def csv_row_definition
112
+ Rigor::Inference::HktRegistry.definition_with_body_tree(
113
+ uri: :"csv::row",
114
+ params: [:K],
115
+ body_tree: csv_row_body_tree,
116
+ source_path: __FILE__,
117
+ source_line: __LINE__ - 5
118
+ )
119
+ end
120
+
93
121
  # @return [Rigor::Inference::HktRegistry] frozen registry
94
122
  # pre-seeded with all bundled HKT registrations +
95
123
  # bodies. Allocated fresh each call rather than
@@ -104,8 +132,8 @@ module Rigor
104
132
  # once ADR-15 phase 4b.x covers the dependency graph.
105
133
  def registry
106
134
  Rigor::Inference::HktRegistry.new(
107
- registrations: [json_value_registration, csv_parsed_registration],
108
- definitions: [json_value_definition, csv_parsed_definition]
135
+ registrations: [json_value_registration, csv_parsed_registration, csv_row_registration],
136
+ definitions: [json_value_definition, csv_parsed_definition, csv_row_definition]
109
137
  )
110
138
  end
111
139
 
@@ -160,6 +188,14 @@ module Rigor
160
188
  }.freeze
161
189
  private_constant :CSV_PARSED_SPEC
162
190
 
191
+ CSV_ROW_SPEC = {
192
+ uri: :"csv::row",
193
+ args: ["String"],
194
+ discriminator: nil,
195
+ post_reduce: nil
196
+ }.freeze
197
+ private_constant :CSV_ROW_SPEC
198
+
163
199
  METHOD_RETURN_OVERRIDES = {
164
200
  # JSON — stdlib's `json` library. Upstream rbs declares
165
201
  # `(string, ?options) -> untyped`; the HKT-builtin tier
@@ -190,17 +226,20 @@ module Rigor
190
226
  ["YAML", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
191
227
  ["Psych", :safe_load, :singleton] => YAML_SAFE_VALUE_SPEC,
192
228
  ["Psych", :safe_load_file, :singleton] => YAML_SAFE_VALUE_SPEC,
193
- # CSV.parse / CSV.read — no-headers shape only.
229
+ # CSV.parse / CSV.read / CSV.readlines / CSV.parse_line — no-headers shape only.
194
230
  # Upstream rbs declares broader return shapes but
195
231
  # the common case is `Array[Array[String?]]` which
196
- # the `csv::parsed[String]` URI matches. The
232
+ # the `csv::parsed[String]` URI matches, or
233
+ # `Array[String?]` which `csv::row[String]` matches. The
197
234
  # `headers: true` shape (`CSV::Table` of `CSV::Row`)
198
235
  # is NOT covered — calls passing the option fall
199
236
  # through to the upstream RBS type. CSV.foreach also
200
237
  # falls through (it yields rows rather than
201
238
  # returning a typed structure).
202
- ["CSV", :parse, :singleton] => CSV_PARSED_SPEC,
203
- ["CSV", :read, :singleton] => CSV_PARSED_SPEC
239
+ ["CSV", :parse, :singleton] => CSV_PARSED_SPEC,
240
+ ["CSV", :read, :singleton] => CSV_PARSED_SPEC,
241
+ ["CSV", :readlines, :singleton] => CSV_PARSED_SPEC,
242
+ ["CSV", :parse_line, :singleton] => CSV_ROW_SPEC
204
243
  }.freeze
205
244
 
206
245
  # @return [Rigor::Type, nil] the reduced HKT type for
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "strscan"
4
4
 
5
+ require_relative "../rbs_extended/reporter"
5
6
  require_relative "../type"
6
7
  require_relative "../type_node"
7
8
 
@@ -558,11 +559,14 @@ module Rigor
558
559
  result
559
560
  end
560
561
 
561
- # ADR-13 slice 3b — record one `dynamic.shape.lossy-projection` event per (head,
562
- # source_location) pair when the projection actually degraded. The builders return
563
- # the source carrier unchanged on non-HashShape / non-Tuple receivers (see
564
- # `Type::Combinator.pick_of` / `omit_of` and the HashShape-only `partial_of` /
565
- # `required_of` / `readonly_of`), so detection is "first arg was lossy".
562
+ # ADR-13 slice 3b — record one `dynamic.shape.lossy-projection` event per (head, position) pair when
563
+ # the projection actually degraded. The builders return the source carrier unchanged on
564
+ # non-HashShape / non-Tuple receivers (see `Type::Combinator.pick_of` / `omit_of` and the
565
+ # HashShape-only `partial_of` / `required_of` / `readonly_of`), so detection is "first arg was lossy".
566
+ #
567
+ # Issue #805 — the `RBS::Location` is flattened to `(path, line, column)` here, at record time: the
568
+ # entry is drained out of every fork-pool worker, and a location survives neither the drain's
569
+ # `Marshal.dump` nor its cross-worker dedup (see {Rigor::RbsExtended::Reporter}).
566
570
  def record_lossy_projection_if_applicable(node, args, result)
567
571
  return if @reporter.nil?
568
572
  return if result.nil?
@@ -570,10 +574,8 @@ module Rigor
570
574
  return if args.empty?
571
575
  return unless Type::Combinator.shape_projection_lossy?(args.first)
572
576
 
573
- @reporter.record_lossy_projection(
574
- head: node.head,
575
- source_location: @source_location
576
- )
577
+ path, line, column = RbsExtended::Reporter.position_of(@source_location)
578
+ @reporter.record_lossy_projection(head: node.head, path: path, line: line, column: column)
577
579
  end
578
580
 
579
581
  def try_parametric_int_builder(node)