Skip to content

Storage and Cache Strategy

Bifrost reuses a stored answer only while its source, language semantics, and configuration still match the request. Durable facts, workspace membership, derived in-memory values, and generated semantic-model objects use different identity and retention rules.

Source content is published as complete relational facts, workspace revisions select the live projection, and bounded readers build complete derived values. Semantic-pack objects remain in a separate catalog.

Storage must support reuse across processes and compatible Git worktrees while keeping stale or incomplete state out of current answers. Indexed relational queries avoid reopening every file, and explicit budgets constrain memory and concurrent work under interactive workloads.

The design separates identity, liveness, and retention. A row can remain on disk after the live workspace stops selecting it. An evicted derived value can be rebuilt from its durable inputs. Garbage collection may lag invalidation while selection rules keep the old state out of current answers.

LayerIdentityLifetimeRole
Content factsContent or Git blob identity, language storage key, and analysis generationAcross revisions, processes, and compatible worktreesQueryable declarations, ranges, imports, hierarchy inputs, and other source-derived facts
Workspace projectionBound workspace, revision, relative path, and selected language generationOne immutable view of a workspaceMaps live paths to content and prevents facts from another revision or worktree leaking into a request
Derived valuesFull semantic and dependency key for the value being computedMemory-resident by default; selected exact-evidence summaries can survive across processesSemantic artifacts, graph projections, evidence indexes, and reusable procedure summaries

The persistent analyzer store is SQLite. Its rows are normalized, queryable relations: declarations and their structured names, source ranges, signatures, imports, containment, supertypes, and other inputs used by higher layers.

For Git-tracked source, the blob object ID supplies content identity. The same bytes can reuse parsed facts at another path, revision, or linked worktree. Language and dialect also enter that identity: TypeScript and TSX, JavaScript and JSX, or C and C++, can produce different facts from byte-identical input.

A parse or extraction job prepares its complete fact set, then publishes that set with a completion marker in one transaction. Cancellation, timeout, parser failure, and interrupted writers leave an unusable candidate. Readers select only committed facts marked complete in the active analysis generation.

For Rust, the lowered semantic IR and each procedure’s partial flows are content facts as well. They are written in the blob’s parse transaction under a publication head and a semantic producer epoch, and they depend on nothing outside the file. A blob without a current publication is reparsed by the startup reconcile. Until then, and for unsaved overlays, the artifact is lowered on demand in memory, and that lowering writes no rows. The byte-capped semantic artifact cache holds complete artifacts of both kinds: decoded from rows or lowered on demand. The Dataflow Engine chapter describes what the rows contain and how taint queries compose them.

The store also needs a path mapping for the files in a workspace. An immutable workspace revision maps workspace-relative paths to content identities and records workspace-specific structures such as package mounts and path-to-symbol anchors.

Each read connection selects the revision it serves. Its live views join that selection with the active per-language generation and complete content facts. This projection is the main isolation boundary:

  • an old row may remain physically present but is invisible to the live view;
  • two worktrees can share blob facts while retaining different path mappings;
  • a request observes one selected projection instead of a mixture of revisions; and
  • replacing a path-to-content mapping does not require rewriting global facts for content that is still reusable elsewhere.

Source files and retained configuration inputs share this revision selection. Their roles remain explicit: only source inputs own declaration and package projection rows. Cargo manifests and Cargo configuration files (.cargo/config.toml) retain their exact bytes independently of parsed source facts, so an older revision remains interpretable after reopening the cache. Changing either advances the workspace revision without replacing unchanged source publications. Cargo interpretation, including the cfg set and patches a configuration file selects, is derived from those selected bytes; parsed manifest and configuration tables are not a second durable authority.

Scoped sessions share the repository’s content facts but select their files through a unique session projection. They never reconcile their partial file listing under the live workspace’s identity. The projection remains available while any analyzer context uses it and is deleted when the last owner drops; the content facts remain reusable by later scoped or whole-workspace builds. Scoped builds use the same cache-location resolution and build lock as normal persisted builds, including both cache-directory overrides.

Unsaved buffers participate as snapshot-specific content. Their bytes enter the request’s content identity and remain separate from the committed disk version at the same path.

Per-file semantic artifacts, usage-ranking graphs, structural indexes, resolver evidence, and bounded graph projections are expensive or request-specific. These immutable values normally live in byte- or weight-bounded in-memory caches. Complete acyclic class-set procedure summaries are a narrow durable exception: the store records their normalized transfer rows, exact child dependencies, structured dispatch reads, and replay charges under one current procedure-local head.

Publication requires:

  1. the key names every input that can change the value;
  2. same-key work is single-flight, so concurrent requests share the same expensive construction;
  3. only a validated complete value becomes ready for later callers; and
  4. failure, cancellation, partial discovery, or budget exhaustion is returned to the requesting caller but is never published as a reusable complete value.

Eviction can require a later rebuild. The rebuilt value follows the same publication contract, so its published meaning remains bound to its key. For durable class-set summaries, a changed child or dispatch read triggers demand-scoped revalidation. Changed output recomputes demanded parents, while equal output only rebinds evidence; incomplete, corrupt, cancelled, or over-budget reuse falls back to a fresh witness-capable solve.

Versions, generations, revisions, and epochs

Section titled “Versions, generations, revisions, and epochs”

Several version-like values protect different boundaries:

MechanismChanges whenEffect
Store schema versionThe relational layout or migration contract changesOpens its own versioned store; collection chooses at most one other by most recent use and removes it after two weeks unused; a busy store waits for later collection
Language epochGrammar, extraction queries, or language-specific persisted semantics changeRotates that language’s active generation so incompatible rows become invisible
Workspace revisionThe live path-to-content or workspace projection changesSelects a new immutable live view without invalidating reusable content elsewhere
Content identityFile bytes or an unsaved overlay changeInvalidates products that depend on that content
Representation versionA derived artifact or graph encoding changesPrevents an old in-memory representation from satisfying the new contract

A language epoch is a stable fingerprint of the parser grammar, extraction queries, and an explicit language-specific semantic salt. Rotation follows a change to persisted meaning, even when the database columns are unchanged; a release number by itself is not a rotation trigger.

Workspace content identity combines normalized relative paths and their content identities, the relevant language epochs, analyzer configuration, and overlays. Absolute checkout paths and process-local generation numbers are excluded. Byte-equivalent checkouts can then match, and each language can rotate independently. When Bifrost cannot establish the required identity, it rebuilds.

SQLite runs in write-ahead logging mode. A bounded pool of read-only connections can serve committed snapshots while one writer connection publishes new facts. Inside a process, a writer actor owns that connection and serializes write jobs; SQLite supplies the corresponding cross-process arbitration.

The complete workspace build adds a per-cache build lock. Linked worktrees often discover the same missing blobs at the same time; independent parsing and publication of that identical set would waste CPU and increase writer contention. Garbage collection takes the same lock because a build publishes content facts before the workspace projection that retains them. A collector must observe either side of that complete reconciliation, never the interval between them. One elected build reconciles the shared store while the others wait and then observe its committed work. The elected build may still parallelize parsing internally before serialized publication.

A normal persisted-build lifecycle is:

  1. compute the desired workspace revision and relevant language epochs;
  2. acquire the build lock for the exact cache database;
  3. recheck the store, because another process may have completed work while this process waited;
  4. parse only missing or incompatible content;
  5. publish each complete fact set through the writer;
  6. commit the new workspace projection;
  7. let reader connections select that immutable revision.

That serialization is scoped to store reconciliation. Ordinary queries run independently of an unrelated analysis request, and readers continue seeing the prior committed snapshot until the new revision becomes available.

Every reusable value has a dependency-complete key. Depending on its layer, that key can include:

  • workspace content identity and selected store generations;
  • disk or overlay content identity;
  • language, dialect, and adapter semantics version;
  • analyzer and resolver configuration;
  • semantic-model or dependency fingerprints;
  • requested projection, filter, or scope; and
  • the representation version of the cached value.

Include an input in the key whenever changing it can change the answer. Exact content and normalized workspace coordinates usually supply the identity; timestamps and absolute checkout paths add no discriminating information. Omitting a semantic dependency risks serving one request’s answer to another.

Generated semantic-model packs have their own sharing and validation rules. They live in a separate versioned catalog with SQLite metadata and content-addressed SHA-256 object shards. Production binds the exact input digest, producer identity and version, semantic schema version, and catalog production version.

The separate catalog has its own lifecycle:

  • analyzer databases remain scoped to a repository’s source and workspace projections;
  • default persistent catalogs reuse exact generated productions through a separate user-owned machine cache, copying only the requested production;
  • workspace models and activation state remain in the repository catalog;
  • an explicitly shared semantic-pack root can also reuse an identical generated dependency pack across otherwise unrelated repositories;
  • pack objects can be staged, validated, and atomically published without entering the analyzer writer pipeline; and
  • catalog retention and provenance do not become analyzer-row liveness rules.

Activation remains snapshot-bound. Catalog membership establishes availability; the workspace and request determine applicability. The modeling contract is described in Semantic Model Packs and Semantic Models and Summaries.

New revisions, epochs, or validity keys immediately stop old state from matching. Physical deletion follows later, keeping foreground edits independent of a full database sweep.

Opportunistic garbage collection then reclaims unreachable analyzer rows, stale language generations, and superseded versioned stores. Reachability is seeded from repository references, active worktree state, and retained workspace revisions, including immutable diff snapshots and current working files that no committed revision represents. Retained revision roots are read inside the deletion transaction so revisions published during the repository walk remain protected. Collection is best effort and throttled. Failure to reclaim space must preserve every live fact set and leave incomplete candidates unpublished. A collection a workspace build has scheduled belongs to that workspace: it runs to completion, and closing the workspace waits for it, so no maintenance write follows a close and a short-lived session still advances the cadence.

DecisionBenefitCost
Persist normalized facts in SQLiteIndexed, composable queries and transactional publicationSchema and migration discipline are required
Share content rows, isolate workspace projectionsCross-worktree reuse without cross-worktree path leakageEvery query must pass through the selected live view
Rotate per-language generations from semantic epochsImmediate logical invalidation with isolated blast radiusOld rows need later physical reclamation
Use one writer and serialize whole-store reconciliationPredictable publication and less duplicate build workSimultaneous cold starts for one cache do not all build independently
Cache only complete immutable derived valuesReuse cannot promote partial work into certaintyCancelled or bounded work may be recomputed
Separate semantic-pack objects from analyzer rowsIndependent provenance, sharing, and retention boundariesOperators must reason about two stores with distinct lifetimes

Status: Current for Rust; Direction for other languages. Name and member resolution is composed from small immutable pieces. Each file’s binding graph and typed facts are stored under the file’s content identity. Each build unit’s context rows, such as a Cargo crate’s module tree and exports, are stored under a digest of their inputs. A query joins those pieces for the selected revision. An edit replaces only the changed file’s pieces and derives again only the build units it affects. The Name Resolution Engine chapter describes the design.

Bifrost does not store a resolved answer for reuse by later requests. Each query joins the stored pieces again. Storing resolved answers would require fragment identities that cover language semantics, scope, visibility, dependency graphs, ambiguity, and negative-result completeness, and an empty candidate set could be stored only when its search is provably exhaustive. Languages that have not moved onto the engine recompute at the coarser complete-value boundaries above; see Language status.

Immutable compositional usage facts specifies workspace composition; its roadmap entry is Compose immutable usage facts.

By default, the persistent cache follows the primary Git repository so linked worktrees can share compatible content facts. Non-Git or ephemeral analysis can use a temporary store. Cache-root overrides, filesystem permissions, relocation, and deletion are operational contracts; see Data Boundaries and Local Analysis for user-facing details.