rigortype 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
  14. data/docs/handbook/02-everyday-types.md +1 -1
  15. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  16. data/docs/handbook/08-understanding-errors.md +1 -1
  17. data/docs/handbook/09-plugins.md +2 -2
  18. data/docs/handbook/10-sorbet.md +1 -1
  19. data/docs/handbook/README.md +2 -2
  20. data/docs/handbook/appendix-go.md +1 -1
  21. data/docs/handbook/appendix-java-csharp.md +2 -2
  22. data/docs/handbook/appendix-mypy.md +1 -1
  23. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  24. data/docs/handbook/appendix-rust.md +1 -1
  25. data/docs/handbook/appendix-type-theory.md +2 -2
  26. data/docs/handbook/appendix-typescript.md +4 -4
  27. data/docs/manual/02-cli-reference.md +29 -16
  28. data/docs/manual/03-configuration.md +1 -1
  29. data/docs/manual/04-diagnostics.md +12 -0
  30. data/docs/manual/05-inspecting-types.md +20 -8
  31. data/docs/manual/07-plugins.md +4 -0
  32. data/docs/manual/08-skills.md +1 -1
  33. data/docs/manual/11-ci.md +1 -1
  34. data/docs/manual/12-caching.md +1 -1
  35. data/docs/manual/15-type-protection-coverage.md +1 -1
  36. data/docs/manual/18-removing-dead-code.md +13 -4
  37. data/docs/manual/19-effect-labels.md +19 -12
  38. data/docs/manual/README.md +2 -2
  39. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  40. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  41. data/docs/manual/plugins/rigor-activejob.md +3 -3
  42. data/docs/manual/plugins/rigor-activerecord.md +78 -5
  43. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  44. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  45. data/docs/manual/plugins/rigor-rspec.md +6 -2
  46. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  47. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  48. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  49. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  50. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  51. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  52. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  53. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  54. data/lib/rigor/analysis/check_rules.rb +383 -48
  55. data/lib/rigor/analysis/crash_signature.rb +116 -0
  56. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  57. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  58. data/lib/rigor/analysis/diagnostic.rb +17 -6
  59. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  60. data/lib/rigor/analysis/incremental.rb +28 -0
  61. data/lib/rigor/analysis/incremental_session.rb +44 -9
  62. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  63. data/lib/rigor/analysis/reachability/graph.rb +44 -16
  64. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  65. data/lib/rigor/analysis/reachability/scan.rb +27 -7
  66. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  67. data/lib/rigor/analysis/result.rb +32 -0
  68. data/lib/rigor/analysis/run_cache_key.rb +53 -1
  69. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  70. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
  71. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  72. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  73. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  74. data/lib/rigor/analysis/runner.rb +298 -38
  75. data/lib/rigor/analysis/worker_session.rb +10 -1
  76. data/lib/rigor/bleeding_edge.rb +7 -6
  77. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  78. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  79. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  80. data/lib/rigor/cache/descriptor.rb +42 -2
  81. data/lib/rigor/cache/file_digest.rb +20 -2
  82. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  83. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  84. data/lib/rigor/cache/store.rb +10 -1
  85. data/lib/rigor/cli/check_command.rb +67 -48
  86. data/lib/rigor/cli/coverage_command.rb +28 -24
  87. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  88. data/lib/rigor/cli/coverage_scan.rb +47 -6
  89. data/lib/rigor/cli/doc_links.rb +100 -0
  90. data/lib/rigor/cli/docs_command.rb +32 -2
  91. data/lib/rigor/cli/effects_command.rb +27 -2
  92. data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
  93. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  94. data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
  95. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  96. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  97. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  98. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  99. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  100. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  101. data/lib/rigor/cli/type_of_command.rb +188 -41
  102. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  103. data/lib/rigor/cli/type_scan_command.rb +10 -1
  104. data/lib/rigor/cli/unused_command.rb +65 -16
  105. data/lib/rigor/cli.rb +22 -9
  106. data/lib/rigor/configuration.rb +7 -4
  107. data/lib/rigor/effects/definition_lines.rb +100 -0
  108. data/lib/rigor/effects/envelope_check.rb +18 -1
  109. data/lib/rigor/effects/liskov_check.rb +17 -8
  110. data/lib/rigor/effects/plugin_facts.rb +1 -1
  111. data/lib/rigor/effects/scanner.rb +2 -3
  112. data/lib/rigor/effects/signature_sources.rb +13 -2
  113. data/lib/rigor/effects/snapshot.rb +53 -21
  114. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  115. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  116. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  117. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  118. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  119. data/lib/rigor/environment/rbs_loader.rb +525 -46
  120. data/lib/rigor/environment.rb +38 -15
  121. data/lib/rigor/inference/acceptance.rb +172 -20
  122. data/lib/rigor/inference/captured_locals.rb +62 -0
  123. data/lib/rigor/inference/content_join.rb +347 -0
  124. data/lib/rigor/inference/def_handle.rb +15 -3
  125. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  126. data/lib/rigor/inference/expression_typer.rb +1020 -141
  127. data/lib/rigor/inference/fork_map.rb +6 -1
  128. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  129. data/lib/rigor/inference/hkt_registry.rb +46 -0
  130. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  131. data/lib/rigor/inference/index_write_widening.rb +48 -0
  132. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  133. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  134. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  135. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  136. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  137. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  138. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  139. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  140. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  141. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  142. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  143. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  144. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  145. data/lib/rigor/inference/mutation_widening.rb +244 -174
  146. data/lib/rigor/inference/narrowing.rb +331 -27
  147. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  148. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  149. data/lib/rigor/inference/precision_scanner.rb +5 -2
  150. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  151. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  152. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  153. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  154. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  155. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  156. data/lib/rigor/inference/version_guard.rb +229 -0
  157. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  158. data/lib/rigor/plugin/base.rb +3 -2
  159. data/lib/rigor/plugin/inflector.rb +14 -5
  160. data/lib/rigor/plugin/io_boundary.rb +107 -4
  161. data/lib/rigor/plugin/loader.rb +14 -0
  162. data/lib/rigor/protection/analysis_guard.rb +60 -0
  163. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  164. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  165. data/lib/rigor/protection/discovery_seed.rb +1 -1
  166. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  167. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  168. data/lib/rigor/protection/mutator.rb +121 -18
  169. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  170. data/lib/rigor/reflection.rb +182 -30
  171. data/lib/rigor/scope/discovery_index.rb +39 -1
  172. data/lib/rigor/scope.rb +385 -4
  173. data/lib/rigor/sig_gen/classification.rb +12 -1
  174. data/lib/rigor/sig_gen/generator.rb +254 -5
  175. data/lib/rigor/sig_gen/renderer.rb +1 -6
  176. data/lib/rigor/sig_gen/writer.rb +3 -0
  177. data/lib/rigor/source/constant_path.rb +79 -2
  178. data/lib/rigor/triage/catalogue.rb +1 -1
  179. data/lib/rigor/type/combinator.rb +10 -0
  180. data/lib/rigor/type/maybe.rb +47 -0
  181. data/lib/rigor/type/refined.rb +1 -2
  182. data/lib/rigor/type/result.rb +53 -0
  183. data/lib/rigor/type.rb +2 -0
  184. data/lib/rigor/version.rb +1 -1
  185. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  186. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  187. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  188. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  189. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  190. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  191. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  192. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  193. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  194. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  195. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  196. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  197. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  198. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  199. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  200. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  201. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  202. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  203. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  204. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  207. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  208. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  209. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  210. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  211. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  212. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  213. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  214. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  215. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  216. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  217. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  218. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  219. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  220. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  221. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  222. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  223. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  224. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  225. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  226. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  227. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  228. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  229. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  230. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  231. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  232. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  233. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  234. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  235. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  236. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  237. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  238. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  239. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  240. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  241. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  242. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  243. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  244. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  245. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  246. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  247. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  248. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  249. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  250. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  251. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  252. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  253. data/sig/rigor/analysis/baseline.rbs +1 -1
  254. data/sig/rigor/environment.rbs +3 -0
  255. data/sig/rigor/inference.rbs +4 -1
  256. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  257. data/sig/rigor/reflection.rbs +3 -1
  258. data/sig/rigor/scope.rbs +25 -1
  259. data/sig/rigor/type.rbs +95 -1
  260. data/sig/rigor.rbs +16 -0
  261. data/skills/rigor-ci-setup/SKILL.md +2 -2
  262. data/skills/rigor-editor-setup/SKILL.md +2 -2
  263. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  264. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  265. data/skills/rigor-plugin-review/SKILL.md +3 -3
  266. metadata +73 -3
@@ -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
@@ -7,7 +7,9 @@ module Rigor
7
7
  # Refined types for predefined Ruby / stdlib constants whose upstream RBS signatures are
8
8
  # broader than the constants' documented runtime invariants.
9
9
  #
10
- # Resolution is two-tiered:
10
+ # Resolution is two-tiered, and **both tiers are closed tables authored in this file**. A name
11
+ # taken from the analysed program is never resolved against the analyzer's own object space —
12
+ # see *Why the table is closed* below.
11
13
  #
12
14
  # **Tier 1 — exact-value whitelist** (`FOLDED_CONSTANTS`):
13
15
  # Constants whose value is bit-for-bit identical across every Ruby version and platform
@@ -23,24 +25,44 @@ module Rigor
23
25
  # and as plain `Integer`s they fall through Tier 2 to the RBS type harmlessly.
24
26
  # `Complex::I` is deferred (no complex-fold consumer).
25
27
  #
26
- # **Tier 2 — runtime String inspection**:
27
- # For any other constant, the module resolves it via `const_get` against the analyzer's
28
- # own Ruby runtime. Core / stdlib constants (e.g. `RUBY_VERSION`, `RUBY_PLATFORM`) are
29
- # always loaded into the analyzer process; project-defined constants are not (they live
30
- # only in ASTs), so their `const_get` raises `NameError` and the lookup falls through to
31
- # the RBS type tier.
28
+ # **Tier 2 — String refinements for a closed set of interpreter constants**
29
+ # (`RUNTIME_STRING_CONSTANTS`): each listed name is read ONCE, at load time, from the analyzer's
30
+ # own runtime and classified by the value found:
32
31
  #
33
- # For a successfully resolved `String` value:
34
- # - empty string → no refinement (fall through to RBS `String`)
32
+ # - not a String, or an empty one → no entry (the name falls through to the RBS type)
35
33
  # - a Ruby numeric literal → `numeric-string`
36
34
  # - non-empty otherwise → `non-empty-string`
37
35
  #
38
- # **Exclusion set** (`RUNTIME_INSPECTION_EXCLUDED`):
39
- # String constants that appear non-empty in the current runtime but are documented to be
40
- # potentially empty in some build configuration or alternative implementation. Exclusions
41
- # are populated by scanning Ruby's C source (version.c, etc.) and RBS comments for any
42
- # constant whose documentation says "may be empty" or "platform-specific default". None
43
- # are known today; the set exists as a safety net.
36
+ # Admission criterion for the list: the constant's value is fixed by the same interpreter choice
37
+ # that already fixes the core / stdlib RBS the engine reads, and it is non-empty in every build.
38
+ # A third-party gem's constant does NOT qualify the analysed project resolves its own copy
39
+ # through its `Gemfile.lock`, which this layer cannot see. This is the criterion
40
+ # `Inference::VersionGuard::VERSION_CONSTANTS` already applies to the constants it folds guards on.
41
+ #
42
+ # **Why the table is closed** ([#680](https://github.com/rigortype/rigor/issues/680)):
43
+ # Tier 2 used to resolve ANY name reaching it — a name read out of the analysed source — with
44
+ # `const_get` against the analyzer's runtime, guarded by `const_defined?(part, false)`. That
45
+ # guard does not answer the question it was written for: `const_defined?` is true for a
46
+ # **registered but not yet triggered autoload**, so `const_get` fired the autoload and the target
47
+ # file was *executed* inside the analyzer. Analysing CRuby's own `lib/prism` reached
48
+ # `prism/translation/ruby_parser.rb`, which calls `exit` at the top level when `sexp_processor`
49
+ # is absent; `SystemExit` is not a `StandardError`, so neither the rescue in this file nor the
50
+ # runner's per-file rescue saw it and the run stopped with no diagnostics and no summary. The
51
+ # target of an autoload is arbitrary — it may exit, raise, print, mutate global state, or take
52
+ # unbounded time — and `rigor check` is routinely pointed at code the user did not write.
53
+ #
54
+ # Closing the table rather than only guarding the walk is what makes that structural: no name
55
+ # from the analysed program reaches `const_get` at all. A census of what the open walk actually
56
+ # bought says the precision cost is nil — over GitLab's `app` + `lib` (11,189 files, 184,965
57
+ # constant references) it answered for five names and ten references (`RUBY_VERSION`,
58
+ # `RUBY_PLATFORM`, `RUBY_DESCRIPTION`, `File::SEPARATOR`, `File::PATH_SEPARATOR`, every one of
59
+ # them listed below), against 17 references that would have autoloaded `ipaddr`; over Mastodon,
60
+ # three names and five references against 43 such. What it answered for *beyond* the list was
61
+ # always a constant that happened to live in **Rigor's own** bundle — `Rigor::VERSION` and
62
+ # `RBS::VERSION` while Rigor checks itself, rubygems and Bundler internals while it checks
63
+ # CRuby's `lib/` — and that is worse than no answer: identical source would type differently
64
+ # depending on how the analyzer was installed. Removing an ad hoc `Object.const_get` from an
65
+ # analysis path is the same move ADR-4 § 330 already made for predicate narrowing.
44
66
  #
45
67
  # This module is consulted by `Environment#constant_for_name` BEFORE the RBS
46
68
  # constant-type table (widest types) but AFTER in-source constant writes (the user's own
@@ -68,70 +90,97 @@ module Rigor
68
90
 
69
91
  # --- tier 2 -------------------------------------------------------
70
92
 
71
- # String constants whose runtime value is non-empty in the current Ruby but that should
72
- # NOT be narrowed because they are documented to be potentially empty in some build or
73
- # implementation.
93
+ # The closed set of names tier 2 may read, by the admission criterion in the module note.
74
94
  #
75
- # Methodology: grep Ruby's version.c and similar C sources, and the RBS comment corpus,
76
- # for any constant annotated with "may be empty" or "platform-specific default". Add
77
- # the full qualified path (without leading "::") when a genuine risk is found.
78
- RUNTIME_INSPECTION_EXCLUDED = Set[].freeze
79
- private_constant :RUNTIME_INSPECTION_EXCLUDED
95
+ # The `RUBY_*` / `Ruby::*` pairs and `Encoding::UNICODE_VERSION` are defined by the interpreter
96
+ # itself; `File::SEPARATOR` / `Separator` / `PATH_SEPARATOR` / `ALT_SEPARATOR` by its `File`
97
+ # core class (`Separator` is the capital-S alias of `SEPARATOR`, declared in core RBS and just
98
+ # as spellable in project code; `ALT_SEPARATOR` is `nil` off Windows and simply gets no entry
99
+ # there); `Gem::VERSION` by rubygems, which is not the interpreter's to version but IS loaded
100
+ # in every process that can run `rigor` at all. A name that resolves to something other than a
101
+ # non-empty String on the running interpreter is skipped, so listing one costs nothing where it
102
+ # does not exist.
103
+ RUNTIME_STRING_CONSTANTS = %w[
104
+ RUBY_VERSION RUBY_RELEASE_DATE RUBY_PLATFORM RUBY_DESCRIPTION RUBY_COPYRIGHT
105
+ RUBY_ENGINE RUBY_ENGINE_VERSION RUBY_REVISION
106
+ Ruby::VERSION Ruby::RELEASE_DATE Ruby::PLATFORM Ruby::DESCRIPTION Ruby::COPYRIGHT
107
+ Ruby::ENGINE Ruby::ENGINE_VERSION Ruby::REVISION
108
+ File::SEPARATOR File::Separator File::PATH_SEPARATOR File::ALT_SEPARATOR
109
+ Encoding::UNICODE_VERSION
110
+ Gem::VERSION
111
+ ].freeze
112
+ private_constant :RUNTIME_STRING_CONSTANTS
80
113
 
81
114
  NON_EMPTY_STRING = Type::Combinator.non_empty_string.freeze
82
115
  NUMERIC_STRING = Type::Combinator.numeric_string.freeze
83
116
  private_constant :NON_EMPTY_STRING, :NUMERIC_STRING
84
117
 
85
- # --- public API ---------------------------------------------------
86
-
87
- # @param name [String] unqualified constant name (e.g. `"Math::PI"`,
88
- # `"RUBY_VERSION"`, `"Ruby::ENGINE"`)
89
- # @return [Rigor::Type, nil] refined type, or nil to fall through
90
- def self.lookup(name)
91
- FOLDED_CONSTANTS[name] || inspect_runtime_string(name)
92
- end
93
-
94
118
  # --- private ------------------------------------------------------
95
119
 
96
- # Resolves `name` via `const_get` in the analyzer's runtime and returns a refined
97
- # String carrier, or nil.
98
- def self.inspect_runtime_string(name)
99
- return nil if RUNTIME_INSPECTION_EXCLUDED.include?(name)
120
+ # @param value [String] a non-empty string
121
+ # @return [Rigor::Type]
122
+ def self.classify_string(value)
123
+ if Type::Refined.ruby_numeric_literal?(value)
124
+ NUMERIC_STRING
125
+ else
126
+ NON_EMPTY_STRING
127
+ end
128
+ end
129
+ private_class_method :classify_string
100
130
 
131
+ # Reads `name` in the analyzer's runtime, returning its value when that is a non-empty String.
132
+ #
133
+ # Called ONLY with the names in {RUNTIME_STRING_CONSTANTS} — Rigor's own source — and only
134
+ # while this file is being loaded. It is never handed a name from the analysed program.
135
+ #
136
+ # @param name [String] a qualified constant path without a leading "::"
137
+ # @return [String, nil]
138
+ def self.runtime_string_value(name)
101
139
  mod = ::Object
102
140
  name.split("::").each do |part|
103
- # Resolve only constants already present — never let analysing a reference drive
104
- # the analyzer's own runtime to autoload or run a `const_missing` hook. A
105
- # `Digest::UUID` reference in project code otherwise makes `const_get` trigger
106
- # `Digest.const_missing` → `require "digest/uuid"`, and a missing optional library
107
- # raises `LoadError` (a `ScriptError`, not the `NameError` the const_get walk
108
- # expects), which would abort the whole run rather than fall through to the RBS
109
- # tier. `const_defined?(part, false)` answers the same "is this resolvable here"
110
- # question without the side effect — a project-defined constant (the common case)
111
- # is simply absent and returns nil, no exception raised.
112
141
  return nil unless mod.is_a?(::Module) && mod.const_defined?(part, false)
142
+ # `const_defined?` is true for a REGISTERED-BUT-NOT-YET-TRIGGERED autoload, so on its own
143
+ # it does not mean "already in memory" and `const_get` would EXECUTE the target file. Only
144
+ # `Module#autoload?` separates the two: it returns the registered path while the autoload
145
+ # is pending and nil once it has run, so declining on it declines exactly the dangerous
146
+ # case and still resolves a constant genuinely loaded ([#680]).
147
+ return nil if mod.autoload?(part)
113
148
 
114
149
  mod = mod.const_get(part, false)
115
150
  end
116
151
 
117
- return nil unless mod.is_a?(::String) && !mod.empty?
118
-
119
- classify_string(mod)
120
- rescue ::NameError, ::TypeError, ::LoadError
152
+ mod.is_a?(::String) && !mod.empty? ? mod : nil
153
+ rescue ::StandardError, ::ScriptError, ::SystemExit
154
+ # Deliberately wider than the `NameError` / `TypeError` / `LoadError` this used to name.
155
+ # Refining a constant is an optimisation and nothing it can hit justifies stopping the
156
+ # process; `SystemExit` is the one that actually did ([#680]) and is not a `StandardError`,
157
+ # and `ScriptError` covers the `LoadError` family for the same reason. `Interrupt`,
158
+ # `SignalException` and `NoMemoryError` are deliberately NOT caught — a Ctrl-C must keep
159
+ # reaching the user, and an out-of-memory process is not one to keep analysing in.
121
160
  nil
122
161
  end
123
- private_class_method :inspect_runtime_string
162
+ private_class_method :runtime_string_value
163
+
164
+ # @return [Hash{String => Rigor::Type}] frozen, built once at load time.
165
+ def self.build_runtime_string_refinements
166
+ RUNTIME_STRING_CONSTANTS.each_with_object({}) do |name, table|
167
+ value = runtime_string_value(name)
168
+ table[name] = classify_string(value) if value
169
+ end.freeze
170
+ end
171
+ private_class_method :build_runtime_string_refinements
124
172
 
125
- # @param value [String] a non-empty string
126
- # @return [Rigor::Type]
127
- def self.classify_string(value)
128
- if Type::Refined.ruby_numeric_literal?(value)
129
- NUMERIC_STRING
130
- else
131
- NON_EMPTY_STRING
132
- end
173
+ RUNTIME_STRING_REFINEMENTS = build_runtime_string_refinements
174
+ private_constant :RUNTIME_STRING_REFINEMENTS
175
+
176
+ # --- public API ---------------------------------------------------
177
+
178
+ # @param name [String] unqualified constant name (e.g. `"Math::PI"`,
179
+ # `"RUBY_VERSION"`, `"Ruby::ENGINE"`)
180
+ # @return [Rigor::Type, nil] refined type, or nil to fall through
181
+ def self.lookup(name)
182
+ FOLDED_CONSTANTS[name] || RUNTIME_STRING_REFINEMENTS[name]
133
183
  end
134
- private_class_method :classify_string
135
184
  end
136
185
  end
137
186
  end
@@ -66,13 +66,32 @@ module Rigor
66
66
  FILE_NON_EMPTY = ->(_arg_types) { NON_EMPTY_STRING }
67
67
  private_constant :FILE_NON_EMPTY
68
68
 
69
+ STRING_NOMINAL = Type::Combinator.nominal_of("String").freeze
70
+ private_constant :STRING_NOMINAL
71
+
72
+ # `IO.read(name)` / `File.read(name)` return nil ONLY when a `length` argument is given
73
+ # and the stream is at EOF; the no-length form always returns a String (it raises on
74
+ # error). Upstream RBS declares one row with an optional length — `-> String?` for every
75
+ # call shape — so the whole-file idiom `File.read(path, mode: 'rb')` read as nilable and
76
+ # fired `possible-nil-receiver` on working code (redmine's Import#read_file_head, three
77
+ # sites). The refinement drops the nil exactly when no positional length is present; a
78
+ # keyword-only trailing hash (`mode:`, `encoding:`) is not a length. Same family as the
79
+ # staged upstream signature fixes (#159).
80
+ IO_READ_NO_LENGTH = lambda do |arg_types|
81
+ positional = arg_types.grep_v(Type::HashShape)
82
+ positional.size <= 1 ? STRING_NOMINAL : nil
83
+ end
84
+ private_constant :IO_READ_NO_LENGTH
85
+
69
86
  # Frozen ((owner_class_name, method_name, kind) => handler) table. The kind tag is
70
87
  # `:both`, `:singleton`, or `:instance`. New entries SHOULD prefer `:both` unless the
71
88
  # singleton- and instance-side shapes genuinely differ.
72
89
  OVERRIDES = {
73
90
  ["Kernel", :__dir__, :both] => KERNEL_DIR,
74
91
  ["File", :expand_path, :singleton] => FILE_NON_EMPTY,
75
- ["File", :dirname, :singleton] => FILE_NON_EMPTY
92
+ ["File", :dirname, :singleton] => FILE_NON_EMPTY,
93
+ ["IO", :read, :singleton] => IO_READ_NO_LENGTH,
94
+ ["File", :read, :singleton] => IO_READ_NO_LENGTH
76
95
  }.freeze
77
96
  private_constant :OVERRIDES
78
97
 
@@ -38,7 +38,17 @@ module Rigor
38
38
  # whose RBS dangles an interface or type-alias reference gets stubs where an older Rigor discarded the
39
39
  # whole batch and cached an env in which those signatures are inert. Same reasoning as v3: the
40
40
  # marshalled env is the cached value, so it MUST be rebuilt for the fix to take effect.
41
- SCHEMA_VERSION = 6
41
+ # v7: #526 — the def-index seed bundles gain the `:extends` table (`extend M` / `extend self` /
42
+ # bare `module_function`), which `finalize_def_index` folds into the singleton-side method tables. A
43
+ # pre-7 bundle would silently contribute no extends for an unchanged file, so cached bundles must
44
+ # read as misses once and rebuild carrying the slot.
45
+ # v8: #577 / ADR-45 WD1 — a plugin {Plugin::IoBoundary} read that finds its path MISSING now records
46
+ # an absence row ({FileEntry.absent}: `:exists` / `"false"`) into the run-result and plugin-producer
47
+ # dependency descriptors, so a file that appears after a run which probed for it (a `db/schema.rb`
48
+ # added to a schema-less project) invalidates the warm entry. A pre-8 entry carries no absence rows
49
+ # and would validate fresh across exactly that edit, so cached entries must read as misses once and
50
+ # rebuild carrying the rows.
51
+ SCHEMA_VERSION = 8
42
52
 
43
53
  # Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
44
54
  # struct so no caller can mutate after the entry is in a Descriptor.
@@ -54,6 +64,11 @@ module Rigor
54
64
  # `:digest`, while the {Runner} run-dependency descriptor and plugin {IoBoundary} reads use `:stat`.
55
65
  VALID_COMPARATORS = %i[digest stat mtime exists].freeze
56
66
 
67
+ # The two `:exists` values — `File.exist?`'s answer rendered with `to_s`, which is what
68
+ # {Descriptor#file_entry_fresh?} compares the live answer against.
69
+ PRESENT = "true"
70
+ ABSENT = "false"
71
+
57
72
  attr_reader :path, :comparator, :value
58
73
 
59
74
  value_fields :path, :comparator, :value
@@ -80,6 +95,30 @@ module Rigor
80
95
  new(path: path, comparator: :stat, value: packed)
81
96
  end
82
97
 
98
+ # ADR-45 WD1 (#577) — the absence row: fresh while `path` does not exist, stale the moment anything
99
+ # (a file, a directory, a symlink) comes into existence there. {Plugin::IoBoundary#read_file}
100
+ # records one when a read fails because the path is missing, so a value computed on "X is absent"
101
+ # invalidates when X appears. Validation is a single `File.exist?` — no stat tuple, no digest,
102
+ # nothing that can move on an unchanged tree — so an absence row never costs a warm run its hit.
103
+ def self.absent(path:)
104
+ new(path: path, comparator: :exists, value: ABSENT)
105
+ end
106
+
107
+ # ADR-45 WD1b (#613) — {.absent}'s positive twin: fresh while `path` exists, stale once it is gone.
108
+ # {Plugin::IoBoundary#file?} / `#directory?` record one when an existence probe found what it asked
109
+ # for and the plugin never read the bytes (a discovery root it globbed, a config file whose mere
110
+ # presence switched a mode). It is the weakest file row — a rewrite in place leaves it fresh — so it
111
+ # is only ever the row for a dependency that really is on existence alone; a read records the `:stat`
112
+ # row over it.
113
+ def self.present(path:)
114
+ new(path: path, comparator: :exists, value: PRESENT)
115
+ end
116
+
117
+ # @return [Boolean] whether this row records the path as absent (see {.absent}).
118
+ def absent?
119
+ comparator == :exists && value == ABSENT
120
+ end
121
+
83
122
  def to_h
84
123
  { "path" => path, "comparator" => comparator.to_s, "value" => value }
85
124
  end
@@ -280,7 +319,8 @@ module Rigor
280
319
  # version) belong in the cache *key*, not the validated dependency set — so a descriptor carrying any
281
320
  # non-file slot is never considered fresh (it was built wrong for this use). ADR-60 WD3 adds `globs`
282
321
  # alongside `files` as a re-validatable slot: a {GlobEntry} is fresh when re-globbing + re-digesting
283
- # reproduces its recorded value.
322
+ # reproduces its recorded value. ADR-45 WD1 (#577): an absence row ({FileEntry.absent}) is fresh
323
+ # while its path is still missing — the negative half of the dependency set.
284
324
  def fresh?
285
325
  return false unless gems.empty? && plugins.empty? && configs.empty? && dependencies.empty?
286
326
 
@@ -30,9 +30,10 @@ module Rigor
30
30
  # identical to a bare `Digest::SHA256.file`.
31
31
  module FileDigest
32
32
  MEMO_KEY = :rigor_cache_file_digest_memo
33
+ STAT_KEY = :rigor_cache_file_stat_memo
33
34
  INSTANT_KEY = :rigor_cache_recording_instant
34
35
  STRICT_KEY = :rigor_cache_strict_validation
35
- private_constant :MEMO_KEY, :INSTANT_KEY, :STRICT_KEY
36
+ private_constant :MEMO_KEY, :STAT_KEY, :INSTANT_KEY, :STRICT_KEY
36
37
 
37
38
  # Set in the environment to force the strict digest-always validation path for a single run, regardless
38
39
  # of the `cache.validation` config setting (the env wins). The escape hatch for a filesystem whose stat
@@ -47,14 +48,17 @@ module Rigor
47
48
  # digest-always path for `cache.validation: digest`.
48
49
  def self.with_run(strict: false)
49
50
  previous_memo = Thread.current[MEMO_KEY]
51
+ previous_stat = Thread.current[STAT_KEY]
50
52
  previous_instant = Thread.current[INSTANT_KEY]
51
53
  previous_strict = Thread.current[STRICT_KEY]
52
54
  Thread.current[MEMO_KEY] = {}
55
+ Thread.current[STAT_KEY] = {}
53
56
  Thread.current[INSTANT_KEY] = now_ns
54
57
  Thread.current[STRICT_KEY] = strict
55
58
  yield
56
59
  ensure
57
60
  Thread.current[MEMO_KEY] = previous_memo
61
+ Thread.current[STAT_KEY] = previous_stat
58
62
  Thread.current[INSTANT_KEY] = previous_instant
59
63
  Thread.current[STRICT_KEY] = previous_strict
60
64
  end
@@ -102,12 +106,26 @@ module Rigor
102
106
  digest = parsed[0]
103
107
  return hexdigest(path) == digest if strict_validation?
104
108
 
105
- st = File.stat(path)
109
+ st = validation_stat(path)
106
110
  return true if !racy?(parsed) && tuple_matches?(st, parsed)
107
111
 
108
112
  hexdigest(path) == digest
109
113
  end
110
114
 
115
+ # The VALIDATION-side stat, served from the per-run table when one is installed — a collecting run
116
+ # validates the effects entry and the diagnostics entry against the same ~thousands-of-files
117
+ # dependency descriptor, and the second pass is pure repetition under the run's own stable-filesystem
118
+ # premise (see the module doc). The RECORDING side ({.pack_stat}) deliberately keeps its direct
119
+ # `File.stat`: it packs the tuple a *future* run validates, after the content was read, and must
120
+ # describe that moment rather than an earlier probe's. A stat failure propagates un-memoised, exactly
121
+ # as {.hexdigest} treats a read failure.
122
+ def self.validation_stat(path)
123
+ memo = Thread.current[STAT_KEY]
124
+ return File.stat(path) if memo.nil?
125
+
126
+ memo[path] ||= File.stat(path)
127
+ end
128
+
111
129
  def self.recording_instant_ns
112
130
  Thread.current[INSTANT_KEY] || now_ns
113
131
  end
@@ -54,8 +54,37 @@ module Rigor
54
54
  # loads as nil (a clean cold rebuild — no migration). 12: ADR-103 WD13 / issue #382 adds the effects
55
55
  # sidecar — `effect_collections` (the per-file {Rigor::Effects::FileCollection}s a collecting run
56
56
  # produced) and the `effects_identity` they were produced under; a pre-12 blob mismatches the SCHEMA
57
- # gate and loads as nil (a clean cold rebuild — no migration).
58
- SCHEMA = 12
57
+ # gate and loads as nil (a clean cold rebuild — no migration). 13: issue #644 adds `constant_decls`
58
+ # (the per-file constant PUBLICATION CENSUS — `{qualified name => [literal] | :unpublishable}` — whose
59
+ # diff drives the new `constant:` edge's producer) and grows each seed bundle's row grammar by its own
60
+ # `constant_writes` census; a pre-13 blob mismatches the SCHEMA gate and loads as nil (a clean cold
61
+ # rebuild — no migration). 14: issue #707 widens each seed-bundle def row from `[node_id, name,
62
+ # fingerprint]` to `[node_id, name, fingerprint, nesting]`, carrying issue #681's recorded
63
+ # `Module.nesting` so a bundle-served callee resolves its constants under the declaration that owns it
64
+ # instead of a peel of the receiver's name; a pre-14 blob mismatches the SCHEMA gate and loads as nil (a
65
+ # clean cold rebuild — no migration). The bump is what keeps a pre-14 blob REJECTED rather than MISREAD:
66
+ # a three-element row destructures with `nesting` nil, which is indistinguishable from a top-level def,
67
+ # so every unchanged file would silently keep the pre-fix peel. 15: issue #682 gives each seed bundle a
68
+ # `header_nestings` table — the `Module.nesting` each class / module DECLARATION HEADER is written in —
69
+ # so a bundle-served class resolves its superclass and include names in the cref Ruby resolves them in
70
+ # rather than by peeling its own qualified name; a pre-15 blob mismatches the SCHEMA gate and loads as
71
+ # nil (a clean cold rebuild — no migration). Absence here is gradual rather than misread (the resolver
72
+ # falls back to the peel), but a warm run that peels where a cold run does not is exactly the
73
+ # `--verify-incremental` divergence 14 was bumped for.
74
+ # 16: issue #722 residue 1 gives each seed bundle's `superclasses` values a ROOTED MARKER — a leading
75
+ # `::` on an ancestor name the site wrote rooted (`class X < ::Base`, `Class.new(::Parent)`) — so the
76
+ # resolvers anchor it at the top level instead of walking the enclosing nesting. The value stays a
77
+ # String, so a pre-16 blob deserialises cleanly and is exactly why the gate has to reject it: an
78
+ # un-rooted value is indistinguishable from "not rooted", and every unchanged file would keep the
79
+ # pre-fix answer on a warm run while a cold run gave the right one.
80
+ # 17: issue #716 changes what a seed-bundle def row's `nesting` MEANS without changing its shape. A
81
+ # top-level def now records the EMPTY chain (Ruby's `Module.nesting` there is `[]`), so the resolver
82
+ # anchors its constants at the top level instead of peeling the caller's namespace; `nil` narrows to
83
+ # "no chain recorded at all". A pre-17 blob stored `nil` for exactly the top-level defs that now store
84
+ # `[]`, deserialises cleanly, and would send every unchanged file's top-level helper back to the peel
85
+ # on a warm run while a cold run resolved at the top level — the `--verify-incremental` divergence 14
86
+ # was bumped for, in the same table.
87
+ SCHEMA = 17
59
88
 
60
89
  # The persisted per-file state.
61
90
  # `cache` maps an analyzed file to its diagnostics.
@@ -69,6 +98,12 @@ module Rigor
69
98
  # ADR-46 slice 3:
70
99
  # `missing` maps a consumer to Set<"kind:name"> it looked up and missed.
71
100
  # `class_decls` maps a path to Set<qualified class name> it declares.
101
+ # Issue #644:
102
+ # `constant_decls` maps a path to that file's constant PUBLICATION CENSUS,
103
+ # `{qualified name => [literal] | :unpublishable}`. The descriptor is what the recheck diffs: a name's
104
+ # published answer is a function of the whole project's write set for it, so a value edit, a write
105
+ # becoming unpublishable, or a second declarer appearing / going away all move the answer without
106
+ # moving the name set.
72
107
  # ADR-85 WD2:
73
108
  # `seed_bundles` maps an analyzed path to its per-file discovery contribution (plain-data tables +
74
109
  # `(node_id, name, fingerprint)` def-node handles + content digest), so a warm recheck rebuilds the
@@ -103,7 +138,7 @@ module Rigor
103
138
  # and a vocabulary / catalogue / `effects:` change must invalidate the summaries alone.
104
139
  Payload = Data.define(:cache, :sources, :digests, :analyzed,
105
140
  :symbol_sources, :ancestry_sources, :symbol_fingerprints,
106
- :missing, :class_decls, :seed_bundles, :plugin_fact_digest,
141
+ :missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
107
142
  :return_summaries, :param_table,
108
143
  :effect_collections, :effects_identity)
109
144
 
@@ -218,6 +253,7 @@ module Rigor
218
253
  symbol_fingerprints: data[:symbol_fingerprints] || {},
219
254
  missing: data[:missing] || {},
220
255
  class_decls: data[:class_decls] || {},
256
+ constant_decls: data[:constant_decls] || {},
221
257
  seed_bundles: data[:seed_bundles] || {},
222
258
  plugin_fact_digest: data[:plugin_fact_digest],
223
259
  return_summaries: data[:return_summaries] || {},
@@ -241,6 +277,7 @@ module Rigor
241
277
  symbol_fingerprints: payload.symbol_fingerprints,
242
278
  missing: payload.missing,
243
279
  class_decls: payload.class_decls,
280
+ constant_decls: payload.constant_decls,
244
281
  seed_bundles: payload.seed_bundles,
245
282
  plugin_fact_digest: payload.plugin_fact_digest,
246
283
  return_summaries: payload.return_summaries,
@@ -9,11 +9,28 @@ require "rbs"
9
9
  #
10
10
  # Patch policy (purely additive):
11
11
  #
12
- # - `_dump` returns an empty string. The cached env loses per-node source-position info, but Rigor does not
13
- # consult `RBS::Location` from any analysis code path (every diagnostic uses Prism's own location), so the
14
- # loss is inert in practice.
15
- # - `_load` reconstructs a sentinel Location backed by an empty `<cached>` Buffer. Code paths that DID consult
16
- # Location after a cache hit see a benign zero-range value rather than crashing.
12
+ # - `_dump` returns the buffer's NAME and nothing else. Per-node source POSITIONS are still dropped (Rigor
13
+ # does not consult them from any analysis path every diagnostic uses Prism's own location), but the
14
+ # file a declaration came from is cheap to keep and is not inert: `rbs.coverage.definition-build-failed`
15
+ # names the conflicting signature files, and dropping the name made a warm run omit that clause while a
16
+ # cold run printed it, so the same project said different things by cache state (issue #696 review,
17
+ # second pass). It costs cache SIZE: a `_dump` payload is raw bytes, not a linkable object graph, so the
18
+ # path is written once per Location rather than once per buffer. Memoising the coerced name per buffer was
19
+ # tried and changes nothing for exactly that reason. Measured +6.6% on a realistic project (Rigor's own
20
+ # 349-file `lib`: 4,332K to 4,616K) and +12.3% on a one-file project (1,672K to 1,876K), both stable
21
+ # across reps — the env blob is fixed overhead, so the ratio falls as a project's own cached data grows
22
+ # and the realistic figure is the lower one. That is the price of a diagnostic that reads the same warm
23
+ # and cold; positions, which are far more numerous and which nothing reads, stay dropped.
24
+ # - `_load` reconstructs a zero-range Location over that name, falling back to a `<cached>` sentinel when
25
+ # the dump carried none. Code paths that DID consult Location after a cache hit see a benign value rather
26
+ # than crashing, and one that reads `buffer.name` now sees the real path.
27
+ # - Both directions were exercised rather than assumed. A NEW blob read by an OLDER `_load` — which ignored
28
+ # its argument — loads and behaves exactly as before; that direction is clean. An OLD blob read by THIS
29
+ # `_load` also loads without raising, and the sentinel fallback keeps it from ever naming `<cached>` as a
30
+ # path — but the conflicting-files clause then goes missing, so a warm run off a pre-change blob says less
31
+ # than a cold run does. ADR-6's store never evicts, so that would persist. `Cache::Store::FORMAT_VERSION`
32
+ # is therefore bumped to 3: `PAYLOAD_ABI_VERSION` already rebuilds across a release, and the bump closes
33
+ # the same-version window too.
17
34
  #
18
35
  # Idempotent: the guard checks `method_defined?(:_dump)` so requiring this file twice (or against an upstream
19
36
  # rbs that adds Marshal hooks itself) is a no-op.
@@ -33,13 +50,19 @@ require "rbs"
33
50
  # reference. Both classes are value objects fully described by `to_s`, so the round-trip is lossless.
34
51
  module RBS
35
52
  class Location
53
+ # The name a Location gets back when the dump carried none — an old blob, or a buffer that never had
54
+ # one. Never a real path, so a consumer that reports file names must filter it
55
+ # ({Rigor::Environment::RbsLoader::CACHED_LOCATION_BUFFER_NAME}).
56
+ CACHED_BUFFER_NAME = "<cached>"
57
+
36
58
  unless method_defined?(:_dump)
37
59
  def _dump(_)
38
- ""
60
+ buffer&.name.to_s
39
61
  end
40
62
 
41
- def self._load(_)
42
- new(buffer: ::RBS::Buffer.new(name: "<cached>", content: ""), start_pos: 0, end_pos: 0)
63
+ def self._load(name)
64
+ name = CACHED_BUFFER_NAME if name.nil? || name.empty?
65
+ new(buffer: ::RBS::Buffer.new(name: name, content: ""), start_pos: 0, end_pos: 0)
43
66
  end
44
67
  end
45
68
  end
@@ -29,7 +29,16 @@ module Rigor
29
29
  # fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
30
30
  # this version, so the first writable run after a bump clears the root and reclaims the unreadable
31
31
  # bytes.
32
- FORMAT_VERSION = 2
32
+ #
33
+ # v3 (issue #696 review, second pass): `RBS::Location#_dump` now carries the buffer NAME rather than an
34
+ # empty string, so an env blob written before this change reconstructs every location behind the
35
+ # `<cached>` sentinel. That degrades gracefully — the sentinel is filtered, never printed as a path —
36
+ # but `rbs.coverage.definition-build-failed` then omits its conflicting-files clause, so a warm run off
37
+ # a pre-change blob says less than a cold run does, indefinitely: ADR-6's store never evicts.
38
+ # `PAYLOAD_ABI_VERSION` already rebuilds across a RELEASE, so the exposure is a same-version tree; this
39
+ # closes that window too, because "the same project reports differently depending on how you ran it" is
40
+ # the defect the diagnostic exists to end and a stale blob reintroduces it.
41
+ FORMAT_VERSION = 3
33
42
 
34
43
  # Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
35
44
  # upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the