rigortype 0.4.0 → 0.4.1
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.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #1536 (ADR-89 WD1 amendment) — what the incremental session knows about the RBS that source-RBS
|
|
8
|
+
# synthesizers derive from each file's comments. The declaration gates read a file's Ruby side only; a
|
|
9
|
+
# synthesizer's output reaches the environment as a `virtual:` buffer, and a reader of that RBS records no
|
|
10
|
+
# edge back to the file, so the session asks this gate one question before deciding a closure: did the
|
|
11
|
+
# edit move any synthesized output? A yes re-analyses the whole project.
|
|
12
|
+
#
|
|
13
|
+
# Each seed bundle carries its file's {Environment::SourceRbsSynthesis.digest} under {DIGEST_KEY}. The
|
|
14
|
+
# gate writes it ({#stamp}), not the scope indexer that builds the rest of the bundle: the digest needs a
|
|
15
|
+
# plugin registry, which discovery never sees. A stamp must describe the synthesized RBS the cached
|
|
16
|
+
# diagnostics of the file's readers were computed under; where that cannot be vouched for, it is nil, which
|
|
17
|
+
# reads as moved.
|
|
18
|
+
#
|
|
19
|
+
# The synthesizers are read from a registry the gate loads for itself, WITHOUT `#prepare`, because the
|
|
20
|
+
# closure is decided before the recheck runner (which loads and prepares its own) exists. That is exact
|
|
21
|
+
# only for a synthesizer reachable without `#prepare`, so {#verify} compares the two sets after every run:
|
|
22
|
+
# a plugin that builds its synthesizer in `#prepare` leaves the gate UNTRUSTED for the rest of the
|
|
23
|
+
# session — every stored digest reads as unknown, and every edit re-analyses the whole project.
|
|
24
|
+
class SourceRbsGate
|
|
25
|
+
DIGEST_KEY = :source_rbs_digest
|
|
26
|
+
|
|
27
|
+
# The paths {#stamp} last stored as unknown because the bytes its bundle was built from are not the
|
|
28
|
+
# bytes the closure was decided on. The session forgets their recorded content digests, so the next run
|
|
29
|
+
# detects them as changed and, reading the unknown stamp, re-analyses the project.
|
|
30
|
+
attr_reader :unbound
|
|
31
|
+
|
|
32
|
+
def initialize(configuration:, cache_store:, plugin_requirer:)
|
|
33
|
+
@configuration = configuration
|
|
34
|
+
@cache_store = cache_store
|
|
35
|
+
@plugin_requirer = plugin_requirer
|
|
36
|
+
@synthesizers = nil
|
|
37
|
+
@readings = nil
|
|
38
|
+
@unbound = Set.new.freeze
|
|
39
|
+
@untrusted = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True once a run's prepared registry declared a different synthesizer set from the one the gate
|
|
43
|
+
# digests with. Sticky for the session: the configuration, and so the plugin set, is fixed for its life.
|
|
44
|
+
def untrusted?
|
|
45
|
+
@untrusted
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether this edit moved what any synthesizer contributes: a changed file whose digest differs from
|
|
49
|
+
# the one on its snapshot bundle (or either is unknown), an added file that contributes anything, or a
|
|
50
|
+
# removed file that did. Decided from the synthesizers' output, never from which plugins `plugins:`
|
|
51
|
+
# names. The readings taken here are kept for {#stamp}.
|
|
52
|
+
def moved?(bundles, changed, added, removed)
|
|
53
|
+
return true if @untrusted && [changed, added, removed].any?(&:any?)
|
|
54
|
+
|
|
55
|
+
@readings = (changed + added).to_h { |path| [path, reading(path)] }
|
|
56
|
+
none = Environment::SourceRbsSynthesis::NO_CONTRIBUTION
|
|
57
|
+
changed.any? { |path| !unmoved?(bundles, path) } ||
|
|
58
|
+
added.any? { |path| @readings.fetch(path).last != none } ||
|
|
59
|
+
removed.any? { |path| stored(bundles, path) != none }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Stamps every bundle that needs a digest: one the runner built this run (a baseline's all, a recheck's
|
|
63
|
+
# re-walked ones), and one carrying nil that this run can now vouch for. A bundle the runner reused keeps
|
|
64
|
+
# the digest it carries, which stays exact because a bundle is reused only for byte-identical content.
|
|
65
|
+
#
|
|
66
|
+
# A digest is vouched for only by a reading of the bytes the bundle was built from: the reading
|
|
67
|
+
# {#moved?} took when the closure was decided, or, when `whole_project` (every file was just
|
|
68
|
+
# re-analysed), one taken now. Anything else is stamped nil and listed in {#unbound}; a file saved after
|
|
69
|
+
# the closure was decided is never re-read, because its readers' cached answers predate that save. An
|
|
70
|
+
# untrusted gate stamps every bundle unknown.
|
|
71
|
+
def stamp(bundles, whole_project: false)
|
|
72
|
+
readings = @readings || {}
|
|
73
|
+
@readings = nil
|
|
74
|
+
@unbound = Set.new
|
|
75
|
+
return distrust(bundles) if @untrusted
|
|
76
|
+
|
|
77
|
+
targets = bundles.select { |path, bundle| stamp?(bundle, whole_project || readings.key?(path)) }
|
|
78
|
+
return bundles if targets.empty?
|
|
79
|
+
|
|
80
|
+
stamped = bundles.dup
|
|
81
|
+
targets.each do |path, bundle|
|
|
82
|
+
stamped[path] = bundle.merge(DIGEST_KEY => bound_digest(path, bundle, readings[path], whole_project))
|
|
83
|
+
end
|
|
84
|
+
stamped
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Compares, by plugin id and order, the synthesizers the gate digests with against those of
|
|
88
|
+
# `prepared_registry` — the registry the run's environment was built from, after `#prepare` (nil when
|
|
89
|
+
# it could not be built, which also distrusts). Runs after every run, a run that digested nothing
|
|
90
|
+
# included: a stamp written by an earlier process from the other set would otherwise be trusted. On a
|
|
91
|
+
# mismatch the gate turns untrusted and every bundle is stamped unknown. Returns the bundles to keep.
|
|
92
|
+
def verify(prepared_registry, bundles)
|
|
93
|
+
return bundles if synthesizer_ids(prepared_registry&.source_rbs_synthesizers) == synthesizer_ids(synthesizers)
|
|
94
|
+
|
|
95
|
+
@untrusted = true
|
|
96
|
+
distrust(bundles)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def unmoved?(bundles, path)
|
|
102
|
+
before = stored(bundles, path)
|
|
103
|
+
!before.nil? && before == @readings.fetch(path).last
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def stored(bundles, path)
|
|
107
|
+
bundles[path]&.fetch(DIGEST_KEY, nil)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def distrust(bundles)
|
|
111
|
+
bundles.transform_values { |bundle| bundle.merge(DIGEST_KEY => nil) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A bundle without a digest always needs one; one carrying nil is re-stamped only when this run can
|
|
115
|
+
# vouch for it, so an unknown stamp left by a distrust or an unbound read clears on the file's next edit
|
|
116
|
+
# or the next whole-project run.
|
|
117
|
+
def stamp?(bundle, vouched)
|
|
118
|
+
!bundle.key?(DIGEST_KEY) || (bundle[DIGEST_KEY].nil? && vouched)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The digest for `bundle` when a reading vouches for the bytes it was built from, else nil (and the path
|
|
122
|
+
# joins {#unbound}).
|
|
123
|
+
def bound_digest(path, bundle, reading, whole_project)
|
|
124
|
+
reading ||= reading(path) if whole_project
|
|
125
|
+
return reading.last if reading && reading.first == bundle[:digest]
|
|
126
|
+
|
|
127
|
+
@unbound << path
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# `[content SHA-256, digest]`, the SHA nil unless the file read the same before and after the digest was
|
|
132
|
+
# taken, so the pair can be matched against a bundle's content digest.
|
|
133
|
+
def reading(path)
|
|
134
|
+
before = content_sha(path)
|
|
135
|
+
digest = Environment::SourceRbsSynthesis.digest(synthesizers, path, @cache_store)
|
|
136
|
+
[before && before == content_sha(path) ? before : nil, digest]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def content_sha(path)
|
|
140
|
+
Digest::SHA256.file(path).hexdigest
|
|
141
|
+
rescue SystemCallError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def synthesizers
|
|
146
|
+
@synthesizers ||= Runner::ProjectPrePasses.new(
|
|
147
|
+
configuration: @configuration, cache_store: @cache_store, buffer: nil,
|
|
148
|
+
plugin_requirer: @plugin_requirer, pool_mode: -> { false }
|
|
149
|
+
).prepared_registry(prepare: false).source_rbs_synthesizers
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def synthesizer_ids(pairs)
|
|
153
|
+
pairs&.map { |plugin, _| plugin.manifest.id.to_s }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -21,13 +21,15 @@ module Rigor
|
|
|
21
21
|
# how an LSP publish for an open `.rbx` reported `call.unresolved-toplevel` for every helper while the
|
|
22
22
|
# unit sat in the index under its relative name). A path outside the root is left alone.
|
|
23
23
|
def relative(path, root)
|
|
24
|
-
# `
|
|
24
|
+
# `absolute_path` against the root first, so `./app/views/x.rbx`, `app/views/x.rbx` and
|
|
25
25
|
# `$ROOT/lib/../app/views/x.rbx` all reduce to the one absolute spelling before anything is
|
|
26
26
|
# compared. Without it an `--instead-of=./app/views/x.rbx` matched no unit and was parsed as plain
|
|
27
27
|
# Ruby — the spellings a shell and an editor produce are not the spelling `Dir.glob` returns.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# `absolute_path`, not `expand_path`: a leading `~` is a directory name here (`~drafts/a.rb`), and
|
|
29
|
+
# `expand_path` reads it as a user's home and raises `ArgumentError` for a user that does not exist.
|
|
30
|
+
root_dir = File.absolute_path(root.to_s)
|
|
31
|
+
text = File.absolute_path(path.to_s, root_dir)
|
|
32
|
+
# BOTH spellings of the root. `absolute_path` does not resolve symlinks, and the root is not always
|
|
31
33
|
# already resolved: `Dir.pwd` is (which is why this was latent), but a caller may pass any path.
|
|
32
34
|
prefixes(root_dir).each do |prefix|
|
|
33
35
|
return text.delete_prefix(prefix) if text.start_with?(prefix)
|
|
@@ -190,11 +190,11 @@ module Rigor
|
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def [](path)
|
|
193
|
-
|
|
193
|
+
entry_for(path)
|
|
194
194
|
end
|
|
195
195
|
|
|
196
196
|
def key?(path)
|
|
197
|
-
|
|
197
|
+
!entry_for(path).nil?
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
# `{ path => ruby_source }` — what {Runner#parse_source} and {WorkerSession#parse_source} read
|
|
@@ -287,7 +287,7 @@ module Rigor
|
|
|
287
287
|
# it compiles a partial's locals into the method's parameters, and it is what makes `locals:` mean
|
|
288
288
|
# anything.
|
|
289
289
|
def parse_scopes(path)
|
|
290
|
-
entry =
|
|
290
|
+
entry = entry_for(path)
|
|
291
291
|
return nil if entry.nil? || entry.locals.empty?
|
|
292
292
|
|
|
293
293
|
[entry.locals.keys.map(&:to_sym)]
|
|
@@ -301,7 +301,7 @@ module Rigor
|
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
def unit_key_for(path)
|
|
304
|
-
|
|
304
|
+
entry_for(path)&.unit_key
|
|
305
305
|
end
|
|
306
306
|
|
|
307
307
|
# Binds the declared `self`, locals and ivar seeds onto the per-file scope, so the unit's body types
|
|
@@ -315,7 +315,7 @@ module Rigor
|
|
|
315
315
|
# the honest reading of "a receiver is declared and the analyzer cannot see it" (ADR-5), and it is
|
|
316
316
|
# silent.
|
|
317
317
|
def seed(scope, path)
|
|
318
|
-
entry =
|
|
318
|
+
entry = entry_for(path)
|
|
319
319
|
return scope if entry.nil?
|
|
320
320
|
|
|
321
321
|
scope = bind_self(scope, entry)
|
|
@@ -369,6 +369,16 @@ module Rigor
|
|
|
369
369
|
|
|
370
370
|
private
|
|
371
371
|
|
|
372
|
+
# The unit `path` names, or nil. A run with no units answers before reducing the path: the reduction
|
|
373
|
+
# realpaths the root on every call, and every analysed file is looked up at least twice, which made 922
|
|
374
|
+
# `File.realpath` calls on a template-free `rigor check` of the v0.3.9 `lib`
|
|
375
|
+
# ([#1507](https://github.com/rigortype/rigor/issues/1507)).
|
|
376
|
+
def entry_for(path)
|
|
377
|
+
return nil if @entries.empty?
|
|
378
|
+
|
|
379
|
+
@entries[normalize(path)]
|
|
380
|
+
end
|
|
381
|
+
|
|
372
382
|
def normalize(path)
|
|
373
383
|
TemplateUnitPaths.relative(path, @root)
|
|
374
384
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "../environment"
|
|
|
6
6
|
require_relative "../project_environment"
|
|
7
7
|
require_relative "../scope"
|
|
8
8
|
require_relative "../cache/store"
|
|
9
|
+
require_relative "shadow_harness"
|
|
9
10
|
require_relative "../plugin"
|
|
10
11
|
require_relative "../rbs_extended/reporter"
|
|
11
12
|
require_relative "../reflection"
|
|
@@ -364,12 +365,12 @@ module Rigor
|
|
|
364
365
|
def build_trust_policy
|
|
365
366
|
trusted_gems = @configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
|
|
366
367
|
roots = [Dir.pwd]
|
|
367
|
-
Array(@configuration.signature_paths).each { |sp| roots << File.
|
|
368
|
+
Array(@configuration.signature_paths).each { |sp| roots << File.absolute_path(sp) }
|
|
368
369
|
trusted_gems.each do |gem_name|
|
|
369
370
|
path = trusted_gem_root(gem_name)
|
|
370
371
|
roots << path if path
|
|
371
372
|
end
|
|
372
|
-
@configuration.plugins_io_allowed_paths.each { |p| roots << File.
|
|
373
|
+
@configuration.plugins_io_allowed_paths.each { |p| roots << File.absolute_path(p) }
|
|
373
374
|
|
|
374
375
|
Plugin::TrustPolicy.new(
|
|
375
376
|
trusted_gems: trusted_gems,
|
|
@@ -440,7 +441,7 @@ module Rigor
|
|
|
440
441
|
results = walk.diagnostics_for_file(
|
|
441
442
|
path: path, scope: scope, root: root, collector_driver: driver
|
|
442
443
|
)
|
|
443
|
-
if
|
|
444
|
+
if ShadowHarness.enabled?
|
|
444
445
|
CheckRules.shadow_verify_converged_collectors(path, root, scope_index, node_collectors)
|
|
445
446
|
end
|
|
446
447
|
results.each_with_object({}.compare_by_identity) do |result, by_plugin|
|
|
@@ -58,7 +58,31 @@ module Rigor
|
|
|
58
58
|
# searches ahead of a class's own `def`s. A pre-10 bundle would silently contribute no prepends for an
|
|
59
59
|
# unchanged file, so that file's consumers would keep answering with the pre-fix MRO — cached bundles
|
|
60
60
|
# must read as misses once and rebuild carrying the slot.
|
|
61
|
-
|
|
61
|
+
# v11: #1558 — a plugin producer served from its own cache now replays its entry's dependency rows into
|
|
62
|
+
# the plugin's IoBoundary, so the run-result entry and the entry of any producer computed after it carry
|
|
63
|
+
# the served value's inputs. A pre-11 entry written while a producer hit lacks those rows and would
|
|
64
|
+
# validate fresh across exactly the edit they exist to catch. A checkout's source digest already moved
|
|
65
|
+
# every such key, and a release clears the root through `Store::PAYLOAD_ABI_VERSION`; the bump closes
|
|
66
|
+
# the remaining window, a same-`VERSION` build installed as `gems/rigortype-<VERSION>`, which
|
|
67
|
+
# {EngineSource} treats as version-pinned (#285) and so keys on the version alone.
|
|
68
|
+
# v12: the def-index seed bundles gain the `:unpositioned_mixins` table, the mixin edges whose order is
|
|
69
|
+
# not a fact, which an ancestor-order reader declines on. A pre-12 bundle would silently contribute no
|
|
70
|
+
# such edge for an unchanged file, so cached bundles must read as misses once and rebuild carrying it.
|
|
71
|
+
# v13/v14: #1587 — the prepend table keeps every statement (a repeated `prepend` is no longer collapsed at
|
|
72
|
+
# the freeze) and a module written to both the include and the prepend table, or a repeated `extend`, is
|
|
73
|
+
# named unpositioned. A pre-14 bundle would hold the collapsed prepend list and miss the taint for an
|
|
74
|
+
# unchanged file, so cached bundles must read as misses once and rebuild.
|
|
75
|
+
# v15: #1592 — an `extend` (or a `class << self` include) written inside a block the walk used to skip is
|
|
76
|
+
# now recorded and named unpositioned. A pre-15 bundle would hold neither for an unchanged file.
|
|
77
|
+
# v16: ADR-119 WD1 — the def-index seed bundles gain the `:siblings` Hash (the `possible_*` / `contested_*`
|
|
78
|
+
# halves of the discovery pairs). A pre-16 bundle would contribute no sibling for an unchanged file, so cached
|
|
79
|
+
# bundles must read as misses once and rebuild carrying it.
|
|
80
|
+
# v17: ADR-119 WD3 — the def-contribution producers fill the siblings (a conditional `def`'s name possible, its
|
|
81
|
+
# def-node slot and envelope contested). A pre-17 bundle carries the empty siblings of the walk before them, so
|
|
82
|
+
# an unchanged file would fold as "no possible fact" where a cold walk records one.
|
|
83
|
+
# v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
|
|
84
|
+
# carries it empty, so an unchanged file's conditional visibility would fold as certain.
|
|
85
|
+
SCHEMA_VERSION = 18
|
|
62
86
|
|
|
63
87
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
64
88
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -105,6 +129,21 @@ module Rigor
|
|
|
105
129
|
new(path: path, comparator: :stat, value: packed)
|
|
106
130
|
end
|
|
107
131
|
|
|
132
|
+
# A `:stat` row whose recording instant is no later than `limit_ns`: any file whose recorded mtime is at or
|
|
133
|
+
# after `limit_ns` is then racy, and validation re-hashes it instead of trusting its stat tuple. Any other
|
|
134
|
+
# row, and one whose instant is already earlier, is returned as is.
|
|
135
|
+
def with_recording_instant_at_most(limit_ns)
|
|
136
|
+
return self unless comparator == :stat
|
|
137
|
+
|
|
138
|
+
parts = value.split
|
|
139
|
+
return self unless parts.size == 6 && Integer(parts[5], 10) > limit_ns
|
|
140
|
+
|
|
141
|
+
parts[5] = limit_ns.to_s
|
|
142
|
+
self.class.new(path: path, comparator: comparator, value: parts.join(" "))
|
|
143
|
+
rescue ArgumentError
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
108
147
|
# ADR-45 WD1 (#577) — the absence row: fresh while `path` does not exist, stale the moment anything
|
|
109
148
|
# (a file, a directory, a symlink) comes into existence there. {Plugin::IoBoundary#read_file}
|
|
110
149
|
# records one when a read fails because the path is missing, so a value computed on "X is absent"
|
|
@@ -112,6 +112,34 @@ module Rigor
|
|
|
112
112
|
hexdigest(path) == digest
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# ADR-45 WD2 (#1507) — `packed` re-packed against `path`'s current stat, for a writer about to record it
|
|
116
|
+
# again: when the tuple moved (a `touch`, a checkout) or the entry is racy, but the file still hashes to the
|
|
117
|
+
# recorded digest, the new pack carries the current tuple and this run's recording instant, so the next
|
|
118
|
+
# validation is one `File::Stat` again rather than a re-hash every time.
|
|
119
|
+
#
|
|
120
|
+
# The hash is taken fresh, never from the per-run memo: a memoised digest describes the bytes as they were
|
|
121
|
+
# when change detection read them, and pairing it with a later stat would vouch for bytes written since. And
|
|
122
|
+
# the re-pack is kept only when the tuple it packed, taken after the hash, is the one taken before it: a
|
|
123
|
+
# write landing while the file is hashed moves the change time, and leaves `packed` as it was.
|
|
124
|
+
#
|
|
125
|
+
# `packed` comes back unchanged when its tuple still matches and it is not racy, and whenever the bytes no
|
|
126
|
+
# longer match the digest: a row that must stay stale is never refreshed into a fresh one. nil when `packed`
|
|
127
|
+
# is not a stat pack. A stat failure (the file is gone) returns `packed` unchanged, which then validates as
|
|
128
|
+
# stale.
|
|
129
|
+
def self.refresh_stat(path, packed)
|
|
130
|
+
parsed = parse_stat(packed)
|
|
131
|
+
return nil if parsed.nil?
|
|
132
|
+
|
|
133
|
+
before = File.stat(path)
|
|
134
|
+
return packed if !racy?(parsed) && tuple_matches?(before, parsed)
|
|
135
|
+
return packed unless Digest::SHA256.file(path).hexdigest == parsed[0]
|
|
136
|
+
|
|
137
|
+
repacked = pack_stat(path, parsed[0])
|
|
138
|
+
repacked && tuple_matches?(before, parse_stat(repacked)) ? repacked : packed
|
|
139
|
+
rescue SystemCallError
|
|
140
|
+
packed
|
|
141
|
+
end
|
|
142
|
+
|
|
115
143
|
# The content-digest field of a packed `:stat` entry ({.pack_stat}); nil when the entry is absent or not
|
|
116
144
|
# well-formed. Exposed for the caller that must compare the recorded digest against bytes it hashes
|
|
117
145
|
# itself rather than against the file on disk — the incremental session's editor-mode freshness check,
|
|
@@ -5,6 +5,10 @@ require "digest"
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
|
|
7
7
|
require_relative "engine_source"
|
|
8
|
+
require_relative "store"
|
|
9
|
+
require_relative "../analysis/shadow_harness"
|
|
10
|
+
require_relative "../analysis/run_cache_key"
|
|
11
|
+
require_relative "../environment"
|
|
8
12
|
|
|
9
13
|
module Rigor
|
|
10
14
|
module Cache
|
|
@@ -138,7 +142,33 @@ module Rigor
|
|
|
138
142
|
# the `write` / `to_str` / `to_int` / hatch definitions and top-level mixins it holds, which the `global.*`
|
|
139
143
|
# write rules decline on project-wide. A pre-29 bundle would fold as "none of those", and a warm run would
|
|
140
144
|
# report a write a cold run exempts.
|
|
141
|
-
|
|
145
|
+
# 30: issue #1536 gives each seed bundle a `source_rbs_digest` — the digest of what every loaded source-RBS
|
|
146
|
+
# synthesizer contributes for the file — which ADR-89 WD1 now compares beside `declaration_signature`
|
|
147
|
+
# in place of switching the gate off whenever `rigor-rbs-inline` is configured. A pre-30 bundle
|
|
148
|
+
# carries none, which the session reads as "unknown" and so as moved: sound, but every first edit to
|
|
149
|
+
# each file would re-analyse the whole project, so the gate rejects the blob and the next run is cold.
|
|
150
|
+
# 31: issue #1574 digests `plugin_fact_digest` and a seed bundle's `source_rbs_digest` with
|
|
151
|
+
# {ValueDigest} instead of over `Marshal.dump` bytes, so the same value digests alike whether a producer
|
|
152
|
+
# computed it or its cache entry served it. A pre-31 digest is in the old form and would never match, so
|
|
153
|
+
# the next run would go cold anyway; the bump makes that a clean miss instead of a "plugin fact surface
|
|
154
|
+
# changed" note that nothing changed.
|
|
155
|
+
# 32: each seed bundle gains an `unpositioned_mixins` table — the file's mixin edges whose order is not a
|
|
156
|
+
# fact (`Scope::DiscoveryIndex#unpositioned_mixins`), which an ancestor-order reader declines on. A pre-32
|
|
157
|
+
# bundle would fold as "every edge positioned", and a warm run would answer an order a cold run declines.
|
|
158
|
+
# 33: the mixin producers change what a seed bundle holds (#1587 — the prepend table keeps every statement,
|
|
159
|
+
# and a both-kind module or a repeated `extend` is named unpositioned), so a pre-33 bundle folds as the
|
|
160
|
+
# collapsed, positioned table and a warm run would answer an order a cold run declines.
|
|
161
|
+
# 34: #1592 — an `extend` or a `class << self` include inside a block the extends walk used to skip is now
|
|
162
|
+
# recorded and named unpositioned, so a pre-34 bundle folds as if the edge did not exist.
|
|
163
|
+
# 35: ADR-119 WD1 — each seed bundle gains a `siblings` Hash, the `possible_*` / `contested_*` siblings of the
|
|
164
|
+
# `Scope::DiscoveryIndex` members that may admit a possible fact. Every sibling is empty until a producer
|
|
165
|
+
# fills one, so a pre-35 bundle folds as "no possible fact", which is what it would hold today; the bump keeps
|
|
166
|
+
# that true once one does.
|
|
167
|
+
# 36: ADR-119 WD3 — the def-contribution producers fill those siblings, so a pre-36 bundle folds an unchanged
|
|
168
|
+
# file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
|
|
169
|
+
# 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
|
|
170
|
+
# carries empty.
|
|
171
|
+
SCHEMA = 37
|
|
142
172
|
|
|
143
173
|
# The persisted per-file state.
|
|
144
174
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -210,8 +240,13 @@ module Rigor
|
|
|
210
240
|
# rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
|
|
211
241
|
# resolved configuration, the analysis **roots** (the path arguments, e.g. `["lib"]`, NOT the expanded
|
|
212
242
|
# file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is
|
|
213
|
-
# handled incrementally by the session, not a full rebuild), the resolved gem set (
|
|
214
|
-
#
|
|
243
|
+
# handled incrementally by the session, not a full rebuild), the resolved gem set (the lockfiles
|
|
244
|
+
# {Analysis::RunCacheKey.resolved_lockfile_paths} resolves — `bundler.lockfile:` / `rbs_collection.lockfile:`,
|
|
245
|
+
# else `./Gemfile.lock` / `./rbs_collection.lock.yaml` — the same files the run-result key digests), the
|
|
246
|
+
# project's own RBS (the `sig/` roots the environment actually loads: the explicit
|
|
247
|
+
# `signature_paths`, or the auto-detected `<root>/sig` when `signature_paths` is nil), and the contents of
|
|
248
|
+
# every `pre_eval:` file (its PATH is in the configuration, but an edit to a file outside the analysed
|
|
249
|
+
# paths changes no analysed file, so it needs its own part). Built WITHOUT
|
|
215
250
|
# constructing the RBS environment so the warm path can gate the load cheaply, before the costly env
|
|
216
251
|
# build. The `--verify-incremental` gate is the safety net for any under-capture (it would surface as an
|
|
217
252
|
# incremental-vs-full mismatch). Returns nil on any error → the caller falls back to a non-persisted run.
|
|
@@ -225,16 +260,22 @@ module Rigor
|
|
|
225
260
|
# part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
|
|
226
261
|
# survive the upgrade untouched.
|
|
227
262
|
def self.fingerprint(configuration:, roots:)
|
|
263
|
+
bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
|
|
228
264
|
parts = [
|
|
229
265
|
"engine:#{Rigor::VERSION}:#{SCHEMA}",
|
|
230
266
|
"config:#{Digest::SHA256.hexdigest(Marshal.dump(configuration.to_h))}",
|
|
231
267
|
"roots:#{Array(roots).map(&:to_s).sort.join("\n")}",
|
|
232
|
-
"gems:#{digest_file_if_present(
|
|
233
|
-
"rbs_collection:#{digest_file_if_present(
|
|
234
|
-
"sig:#{digest_signature_paths(configuration
|
|
268
|
+
"gems:#{digest_file_if_present(bundler_lock)}",
|
|
269
|
+
"rbs_collection:#{digest_file_if_present(collection_lock)}",
|
|
270
|
+
"sig:#{digest_signature_paths(project_sig_roots(configuration))}",
|
|
271
|
+
"pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
|
|
235
272
|
]
|
|
236
273
|
identity = EngineSource.process_identity
|
|
237
274
|
parts << "engine-source:#{identity}" if identity
|
|
275
|
+
# ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
|
|
276
|
+
# wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
|
|
277
|
+
shadow = Analysis::ShadowHarness.cache_identity
|
|
278
|
+
parts << "shadow-harness:#{shadow}" if shadow
|
|
238
279
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
239
280
|
rescue StandardError
|
|
240
281
|
# {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
|
|
@@ -246,10 +287,29 @@ module Rigor
|
|
|
246
287
|
end
|
|
247
288
|
|
|
248
289
|
def self.digest_file_if_present(path)
|
|
249
|
-
|
|
290
|
+
path = path&.to_s
|
|
291
|
+
path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
|
|
250
292
|
end
|
|
251
293
|
private_class_method :digest_file_if_present
|
|
252
294
|
|
|
295
|
+
# Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
|
|
296
|
+
# even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
|
|
297
|
+
def self.digest_pre_eval(paths)
|
|
298
|
+
digest = Digest::SHA256.new
|
|
299
|
+
Array(paths).each do |path|
|
|
300
|
+
digest << path.to_s << "\0" << digest_file_if_present(path.to_s) << "\0"
|
|
301
|
+
end
|
|
302
|
+
digest.hexdigest
|
|
303
|
+
end
|
|
304
|
+
private_class_method :digest_pre_eval
|
|
305
|
+
|
|
306
|
+
# The `sig/` roots the environment loads for this configuration: the configured `signature_paths`, or the
|
|
307
|
+
# auto-detected `<root>/sig` when those are nil ({Environment.project_signature_roots}).
|
|
308
|
+
def self.project_sig_roots(configuration)
|
|
309
|
+
Environment.project_signature_roots(configuration.signature_paths).map(&:to_s)
|
|
310
|
+
end
|
|
311
|
+
private_class_method :project_sig_roots
|
|
312
|
+
|
|
253
313
|
# Content-digest every `.rbs` under the configured signature paths (sorted for determinism) so a project
|
|
254
314
|
# RBS edit invalidates the snapshot. Sig trees are small; content (not mtime) keeps it stable across
|
|
255
315
|
# checkouts.
|
|
@@ -351,7 +411,9 @@ module Rigor
|
|
|
351
411
|
effects_identity: payload.effects_identity,
|
|
352
412
|
run_level_rows: payload.run_level_rows
|
|
353
413
|
)
|
|
354
|
-
blob
|
|
414
|
+
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
415
|
+
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
416
|
+
blob = Zlib::Deflate.deflate(raw, Store::DEFLATE_LEVEL)
|
|
355
417
|
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
356
418
|
File.binwrite(tmp, blob)
|
|
357
419
|
File.rename(tmp, @path)
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Rigor
|
|
|
25
25
|
# On-disk byte-layout version. Bumped on incompatible format changes (independent of
|
|
26
26
|
# {Descriptor::SCHEMA_VERSION}, which covers the descriptor schema rather than the byte layout). v2
|
|
27
27
|
# (ADR-54 WD2): the value payload is zlib-deflated on write and inflated on read — Marshal blobs
|
|
28
|
-
#
|
|
28
|
+
# compressed to 13–16 % at zlib's default level (now {DEFLATE_LEVEL}) at an inflate cost an order of
|
|
29
|
+
# magnitude below their `Marshal.load`. v1 entries
|
|
29
30
|
# fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
|
|
30
31
|
# this version, so the first writable run after a bump clears the root and reclaims the unreadable
|
|
31
32
|
# bytes.
|
|
@@ -60,6 +61,14 @@ module Rigor
|
|
|
60
61
|
|
|
61
62
|
STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
|
|
62
63
|
|
|
64
|
+
# #1507 — the zlib level for everything written under the cache root: these value payloads (ADR-54 WD2),
|
|
65
|
+
# the incremental snapshot, and `rigor unused`'s scan bundle. The snapshot and the scan bundle are
|
|
66
|
+
# rewritten whole whenever a run changed anything, so their deflate is on the edit path. On Mastodon the
|
|
67
|
+
# default level took 122 ms for the snapshot (9.5 MB raw) and 131 ms for `rbs.environment` (22.8 MB raw);
|
|
68
|
+
# `BEST_SPEED` takes 38 ms and 49 ms, for a cache about a quarter larger. Inflate reads any level, so the
|
|
69
|
+
# level is not part of the format.
|
|
70
|
+
DEFLATE_LEVEL = Zlib::BEST_SPEED
|
|
71
|
+
|
|
63
72
|
# Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte format version.
|
|
64
73
|
HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
|
|
65
74
|
|
|
@@ -241,8 +250,13 @@ module Rigor
|
|
|
241
250
|
# validation always re-checks the filesystem — but a single run only looks up once.
|
|
242
251
|
#
|
|
243
252
|
# `generation_cap:` carries the same producer-declared compaction budget as {#fetch_or_compute}.
|
|
253
|
+
#
|
|
254
|
+
# `on_hit:` is called with the entry's stored dependency descriptor on a fresh hit, before the value
|
|
255
|
+
# is returned, and never on a miss. A caller whose own dependency record must carry a served value's
|
|
256
|
+
# inputs replays them from it (#1558, {Plugin::Base#cache_for}): a hit runs no block, so nothing else
|
|
257
|
+
# reports what the value was computed from.
|
|
244
258
|
def fetch_or_validate(producer_id:, key_descriptor:, generation_cap:, params: {},
|
|
245
|
-
serialize: nil, deserialize: nil)
|
|
259
|
+
serialize: nil, deserialize: nil, on_hit: nil)
|
|
246
260
|
validate_producer_id!(producer_id)
|
|
247
261
|
declare_generation_cap(producer_id, generation_cap)
|
|
248
262
|
disk = ensure_schema_version!
|
|
@@ -252,6 +266,7 @@ module Rigor
|
|
|
252
266
|
cached = path && read_entry(path, deserialize: deserialize)
|
|
253
267
|
if (validated = fresh_pair_value(cached))
|
|
254
268
|
@monitor.synchronize { record(:hits, producer_id) }
|
|
269
|
+
on_hit&.call(validated[1])
|
|
255
270
|
return validated[0]
|
|
256
271
|
end
|
|
257
272
|
|
|
@@ -285,6 +300,51 @@ module Rigor
|
|
|
285
300
|
validated[0]
|
|
286
301
|
end
|
|
287
302
|
|
|
303
|
+
# ADR-45 WD2 (#1507) — a record-and-validate entry's value WITHOUT validating its dependencies. The
|
|
304
|
+
# `--incremental` session reads the previous run-result slot to carry forward the rows a narrowed run
|
|
305
|
+
# cannot re-derive, and that slot is expected to be stale: the edit that started this run is in it. So
|
|
306
|
+
# the answer here is never served — only rows are taken from it, and they are validated again, with
|
|
307
|
+
# everything else, the next time the slot is read. nil on a miss, a malformed entry or an unavailable
|
|
308
|
+
# disk tier. Records neither a hit nor a miss, since nothing was looked up to be served.
|
|
309
|
+
def peek_unvalidated(producer_id:, key_descriptor:, params: {})
|
|
310
|
+
validate_producer_id!(producer_id)
|
|
311
|
+
return nil unless ensure_schema_version!
|
|
312
|
+
|
|
313
|
+
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
314
|
+
pair = read_entry(entry_path(producer_id, key))&.value
|
|
315
|
+
return nil unless pair.is_a?(Array) && pair.size == 2 && pair[1].is_a?(Descriptor)
|
|
316
|
+
|
|
317
|
+
pair[0]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# ADR-45 WD2 (#1507) — the write half of {#fetch_or_validate} on its own, and unconditional. That method
|
|
321
|
+
# writes only after it failed to validate what is there; a writer that has just computed an answer of its
|
|
322
|
+
# own must store THAT answer even when the entry already on disk still validates, because it is the one
|
|
323
|
+
# the run printed. Same entry format and the same failure contract as {#fetch_or_validate}'s write: a
|
|
324
|
+
# disk-side failure is swallowed, a value `Marshal` cannot dump raises. Returns whether it wrote.
|
|
325
|
+
def store_validated(producer_id:, key_descriptor:, generation_cap:, value:, dependencies:, params: {})
|
|
326
|
+
validate_producer_id!(producer_id)
|
|
327
|
+
declare_generation_cap(producer_id, generation_cap)
|
|
328
|
+
return false unless ensure_schema_version!
|
|
329
|
+
|
|
330
|
+
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
331
|
+
wrote = try_write_entry(entry_path(producer_id, key), key_descriptor, [value, dependencies])
|
|
332
|
+
@monitor.synchronize { record(:writes, producer_id) } if wrote
|
|
333
|
+
wrote
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# ADR-45 WD2 (#1507) — removes one entry, for a writer that has just superseded it under another key and
|
|
337
|
+
# knows nothing will ask for it again. Best-effort like every other unlink here; a no-op on a read-only
|
|
338
|
+
# store.
|
|
339
|
+
def discard(producer_id:, key_descriptor:, params: {})
|
|
340
|
+
validate_producer_id!(producer_id)
|
|
341
|
+
return if @read_only || !ensure_schema_version!
|
|
342
|
+
|
|
343
|
+
path = entry_path(producer_id, key_descriptor.cache_key_for(producer_id: producer_id, params: params))
|
|
344
|
+
unlink_entry_and_shard?(path) if File.file?(path)
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
|
|
288
348
|
# ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only.
|
|
289
349
|
#
|
|
290
350
|
# The generation cap of pass 2 comes from what the producers themselves declared through this Store's
|
|
@@ -469,7 +529,7 @@ module Rigor
|
|
|
469
529
|
FileUtils.mkdir_p(File.dirname(path))
|
|
470
530
|
|
|
471
531
|
descriptor_bytes = descriptor.to_canonical_bytes
|
|
472
|
-
value_bytes = Zlib::Deflate.deflate(serialize_value(value, serialize))
|
|
532
|
+
value_bytes = Zlib::Deflate.deflate(serialize_value(value, serialize), DEFLATE_LEVEL)
|
|
473
533
|
|
|
474
534
|
body = +"".b
|
|
475
535
|
body << HEADER
|