rigortype 0.3.6 → 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 (213) 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/manual/02-cli-reference.md +16 -7
  15. data/docs/manual/05-inspecting-types.md +20 -8
  16. data/docs/manual/07-plugins.md +4 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  18. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  20. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  21. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  22. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  24. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  28. data/lib/rigor/analysis/check_rules.rb +383 -48
  29. data/lib/rigor/analysis/crash_signature.rb +116 -0
  30. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  32. data/lib/rigor/analysis/incremental.rb +28 -0
  33. data/lib/rigor/analysis/incremental_session.rb +44 -9
  34. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  35. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  36. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  37. data/lib/rigor/analysis/result.rb +32 -0
  38. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  43. data/lib/rigor/analysis/runner.rb +147 -10
  44. data/lib/rigor/analysis/worker_session.rb +10 -1
  45. data/lib/rigor/bleeding_edge.rb +7 -6
  46. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  48. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  49. data/lib/rigor/cache/descriptor.rb +42 -2
  50. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  51. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  52. data/lib/rigor/cache/store.rb +10 -1
  53. data/lib/rigor/cli/coverage_command.rb +23 -18
  54. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  55. data/lib/rigor/cli/coverage_scan.rb +47 -6
  56. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  57. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  58. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  59. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  60. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  61. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  62. data/lib/rigor/cli/type_of_command.rb +188 -41
  63. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  64. data/lib/rigor/cli/type_scan_command.rb +10 -1
  65. data/lib/rigor/cli.rb +1 -1
  66. data/lib/rigor/configuration.rb +7 -4
  67. data/lib/rigor/effects/plugin_facts.rb +1 -1
  68. data/lib/rigor/effects/scanner.rb +2 -3
  69. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  70. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  71. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  72. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  73. data/lib/rigor/environment/rbs_loader.rb +525 -46
  74. data/lib/rigor/environment.rb +38 -15
  75. data/lib/rigor/inference/acceptance.rb +172 -20
  76. data/lib/rigor/inference/captured_locals.rb +62 -0
  77. data/lib/rigor/inference/content_join.rb +347 -0
  78. data/lib/rigor/inference/def_handle.rb +15 -3
  79. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  80. data/lib/rigor/inference/expression_typer.rb +1020 -141
  81. data/lib/rigor/inference/fork_map.rb +6 -1
  82. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  83. data/lib/rigor/inference/hkt_registry.rb +46 -0
  84. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  85. data/lib/rigor/inference/index_write_widening.rb +48 -0
  86. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  89. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  93. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  95. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  96. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  97. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  98. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  99. data/lib/rigor/inference/mutation_widening.rb +244 -174
  100. data/lib/rigor/inference/narrowing.rb +331 -27
  101. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  102. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  103. data/lib/rigor/inference/precision_scanner.rb +5 -2
  104. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  105. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  106. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  107. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  108. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  109. data/lib/rigor/inference/version_guard.rb +229 -0
  110. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  111. data/lib/rigor/plugin/base.rb +3 -2
  112. data/lib/rigor/plugin/inflector.rb +14 -5
  113. data/lib/rigor/plugin/io_boundary.rb +107 -4
  114. data/lib/rigor/plugin/loader.rb +14 -0
  115. data/lib/rigor/protection/analysis_guard.rb +60 -0
  116. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  117. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  118. data/lib/rigor/protection/discovery_seed.rb +1 -1
  119. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  120. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  121. data/lib/rigor/protection/mutator.rb +121 -18
  122. data/lib/rigor/reflection.rb +182 -30
  123. data/lib/rigor/scope/discovery_index.rb +39 -1
  124. data/lib/rigor/scope.rb +385 -4
  125. data/lib/rigor/sig_gen/classification.rb +12 -1
  126. data/lib/rigor/sig_gen/generator.rb +254 -5
  127. data/lib/rigor/sig_gen/renderer.rb +1 -6
  128. data/lib/rigor/sig_gen/writer.rb +3 -0
  129. data/lib/rigor/source/constant_path.rb +79 -2
  130. data/lib/rigor/triage/catalogue.rb +1 -1
  131. data/lib/rigor/type/combinator.rb +10 -0
  132. data/lib/rigor/type/maybe.rb +47 -0
  133. data/lib/rigor/type/refined.rb +1 -2
  134. data/lib/rigor/type/result.rb +53 -0
  135. data/lib/rigor/type.rb +2 -0
  136. data/lib/rigor/version.rb +1 -1
  137. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  139. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  140. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  141. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  142. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  147. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  148. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  150. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  151. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  152. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  153. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  154. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  156. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  157. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  159. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  160. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  163. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  164. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  165. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  166. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  167. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  168. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  169. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  170. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  172. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  173. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  174. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  175. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  176. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  177. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  178. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  179. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  180. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  181. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  182. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  183. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  184. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  185. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  186. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  187. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  188. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  189. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  190. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  191. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  192. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  193. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  194. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  195. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  196. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  200. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  201. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  202. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  203. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  204. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  205. data/sig/rigor/analysis/baseline.rbs +1 -1
  206. data/sig/rigor/environment.rbs +3 -0
  207. data/sig/rigor/inference.rbs +4 -1
  208. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  209. data/sig/rigor/reflection.rbs +3 -1
  210. data/sig/rigor/scope.rbs +25 -1
  211. data/sig/rigor/type.rbs +95 -1
  212. data/sig/rigor.rbs +16 -0
  213. metadata +69 -3
@@ -126,6 +126,10 @@ module Rigor
126
126
  snapshot_project_signature_state(environment)
127
127
  snapshot_effect_annotation_carrier(environment&.rbs_loader)
128
128
  result = files.flat_map { |path| @analyze_file.call(path, environment) }
129
+ # Issue #696 — AFTER the loop, deliberately. Definition builds are lazy (ADR-54 WD1: per class, on
130
+ # first demand), so a snapshot taken beside the ones above — which run BEFORE `files.flat_map` —
131
+ # would read an empty list on every run, including the ones this diagnostic exists for.
132
+ record_definition_build_failures(environment&.rbs_loader&.definition_build_failures)
129
133
  if @collect_stats
130
134
  loader = environment.rbs_loader
131
135
  @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
@@ -567,6 +571,11 @@ module Rigor
567
571
  snapshot_effect_annotation_carrier(environment.rbs_loader)
568
572
  diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
569
573
  loader = environment.rbs_loader
574
+ # Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
575
+ # failing build, and it must snapshot the same slot the two real paths do. Reached whenever
576
+ # `fork` is unavailable (Windows) and on `--incremental` / effects runs without it: a run that
577
+ # degraded to sequential must not also report less than a sequential run would.
578
+ record_definition_build_failures(loader&.definition_build_failures)
570
579
  @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
571
580
  @snapshots.signature_paths = loader&.signature_paths || [].freeze
572
581
  @snapshots.quarantined_signatures =
@@ -608,10 +617,35 @@ module Rigor
608
617
  plugin_id: entry.plugin_id, path: entry.path, message: entry.message
609
618
  )
610
619
  end
620
+ # Issue #696. Fetched with a default so an older drain stays compatible, exactly as the line above.
621
+ record_definition_build_failures(drained[:definition_build_failures])
611
622
  end
612
623
 
613
624
  private
614
625
 
626
+ # Issue #696 — accumulate the per-class `RBS::DefinitionBuilder` failures this run observed, deduped
627
+ # by class name and kept in first-seen order.
628
+ #
629
+ # Accumulating rather than assigning is what makes a pooled run say what a sequential one says: each
630
+ # worker holds its OWN loader and its own memo (the `@state` that also gates the stderr banner, which
631
+ # is why the banner repeats per worker — #295), so a class that fails in two workers arrives twice
632
+ # and a class only one worker touched arrives once. The union over the workers is the set the RUN
633
+ # hit; neither worker alone is.
634
+ #
635
+ # NOT gated on `project_signature_paths?`, unlike {#snapshot_project_signature_state}. That gate
636
+ # exists because each condition there forces the otherwise-lazy env build, which would warm the cache
637
+ # on a project with no sig set of its own. Nothing is forced here — this reads what the analysis
638
+ # already demanded — and the failure is not a project-sig-only condition anyway: two BUNDLED sources
639
+ # declaring the same method collapse a class with no `signature_paths:` in sight (the
640
+ # `bigdecimal` / `BigMath` shape, #299). Gating it would suppress exactly the variant the user cannot
641
+ # fix by editing their own `sig/`.
642
+ def record_definition_build_failures(failures)
643
+ return if failures.nil? || failures.empty?
644
+
645
+ @snapshots.definition_build_failures =
646
+ (@snapshots.definition_build_failures + failures).uniq(&:first).freeze
647
+ end
648
+
615
649
  # True when the project declares its own `signature_paths:` (the only place the
616
650
  # qualified-name-without-namespace mistake lives).
617
651
  def project_signature_paths?
@@ -53,17 +53,11 @@ module Rigor
53
53
  # consumes them and so never pays the double parse (see {#discover} + `Runner#ensure_project_discovery`).
54
54
  # The slot names mirror the discovery half of {Result} exactly.
55
55
  Discovery = Data.define(
56
- :discovered_classes,
57
- :discovered_def_nodes,
58
- :discovered_singleton_def_nodes,
59
- :discovered_def_sources,
60
- :discovered_singleton_def_sources,
61
- :discovered_superclasses,
62
- :discovered_includes,
63
- :discovered_class_sources,
64
- :discovered_method_visibilities,
65
- :discovered_methods,
66
- :data_member_layouts,
56
+ :discovered_classes, :discovered_def_nodes, :discovered_def_nestings,
57
+ :discovered_singleton_def_nodes, :discovered_def_sources, :discovered_singleton_def_sources,
58
+ :discovered_superclasses, :discovered_header_nestings, :discovered_includes,
59
+ :discovered_class_sources, :constant_values, :constant_sources, :constant_writes,
60
+ :discovered_method_visibilities, :discovered_methods, :data_member_layouts,
67
61
  :struct_member_layouts
68
62
  )
69
63
 
@@ -156,12 +150,22 @@ module Rigor
156
150
  Discovery.new(
157
151
  discovered_classes: index.fetch(:classes),
158
152
  discovered_def_nodes: def_index.fetch(:def_nodes),
153
+ # Issue #681 — the recorded `Module.nesting` per declared `def`, so a callee's body re-walk
154
+ # resolves its constants the way the declaration that owns it does.
155
+ discovered_def_nestings: def_index.fetch(:def_nestings),
159
156
  discovered_singleton_def_nodes: def_index.fetch(:singleton_def_nodes),
160
157
  discovered_def_sources: def_index.fetch(:def_sources),
161
158
  discovered_singleton_def_sources: def_index.fetch(:singleton_def_sources),
162
159
  discovered_superclasses: def_index.fetch(:superclasses),
160
+ # Issue #682 — the nesting each declaration header is written in, so an ancestor name resolves in
161
+ # the cref Ruby resolves it in rather than by peeling the subclass's own qualified name.
162
+ discovered_header_nestings: def_index.fetch(:header_nestings),
163
163
  discovered_includes: def_index.fetch(:includes),
164
164
  discovered_class_sources: def_index.fetch(:class_sources),
165
+ # Issue #644 — the cross-file value-constant publication table and its write attribution.
166
+ constant_values: def_index.fetch(:constant_values),
167
+ constant_sources: def_index.fetch(:constant_sources),
168
+ constant_writes: def_index.fetch(:constant_writes),
165
169
  discovered_method_visibilities: def_index.fetch(:method_visibilities),
166
170
  discovered_methods: def_index.fetch(:methods),
167
171
  data_member_layouts: def_index.fetch(:data_member_layouts),
@@ -17,12 +17,15 @@ module Rigor
17
17
  class RunSnapshots
18
18
  attr_accessor :class_decl_paths, :signature_paths,
19
19
  :synthesized_namespaces, :quarantined_signatures, :conformance_results,
20
- :env_build_failure, :effect_annotation_carrier
20
+ :env_build_failure, :definition_build_failures, :effect_annotation_carrier
21
21
 
22
22
  # Constructor defaults match the {Runner} constructor: the pre-seed values `build_run_stats` /
23
23
  # `pre_file_diagnostics` read before the first analysis path runs are frozen empties. The
24
24
  # `env_build_failure` slot is nil (no failure) rather than an empty collection — it holds a single
25
- # `[error_class, first_line, buffer_names]` tuple or nothing.
25
+ # `[error_class, first_line, buffer_names]` tuple or nothing. Its per-class sibling
26
+ # `definition_build_failures` (#696) holds a LIST, because a collapsed universe fails many classes,
27
+ # and is accumulated ACROSS pool workers rather than assigned once — see
28
+ # {PoolCoordinator#merge_worker_reporters}.
26
29
  def initialize
27
30
  @class_decl_paths = {}.freeze
28
31
  @signature_paths = [].freeze
@@ -30,6 +33,7 @@ module Rigor
30
33
  @quarantined_signatures = [].freeze
31
34
  @conformance_results = [].freeze
32
35
  @env_build_failure = nil
36
+ @definition_build_failures = [].freeze
33
37
  @effect_annotation_carrier = [].freeze
34
38
  end
35
39
 
@@ -42,6 +46,7 @@ module Rigor
42
46
  @quarantined_signatures = []
43
47
  @conformance_results = []
44
48
  @env_build_failure = nil
49
+ @definition_build_failures = []
45
50
  @effect_annotation_carrier = [].freeze
46
51
  end
47
52
  end
@@ -344,16 +344,31 @@ module Rigor
344
344
  @cached_plugin_prepare_diagnostics = [].freeze
345
345
  @project_discovered_classes = {}.freeze
346
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
347
349
  @project_discovered_singleton_def_nodes = {}.freeze
348
350
  @project_discovered_def_sources = {}.freeze
349
351
  @project_discovered_singleton_def_sources = {}.freeze
350
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
351
355
  @project_discovered_includes = {}.freeze
352
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
353
366
  @project_discovered_method_visibilities = {}.freeze
354
367
  @project_discovered_methods = {}.freeze
355
368
  @project_data_member_layouts = {}.freeze
356
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
357
372
  # ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
358
373
  # `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
359
374
  # scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
@@ -404,6 +419,7 @@ module Rigor
404
419
  return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
405
420
 
406
421
  expansion = expand_paths(paths)
422
+ @project_discovery_expansion = project_discovery_expansion(paths, expansion)
407
423
  @snapshots.reset_for_run
408
424
  # Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
409
425
  @project_discovery_done = false
@@ -530,6 +546,22 @@ module Rigor
530
546
  result.transform_values(&:freeze).freeze
531
547
  end
532
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
+
533
565
  # ADR-67 WD6c lift — the inferred-param table this run seeded from (frozen; empty when
534
566
  # `parameter_inference:` is off or the pre-pass failed soft). The incremental session reads it back
535
567
  # after a baseline so the snapshot records the seeds the cached diagnostics were computed under.
@@ -637,6 +669,8 @@ module Rigor
637
669
  def evaluate_return_types_setup(paths, specs)
638
670
  expansion = expand_paths(paths || @configuration.paths)
639
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
640
674
  @run_generation = Object.new.freeze
641
675
  run_project_pre_passes(expansion: expansion)
642
676
  ensure_project_discovery(expansion)
@@ -959,6 +993,10 @@ module Rigor
959
993
  close_effect_graph
960
994
  diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
961
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
962
1000
  diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
963
1001
  diagnostics += @diagnostic_aggregator.conforms_to_diagnostics
964
1002
  diagnostics += @diagnostic_aggregator.rbs_extended_reporter_diagnostics
@@ -1081,15 +1119,33 @@ module Rigor
1081
1119
  end
1082
1120
 
1083
1121
  def build_run_dependency_descriptor(expansion, rbs_descriptor)
1084
- 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 = []
1085
1125
  @plugin_registry.plugins.each do |plugin|
1086
1126
  # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
1087
1127
  # are frozen after the run, and a plugin that never built a boundary read no files through it,
1088
1128
  # so it contributes no dependencies.
1089
1129
  boundary = plugin.instance_variable_get(:@io_boundary)
1090
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
1091
1134
  end
1092
- 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
+ )
1093
1149
  end
1094
1150
 
1095
1151
  def analyzed_file_entries(expansion)
@@ -1185,12 +1241,18 @@ module Rigor
1185
1241
  def apply_discovery_result(discovery)
1186
1242
  @project_discovered_classes = discovery.discovered_classes
1187
1243
  @project_discovered_def_nodes = discovery.discovered_def_nodes
1244
+ @project_discovered_def_nestings = discovery.discovered_def_nestings
1188
1245
  @project_discovered_singleton_def_nodes = discovery.discovered_singleton_def_nodes
1189
1246
  @project_discovered_def_sources = discovery.discovered_def_sources
1190
1247
  @project_discovered_singleton_def_sources = discovery.discovered_singleton_def_sources
1191
1248
  @project_discovered_superclasses = discovery.discovered_superclasses
1249
+ @project_discovered_header_nestings = discovery.discovered_header_nestings
1192
1250
  @project_discovered_includes = discovery.discovered_includes
1193
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
1194
1256
  @project_discovered_method_visibilities = discovery.discovered_method_visibilities
1195
1257
  @project_discovered_methods = discovery.discovered_methods
1196
1258
  @project_data_member_layouts = discovery.data_member_layouts
@@ -1210,6 +1272,8 @@ module Rigor
1210
1272
  return if @project_discovery_done
1211
1273
 
1212
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
1213
1277
  if @collect_seed_bundles
1214
1278
  # ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
1215
1279
  # capture the refreshed bundle set for the session to persist.
@@ -1222,6 +1286,39 @@ module Rigor
1222
1286
  end
1223
1287
  end
1224
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
+
1225
1322
  # ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
1226
1323
  # analysis assembly (`Runner#symbol_fingerprints` / `#class_declarations`, consumed by
1227
1324
  # {IncrementalSession} after the run), so they force the build eagerly — matching the pre-slice-1
@@ -1232,7 +1329,8 @@ module Rigor
1232
1329
  end
1233
1330
 
1234
1331
  private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
1235
- :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?
1236
1334
 
1237
1335
  # Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
1238
1336
  # `version:`. Prism exposes no version list, so the floor is found empirically — only when a
@@ -1363,6 +1461,7 @@ module Rigor
1363
1461
  synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
1364
1462
  quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
1365
1463
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
1464
+ definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
1366
1465
  conformance_results_snapshot: -> { @snapshots.conformance_results }
1367
1466
  )
1368
1467
  end
@@ -1581,6 +1680,9 @@ module Rigor
1581
1680
  tables[:run_generation] = @run_generation if @run_generation
1582
1681
  tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
1583
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
1584
1686
  unless @project_discovered_singleton_def_nodes.empty?
1585
1687
  tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
1586
1688
  end
@@ -1588,6 +1690,9 @@ module Rigor
1588
1690
  unless @project_discovered_superclasses.empty?
1589
1691
  tables[:discovered_superclasses] = @project_discovered_superclasses
1590
1692
  end
1693
+ unless @project_discovered_header_nestings.empty?
1694
+ tables[:discovered_header_nestings] = @project_discovered_header_nestings
1695
+ end
1591
1696
  tables[:discovered_includes] = @project_discovered_includes unless @project_discovered_includes.empty?
1592
1697
  unless @project_discovered_method_visibilities.empty?
1593
1698
  tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
@@ -1595,14 +1700,22 @@ module Rigor
1595
1700
  tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
1596
1701
  seed_opt_in_pre_pass_tables(tables)
1597
1702
  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
1703
+ seed_dependency_attribution_tables(tables)
1603
1704
  tables
1604
1705
  end
1605
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
+
1606
1719
  # Issue #260 — the mutable starting point {#project_scope_seed_tables} fills in: a copy of the opt-in
1607
1720
  # `discovery_seed:` when one was supplied, otherwise the empty Hash every other run has always started
1608
1721
  # from. Extracted so the seed costs {#project_scope_seed_tables} no branch of its complexity budget.
@@ -1622,9 +1735,33 @@ module Rigor
1622
1735
  # Extracted to keep {#project_scope_seed_tables} under the complexity budget.
1623
1736
  def seed_opt_in_pre_pass_tables(tables)
1624
1737
  tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
1625
- 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?
1626
1763
 
1627
- tables[:in_source_constants] = @project_pre_eval_constants
1764
+ @project_pre_eval_constants.merge(@project_constant_values).freeze
1628
1765
  end
1629
1766
 
1630
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(
@@ -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