rigortype 0.1.16 → 0.1.18

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  6. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
  7. data/lib/rigor/analysis/check_rules.rb +180 -73
  8. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  9. data/lib/rigor/analysis/diagnostic.rb +18 -0
  10. data/lib/rigor/analysis/incremental.rb +162 -0
  11. data/lib/rigor/analysis/incremental_session.rb +337 -0
  12. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  13. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  14. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  15. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  16. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  17. data/lib/rigor/analysis/runner.rb +477 -1110
  18. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  19. data/lib/rigor/analysis/worker_session.rb +47 -8
  20. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  21. data/lib/rigor/cache/descriptor.rb +50 -49
  22. data/lib/rigor/cache/incremental_snapshot.rb +153 -0
  23. data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
  24. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  25. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  26. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  27. data/lib/rigor/cache/rbs_environment.rb +2 -8
  28. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  29. data/lib/rigor/cache/store.rb +145 -14
  30. data/lib/rigor/cli/annotate_command.rb +2 -7
  31. data/lib/rigor/cli/baseline_command.rb +2 -7
  32. data/lib/rigor/cli/check_command.rb +705 -0
  33. data/lib/rigor/cli/ci_detector.rb +94 -0
  34. data/lib/rigor/cli/command.rb +47 -0
  35. data/lib/rigor/cli/coverage_command.rb +3 -23
  36. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  37. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  38. data/lib/rigor/cli/diff_command.rb +3 -7
  39. data/lib/rigor/cli/explain_command.rb +2 -7
  40. data/lib/rigor/cli/lsp_command.rb +3 -7
  41. data/lib/rigor/cli/mcp_command.rb +3 -7
  42. data/lib/rigor/cli/options.rb +57 -0
  43. data/lib/rigor/cli/plugin_command.rb +3 -7
  44. data/lib/rigor/cli/plugins_command.rb +2 -7
  45. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  46. data/lib/rigor/cli/renderable.rb +26 -0
  47. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  48. data/lib/rigor/cli/skill_command.rb +3 -7
  49. data/lib/rigor/cli/trace_command.rb +143 -0
  50. data/lib/rigor/cli/trace_renderer.rb +310 -0
  51. data/lib/rigor/cli/triage_command.rb +2 -7
  52. data/lib/rigor/cli/type_of_command.rb +5 -38
  53. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  54. data/lib/rigor/cli/type_scan_command.rb +3 -23
  55. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  56. data/lib/rigor/cli.rb +15 -532
  57. data/lib/rigor/configuration/dependencies.rb +18 -1
  58. data/lib/rigor/configuration/severity_profile.rb +22 -3
  59. data/lib/rigor/configuration.rb +16 -3
  60. data/lib/rigor/environment/rbs_loader.rb +129 -71
  61. data/lib/rigor/environment.rb +1 -1
  62. data/lib/rigor/inference/acceptance.rb +10 -0
  63. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  64. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  73. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  74. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  75. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  76. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  77. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  78. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  79. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  80. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  81. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  82. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  83. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  84. data/lib/rigor/inference/expression_typer.rb +149 -63
  85. data/lib/rigor/inference/flow_tracer.rb +180 -0
  86. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  88. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  89. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  90. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  91. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  92. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  93. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  94. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  95. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  96. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  97. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  98. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  99. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  100. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  101. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  102. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  103. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  104. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  105. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  106. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  107. data/lib/rigor/inference/method_dispatcher.rb +185 -84
  108. data/lib/rigor/inference/narrowing.rb +262 -5
  109. data/lib/rigor/inference/scope_indexer.rb +208 -21
  110. data/lib/rigor/inference/statement_evaluator.rb +110 -48
  111. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  112. data/lib/rigor/language_server/completion_provider.rb +4 -4
  113. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  114. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  115. data/lib/rigor/language_server/hover_provider.rb +4 -4
  116. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  117. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  118. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  119. data/lib/rigor/plugin/base.rb +302 -45
  120. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  121. data/lib/rigor/plugin/registry.rb +281 -15
  122. data/lib/rigor/plugin.rb +1 -0
  123. data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
  124. data/lib/rigor/rbs_extended.rb +39 -0
  125. data/lib/rigor/scope/discovery_index.rb +58 -0
  126. data/lib/rigor/scope.rb +150 -167
  127. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  128. data/lib/rigor/source/literals.rb +14 -0
  129. data/lib/rigor/type/acceptance_router.rb +19 -0
  130. data/lib/rigor/type/accepts_result.rb +3 -10
  131. data/lib/rigor/type/app.rb +3 -7
  132. data/lib/rigor/type/bot.rb +2 -3
  133. data/lib/rigor/type/bound_method.rb +5 -12
  134. data/lib/rigor/type/combinator.rb +22 -0
  135. data/lib/rigor/type/constant.rb +2 -3
  136. data/lib/rigor/type/data_class.rb +80 -0
  137. data/lib/rigor/type/data_instance.rb +100 -0
  138. data/lib/rigor/type/difference.rb +5 -10
  139. data/lib/rigor/type/dynamic.rb +5 -10
  140. data/lib/rigor/type/hash_shape.rb +5 -15
  141. data/lib/rigor/type/integer_range.rb +5 -10
  142. data/lib/rigor/type/intersection.rb +5 -10
  143. data/lib/rigor/type/nominal.rb +5 -10
  144. data/lib/rigor/type/refined.rb +5 -10
  145. data/lib/rigor/type/singleton.rb +5 -10
  146. data/lib/rigor/type/top.rb +2 -3
  147. data/lib/rigor/type/tuple.rb +5 -10
  148. data/lib/rigor/type/union.rb +5 -10
  149. data/lib/rigor/type.rb +2 -0
  150. data/lib/rigor/value_semantics.rb +77 -0
  151. data/lib/rigor/version.rb +1 -1
  152. data/lib/rigor.rb +1 -1
  153. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  156. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  157. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  158. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  159. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  160. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  161. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  163. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  164. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  166. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  167. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  168. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  169. data/sig/rigor/cache.rbs +19 -0
  170. data/sig/rigor/environment.rbs +0 -2
  171. data/sig/rigor/inference.rbs +27 -0
  172. data/sig/rigor/plugin/base.rbs +1 -2
  173. data/sig/rigor/rbs_extended.rbs +2 -0
  174. data/sig/rigor/scope.rbs +42 -25
  175. data/sig/rigor/source.rbs +1 -0
  176. data/sig/rigor/type.rbs +58 -1
  177. data/sig/rigor.rbs +6 -1
  178. data/skills/rigor-ci-setup/SKILL.md +319 -0
  179. metadata +36 -2
  180. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "rbs_descriptor"
4
+ require_relative "rbs_cache_producer"
4
5
 
5
6
  module Rigor
6
7
  module Cache
@@ -15,19 +16,12 @@ module Rigor
15
16
  # Cache descriptor shape is shared with every other cache
16
17
  # producer that depends on the RBS environment — see
17
18
  # {RbsDescriptor.build} for the slot definitions.
18
- class RbsConstantTable
19
+ class RbsConstantTable < RbsCacheProducer
19
20
  PRODUCER_ID = "rbs.constant_type_table"
20
21
 
21
22
  # @param loader [Rigor::Environment::RbsLoader]
22
23
  # @param store [Rigor::Cache::Store]
23
24
  # @return [Hash{String => Rigor::Type}]
24
- def self.fetch(loader:, store:)
25
- descriptor = RbsDescriptor.build(loader)
26
- store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
27
- compute(loader)
28
- end
29
- end
30
-
31
25
  def self.compute(loader)
32
26
  table = {}
33
27
  loader.each_constant_decl do |name, entry|
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "rbs_descriptor"
4
+ require_relative "rbs_cache_producer"
4
5
  require_relative "rbs_environment_marshal_patch"
5
6
 
6
7
  module Rigor
@@ -26,19 +27,12 @@ module Rigor
26
27
  # Cache descriptor shape is shared with every other cache
27
28
  # producer that depends on the RBS environment — see
28
29
  # {RbsDescriptor.build}.
29
- class RbsEnvironment
30
+ class RbsEnvironment < RbsCacheProducer
30
31
  PRODUCER_ID = "rbs.environment"
31
32
 
32
33
  # @param loader [Rigor::Environment::RbsLoader]
33
34
  # @param store [Rigor::Cache::Store]
34
35
  # @return [::RBS::Environment]
35
- def self.fetch(loader:, store:)
36
- descriptor = RbsDescriptor.build(loader)
37
- store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
38
- compute(loader)
39
- end
40
- end
41
-
42
36
  def self.compute(loader)
43
37
  Rigor::Environment::RbsLoader.build_env_for(
44
38
  libraries: loader.libraries,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "rbs_descriptor"
4
+ require_relative "rbs_cache_producer"
4
5
 
5
6
  module Rigor
6
7
  module Cache
@@ -18,19 +19,12 @@ module Rigor
18
19
  # Cache descriptor shape is shared with {RbsConstantTable} via
19
20
  # {RbsDescriptor.build}; a single signature change or rbs gem
20
21
  # bump invalidates both producers in lockstep.
21
- class RbsKnownClassNames
22
+ class RbsKnownClassNames < RbsCacheProducer
22
23
  PRODUCER_ID = "rbs.known_class_names"
23
24
 
24
25
  # @param loader [Rigor::Environment::RbsLoader]
25
26
  # @param store [Rigor::Cache::Store]
26
27
  # @return [Set<String>]
27
- def self.fetch(loader:, store:)
28
- descriptor = RbsDescriptor.build(loader)
29
- store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
30
- compute(loader)
31
- end
32
- end
33
-
34
28
  def self.compute(loader)
35
29
  names = Set.new
36
30
  loader.each_known_class_name { |name| names << name }
@@ -5,6 +5,7 @@ require "fileutils"
5
5
  require "json"
6
6
  require "monitor"
7
7
  require "securerandom"
8
+ require "zlib"
8
9
 
9
10
  require_relative "descriptor"
10
11
 
@@ -17,17 +18,27 @@ module Rigor
17
18
  # and nothing else.
18
19
  #
19
20
  # Read failures (missing file, bad magic, format-version mismatch,
20
- # corrupt SHA-256 trailer, unmarshal-able payload) are silently
21
- # treated as cache misses; the producer block reruns and the
22
- # next write replaces the bad entry. The trailing SHA-256 catches
23
- # accidental corruption (partial writes, FS errors); it is **not**
24
- # a security boundary, per ADR-2's trusted-gem trust model.
21
+ # corrupt SHA-256 trailer, un-inflatable or unmarshal-able payload)
22
+ # are silently treated as cache misses; the producer block reruns
23
+ # and the next write replaces the bad entry. The trailing SHA-256
24
+ # catches accidental corruption (partial writes, FS errors); it is
25
+ # **not** a security boundary, per ADR-2's trusted-gem trust model.
25
26
  class Store # rubocop:disable Metrics/ClassLength
27
+ # On-disk byte-layout version. Bumped on incompatible format
28
+ # changes (independent of {Descriptor::SCHEMA_VERSION}, which
29
+ # covers the descriptor schema rather than the byte layout).
30
+ # v2 (ADR-54 WD2): the value payload is zlib-deflated on write
31
+ # and inflated on read — Marshal blobs compress to 13–16 % at
32
+ # an inflate cost an order of magnitude below their
33
+ # `Marshal.load`. v1 entries fail the header check and read as
34
+ # silent misses; the `schema_version.txt` marker additionally
35
+ # carries this version, so the first writable run after a bump
36
+ # clears the root and reclaims the unreadable bytes.
37
+ FORMAT_VERSION = 2
38
+
26
39
  # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
27
- # format version. Bumped on incompatible on-disk format changes
28
- # (independent of {Descriptor::SCHEMA_VERSION}, which covers
29
- # the descriptor schema rather than the byte layout).
30
- HEADER = "RIGOR\x00\x01".b.freeze
40
+ # format version.
41
+ HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
31
42
 
32
43
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
33
44
 
@@ -44,9 +55,11 @@ module Rigor
44
55
  # invocations can read from the same cache concurrently
45
56
  # without churning it. See
46
57
  # `docs/design/20260516-editor-mode.md` § "Cache behaviour".
47
- def initialize(root:, read_only: false)
58
+ def initialize(root:, read_only: false, max_bytes: nil)
48
59
  @root = root.to_s.dup.freeze
49
60
  @read_only = read_only
61
+ @max_bytes = max_bytes&.then { |n| Integer(n) }
62
+ @schema_version_ensured = false
50
63
  @hits = 0
51
64
  @misses = 0
52
65
  @writes = 0
@@ -106,6 +119,18 @@ module Rigor
106
119
  # When the root does not exist or has no schema-version
107
120
  # marker, `schema_version` is nil and the producer list is
108
121
  # empty.
122
+ # The `schema_version.txt` marker content. Covers BOTH
123
+ # invalidation axes: the descriptor schema and the on-disk byte
124
+ # layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the
125
+ # old entries permanently unreadable (header mismatch → miss)
126
+ # but, alone, would never reclaim their bytes — they can sit
127
+ # below the eviction cap forever. Folding the format version
128
+ # into the marker routes the bump through the established
129
+ # clear-the-root path instead.
130
+ def self.schema_marker_value
131
+ "#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
132
+ end
133
+
109
134
  def self.disk_inventory(root:)
110
135
  root_s = root.to_s
111
136
  marker = File.join(root_s, "schema_version.txt")
@@ -197,6 +222,81 @@ module Rigor
197
222
  value
198
223
  end
199
224
 
225
+ # ADR-45 — record-and-validate variant. Unlike {fetch_or_compute},
226
+ # which keys the entry on its descriptor (so the inputs MUST be
227
+ # known before running), this keys on `key_descriptor` (the stable
228
+ # inputs known up front) and stores, alongside the value, a
229
+ # `dependency_descriptor` of the files the value actually read —
230
+ # including inputs discovered DURING the computation (e.g. a plugin
231
+ # reading a file mid-analysis). On the next run the stored
232
+ # dependencies are re-validated against the filesystem
233
+ # ({Descriptor#fresh?}); a stale dependency forces a recompute.
234
+ #
235
+ # The block MUST return `[value, dependency_descriptor]`. Disk reads
236
+ # are not in-process-memoised — validation always re-checks the
237
+ # filesystem — but a single run only looks up once.
238
+ def fetch_or_validate(producer_id:, key_descriptor:, params: {}, serialize: nil, deserialize: nil)
239
+ validate_producer_id!(producer_id)
240
+ ensure_schema_version!
241
+
242
+ key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
243
+ path = entry_path(producer_id, key)
244
+ cached = read_entry(path, deserialize: deserialize)
245
+ if cached && (pair = cached.value).is_a?(Array) && pair.size == 2 &&
246
+ pair[1].is_a?(Descriptor) && pair[1].fresh?
247
+ @monitor.synchronize { record(:hits, producer_id) }
248
+ return pair[0]
249
+ end
250
+
251
+ value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
252
+ wrote = false
253
+ unless @read_only
254
+ # A cache write must never break the run. If the value is not
255
+ # Marshal-clean (or any disk error occurs) skip caching and
256
+ # return the freshly-computed value — the next run recomputes.
257
+ begin
258
+ write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
259
+ wrote = true
260
+ rescue StandardError
261
+ wrote = false
262
+ end
263
+ end
264
+ @monitor.synchronize do
265
+ record(:misses, producer_id)
266
+ record(:writes, producer_id) if wrote
267
+ end
268
+ value
269
+ end
270
+
271
+ # ADR-6 § "Eviction" — LRU pass over the on-disk cache. No-op when
272
+ # `max_bytes:` was not configured or the store is read-only.
273
+ # Walks all `.entry` files, sorts by mtime ascending (oldest = least
274
+ # recently used), and unlinks from the oldest until the total is at
275
+ # or below the cap. Touch-on-disk-read ({read_entry}) is the
276
+ # cross-process LRU signal: every disk hit (not in-process-memo hit)
277
+ # updates the mtime so recently-read entries survive the eviction pass.
278
+ # Any FS error is swallowed — eviction must never break a run.
279
+ def evict!
280
+ return if @max_bytes.nil? || @read_only
281
+
282
+ entries = collect_entry_stats
283
+ total = entries.sum { |e| e[:bytes] }
284
+ return if total <= @max_bytes
285
+
286
+ entries.sort_by! { |e| e[:mtime] }
287
+ entries.each do |entry|
288
+ break if total <= @max_bytes
289
+
290
+ File.unlink(entry[:path])
291
+ total -= entry[:bytes]
292
+ rescue StandardError
293
+ next
294
+ end
295
+ nil
296
+ rescue StandardError
297
+ nil
298
+ end
299
+
200
300
  private
201
301
 
202
302
  Entry = Data.define(:descriptor_bytes, :value)
@@ -238,6 +338,7 @@ module Rigor
238
338
  value = safe_load(value_bytes, deserialize)
239
339
  return nil if value.equal?(LOAD_FAILED)
240
340
 
341
+ touch_for_lru(path) if @max_bytes
241
342
  Entry.new(descriptor_bytes, value)
242
343
  end
243
344
 
@@ -271,11 +372,15 @@ module Rigor
271
372
  LOAD_FAILED = Object.new.freeze
272
373
  private_constant :LOAD_FAILED
273
374
 
375
+ # Inflates the stored value payload (ADR-54 WD2), then hands the
376
+ # raw bytes to the deserialiser. Any failure — corrupt deflate
377
+ # stream included — reads as a miss.
274
378
  def safe_load(bytes, deserialize)
379
+ raw = Zlib::Inflate.inflate(bytes)
275
380
  if deserialize
276
- deserialize.call(bytes)
381
+ deserialize.call(raw)
277
382
  else
278
- Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
383
+ Marshal.load(raw) # rubocop:disable Security/MarshalLoad
279
384
  end
280
385
  rescue StandardError
281
386
  LOAD_FAILED
@@ -285,7 +390,7 @@ module Rigor
285
390
  FileUtils.mkdir_p(File.dirname(path))
286
391
 
287
392
  descriptor_bytes = descriptor.to_canonical_bytes
288
- value_bytes = serialize_value(value, serialize)
393
+ value_bytes = Zlib::Deflate.deflate(serialize_value(value, serialize))
289
394
 
290
395
  body = +"".b
291
396
  body << HEADER
@@ -331,10 +436,15 @@ module Rigor
331
436
  # never collides with a read under the old). The next
332
437
  # writable run will repair the cache.
333
438
  return if @read_only
439
+ # The marker is process-stable; one check per Store is
440
+ # enough (a benign double-check under a thread race just
441
+ # repeats idempotent work).
442
+ return if @schema_version_ensured
334
443
 
444
+ @schema_version_ensured = true
335
445
  FileUtils.mkdir_p(@root)
336
446
  marker = File.join(@root, "schema_version.txt")
337
- current = Descriptor::SCHEMA_VERSION.to_s
447
+ current = self.class.schema_marker_value
338
448
 
339
449
  if File.file?(marker)
340
450
  on_disk = File.read(marker).strip
@@ -370,6 +480,27 @@ module Rigor
370
480
  end
371
481
  end
372
482
 
483
+ # Updates both atime and mtime of `path` to the current time — the
484
+ # cross-process LRU signal used by {evict!}. Best-effort: any FS
485
+ # error (read-only mount, deleted file) is silently ignored.
486
+ def touch_for_lru(path)
487
+ now = Time.now
488
+ File.utime(now, now, path)
489
+ rescue StandardError
490
+ nil
491
+ end
492
+
493
+ # Returns an array of `{ path:, mtime:, bytes: }` hashes for every
494
+ # `.entry` file under the cache root, skipping unreadable entries.
495
+ def collect_entry_stats
496
+ Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
497
+ stat = File.stat(path)
498
+ { path: path, mtime: stat.mtime, bytes: stat.size }
499
+ rescue StandardError
500
+ nil
501
+ end
502
+ end
503
+
373
504
  def read_varint(bytes, offset)
374
505
  result = 0
375
506
  shift = 0
@@ -9,6 +9,7 @@ require_relative "../scope"
9
9
  require_relative "../inference/def_return_typer"
10
10
  require_relative "../inference/scope_indexer"
11
11
  require_relative "prism_colorizer"
12
+ require_relative "command"
12
13
 
13
14
  module Rigor
14
15
  class CLI
@@ -25,19 +26,13 @@ module Rigor
25
26
  # since the appended text is always a comment — and printed to
26
27
  # stdout with IRB-style syntax highlighting via
27
28
  # {PrismColorizer}.
28
- class AnnotateCommand
29
+ class AnnotateCommand < Command
29
30
  USAGE = "Usage: rigor annotate [options] FILE"
30
31
 
31
32
  # Appended ` #=> dump_type: <type>` suffix. Matched and
32
33
  # stripped before re-annotating so re-running is idempotent.
33
34
  ANNOTATION_PATTERN = /\s*#=>\s*dump_type:.*\z/
34
35
 
35
- def initialize(argv:, out:, err:)
36
- @argv = argv
37
- @out = out
38
- @err = err
39
- end
40
-
41
36
  # @return [Integer] CLI exit status.
42
37
  def run
43
38
  options = parse_options
@@ -6,6 +6,7 @@ require_relative "../analysis/baseline"
6
6
  require_relative "../analysis/runner"
7
7
  require_relative "../cache/store"
8
8
  require_relative "../configuration"
9
+ require_relative "command"
9
10
 
10
11
  module Rigor
11
12
  class CLI
@@ -20,18 +21,12 @@ module Rigor
20
21
  # rigor baseline dump
21
22
  # rigor baseline drift
22
23
  # rigor baseline prune
23
- class BaselineCommand # rubocop:disable Metrics/ClassLength
24
+ class BaselineCommand < Command # rubocop:disable Metrics/ClassLength
24
25
  EXIT_USAGE = 64
25
26
  DEFAULT_BASELINE_PATH = ".rigor-baseline.yml"
26
27
 
27
28
  SUBCOMMANDS = %w[generate regenerate dump drift prune].freeze
28
29
 
29
- def initialize(argv:, out: $stdout, err: $stderr)
30
- @argv = argv
31
- @out = out
32
- @err = err
33
- end
34
-
35
30
  def run
36
31
  subcommand = @argv.shift
37
32
  case subcommand