Class: Rigor::Cache::IncrementalSnapshot
- Inherits:
-
Object
- Object
- Rigor::Cache::IncrementalSnapshot
- Defined in:
- lib/rigor/cache/incremental_snapshot.rb
Overview
ADR-46 — disk persistence for the incremental analyzer's per-file state, so a --incremental session
survives across processes (one rigor check invocation reads the prior run's per-file diagnostics +
dependency graph, re-analyzes only the changed closure, and serves the rest from disk).
Unlike ADR-45's whole-run cache (record-and-validate ONE entry, invalidated by any analyzed-file change), this snapshot is loaded UNCONDITIONALLY when the global fingerprint matches — the per-file digests inside it drive the incremental re-analysis decision; they do not gate the load. The fingerprint captures the inputs whose change requires a full rebuild — the resolved configuration, the RBS environment, the engine version — but NOT the analyzed source contents. A fingerprint mismatch (config / gem / version change) drops the snapshot and forces a full re-analysis, the conservative direction.
Every operation is fault-tolerant: a missing, unreadable, schema-mismatched, fingerprint-mismatched, or corrupt snapshot loads as nil (→ a cold full run), and a write failure is swallowed (→ the next run is cold). A cache must never break a run (the ADR-45 invariant).
Defined Under Namespace
Classes: Payload
Constant Summary collapse
- SCHEMA =
Bump when the on-disk shape changes so stale snapshots are ignored rather than mis-deserialized. 5: the blob is zlib-deflated (ADR-54 WD2 parity with
Storeentries — the snapshot is the one cache artefact that does not go throughStore); a raw pre-5 blob fails the inflate and loads as nil, the usual fault-tolerant cold-run path. 6: adds the ADR-85 WD2seed_bundlessection (per-file discovery contributions with(node_id, name, fingerprint)def-node handles); a pre-6 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 7: the seed bundle gains asingleton_def_sourcestable (ADR-46 slice 4 extended to class/singleton methods) ANDdigestsswitches to ADR-87 packed stat entries; a pre-7 blob mismatches the gate and loads as nil (clean cold rebuild). 8: each seed bundle additionally gains a comment-strippedcode_fingerprintfor the B1 bundle-equality gate; a pre-8 blob mismatches and loads as nil (clean cold rebuild). 9: adds the ADR-88 WD1plugin_fact_digest(a fingerprint of the plugin fact SURFACE — ADR-9 facts, ADR-60 producer values, andincremental_state_fingerprinthooks — that a cached diagnostic can depend on but the global fingerprint does not capture); a pre-9 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 10: ADR-89 WD1 adds a per-filedeclaration_signatureto each seed bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate compares) and WD2 addsreturn_summaries(per-def observed-key return descriptors + mutation-effect sets the behavioural-stability gate compares); a pre-10 blob mismatches the SCHEMA gate and loads as nil (a clean cold rebuild — no migration). 10
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Returns the value of attribute path.
Class Method Summary collapse
-
.fingerprint(configuration:, roots:) ⇒ Object
The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full rebuild — the engine version + schema, the resolved configuration, the analysis roots (the path arguments, e.g.
["lib"], NOT the expanded file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is handled incrementally by the session, not a full rebuild), the resolved gem set (Gemfile.lock/rbs_collection), and the project's own RBS (signature_pathsfile contents).
Instance Method Summary collapse
-
#initialize(root:) ⇒ IncrementalSnapshot
constructor
A new instance of IncrementalSnapshot.
-
#load(fingerprint:) ⇒ Object
The stored Payload, or nil when absent / unreadable / schema or fingerprint mismatch / corrupt.
-
#save(fingerprint:, payload:) ⇒ Object
Persist
payloadunderfingerprint.
Constructor Details
#initialize(root:) ⇒ IncrementalSnapshot
Returns a new instance of IncrementalSnapshot.
118 119 120 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 118 def initialize(root:) @path = File.join(root.to_s, "incremental", "snapshot.bin") end |
Instance Attribute Details
#path ⇒ Object (readonly)
Returns the value of attribute path.
122 123 124 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 122 def path @path end |
Class Method Details
.fingerprint(configuration:, roots:) ⇒ Object
The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
rebuild — the engine version + schema, the resolved configuration, the analysis roots (the path
arguments, e.g. ["lib"], NOT the expanded file list — so a snapshot is keyed to an invocation's roots
but adding / removing a file under them is handled incrementally by the session, not a full rebuild),
the resolved gem set (Gemfile.lock / rbs_collection), and the project's own RBS (signature_paths
file contents). Built WITHOUT constructing the RBS environment so the warm path can gate the load
cheaply, before the costly env build. The --verify-incremental gate is the safety net for any
under-capture (it would surface as an incremental-vs-full mismatch). Returns nil on any error → the
caller falls back to a non-persisted run.
85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 85 def self.fingerprint(configuration:, roots:) parts = [ "engine:#{Rigor::VERSION}:#{SCHEMA}", "config:#{Digest::SHA256.hexdigest(Marshal.dump(configuration.to_h))}", "roots:#{Array(roots).map(&:to_s).sort.join("\n")}", "gems:#{digest_file_if_present('Gemfile.lock')}", "rbs_collection:#{digest_file_if_present('rbs_collection.lock.yaml')}", "sig:#{digest_signature_paths(configuration.signature_paths)}" ] Digest::SHA256.hexdigest(parts.join("\x00")) rescue StandardError nil end |
Instance Method Details
#load(fingerprint:) ⇒ Object
The stored Payload, or nil when absent / unreadable / schema or fingerprint mismatch / corrupt. Never raises.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 126 def load(fingerprint:) data = Marshal.load(Zlib::Inflate.inflate(File.binread(@path))) # rubocop:disable Security/MarshalLoad return nil unless data.is_a?(Hash) && data[:schema] == SCHEMA && data[:fingerprint] == fingerprint Payload.new( cache: data[:cache], sources: data[:sources], digests: data[:digests], analyzed: data[:analyzed], symbol_sources: data[:symbol_sources] || {}, ancestry_sources: data[:ancestry_sources] || {}, symbol_fingerprints: data[:symbol_fingerprints] || {}, missing: data[:missing] || {}, class_decls: data[:class_decls] || {}, seed_bundles: data[:seed_bundles] || {}, plugin_fact_digest: data[:plugin_fact_digest], return_summaries: data[:return_summaries] || {} ) rescue StandardError nil end |
#save(fingerprint:, payload:) ⇒ Object
Persist payload under fingerprint. Writes via a temp file + atomic rename so a concurrent reader
never sees a half-written snapshot. Returns true on success, false on any failure (never raises).
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/rigor/cache/incremental_snapshot.rb', line 148 def save(fingerprint:, payload:) FileUtils.mkdir_p(File.dirname(@path)) raw = Marshal.dump( schema: SCHEMA, fingerprint: fingerprint, cache: payload.cache, sources: payload.sources, digests: payload.digests, analyzed: payload.analyzed, symbol_sources: payload.symbol_sources, ancestry_sources: payload.ancestry_sources, symbol_fingerprints: payload.symbol_fingerprints, missing: payload.missing, class_decls: payload.class_decls, seed_bundles: payload.seed_bundles, plugin_fact_digest: payload.plugin_fact_digest, return_summaries: payload.return_summaries ) blob = Zlib::Deflate.deflate(raw) tmp = "#{@path}.#{Process.pid}.tmp" File.binwrite(tmp, blob) File.rename(tmp, @path) true rescue StandardError false end |