Class: Rigor::Cache::IncrementalSnapshot

Inherits:
Object
  • Object
show all
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 Store entries — the snapshot is the one cache artefact that does not go through Store); a raw pre-5 blob fails the inflate and loads as nil, the usual fault-tolerant cold-run path. 6: adds the ADR-85 WD2 seed_bundles section (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 a singleton_def_sources table (ADR-46 slice 4 extended to class/singleton methods) AND digests switches 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-stripped code_fingerprint for the B1 bundle-equality gate; a pre-8 blob mismatches and loads as nil (clean cold rebuild). 9: adds the ADR-88 WD1 plugin_fact_digest (a fingerprint of the plugin fact SURFACE — ADR-9 facts, ADR-60 producer values, and incremental_state_fingerprint hooks — 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-file declaration_signature to each seed bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate compares) and WD2 adds return_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

Class Method Summary collapse

Instance Method Summary collapse

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

#pathObject (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