brainy/docs/concepts/generation-fact-log.md

118 lines
5.5 KiB
Markdown
Raw Permalink Normal View History

---
title: The Generation Fact Log
slug: concepts/generation-fact-log
public: true
category: concepts
template: concept
order: 6
description: Every committed write also appends a self-verifying "fact" — an after-image commit record — to an append-only log. What facts are, the crash-safety model, the scanFacts() streaming surface, family stamps, and how index providers consume the log for sequential heals.
next:
- concepts/consistency-model
- guides/snapshots-and-time-travel
---
# The Generation Fact Log
Since 8.4.0, every committed generation also appends a **fact** — a compact record of what each
touched entity or relationship *became* — to an append-only, checksummed log under
`_generations/facts/`. Where the generational history answers *"what did things look like
before?"* (before-images, powering `asOf()` and rollback), the fact log answers *"what happened,
in order?"* — one sequential, self-verifying stream of the store's present being written.
Nothing about querying changes. The fact log exists for three consumers:
1. **Index heals and rebuilds** — one sequential read in commit order replaces a per-entity
directory walk over millions of files.
2. **Incremental catch-up** — a derived index that knows which generation it reflects reads *just
the gap*, instead of rebuilding from scratch.
3. **Replay and audit tooling** — anything that wants the store's committed timeline as a stream.
## What a fact is
One fact per committed generation:
- **`generation`** and **`timestamp`** — which commit, when.
- **`ops`** — every write in that commit: `{ kind: 'noun' | 'verb', id, record }` where `record`
holds the entity's full after-image (both stored legs), or **`null` for a tombstone** — a
removal carries no body, by design.
- **`meta`** — the transaction metadata `transact()` was submitted with, when present.
- **`blobHashes`** — content-blob references, for exact reclamation accounting.
Facts accumulate **from the first write after upgrading** — pre-existing history is not
retroactively converted, and consumers fall back to the enumeration walk when no log exists.
## Crash safety, in one paragraph
Facts are appended and fsynced **inside the same durability window as the commit itself**, before
the commit point — so after a crash, the log can only ever be *ahead* of committed truth, never
behind it with a hole. On open, the store reconciles the log back to the committed watermark:
torn tails are detected by per-record checksums and cut; whole records beyond the watermark are
truncated. The invariant every reader can rely on: **an absent generation was never committed; a
present fact was.** `transact()` facts are durable the moment `transact()` returns; single-op
facts share the same group-commit flush as the rest of their generation, so a hard kill loses the
fact and the generation *together* — never a torn state.
## Reading the log
```typescript
const scan = brain.scanFacts({ fromGeneration: 1 })
if (scan) {
// Telemetry up front — progress bars get a denominator from second zero.
console.log(scan.headGeneration, scan.segmentCount, scan.approxFactCount)
for await (const batch of scan.batches()) {
// Each batch: { facts, firstGeneration, lastGeneration, factCount, byteSize, segmentId }
for (const fact of batch.facts) {
for (const op of fact.ops) {
if (op.record === null) {
// a tombstone: op.id was removed in this generation
}
}
}
}
console.log(scan.summary()) // { factsYielded, segmentsRead } — the cross-check
}
```
- `scanFacts()` returns `null` when the store hosts no fact log (older store, or a storage adapter
without binary append support) — fall back to enumerating entities.
- Scans run against a **snapshot**: facts appended after the scan opens never bleed in, each fact
is yielded exactly once, and a detected gap aborts loudly — never a silent skip.
- `brain.factSegmentPaths()` returns the immutable, *sealed* segment files for zero-copy consumers
(the append-mutable tail is excluded — read it through `scanFacts()`).
## Family stamps: how a projection proves it's current
Anything derived from the store — an index, the entity file tree itself — carries a **family
stamp**: a small JSON record of *which committed generation the projection reflects*
(`sourceGeneration`) plus the invariants that verify it whole (exact per-file byte sizes for
bounded families; rollup invariants like entity counts for unbounded ones). At open, coherence is
a **comparison**, not a walk:
- stamp equals the committed watermark and invariants hold → serve;
- stamp is behind → the projection reads just the gap from the fact log;
- invariants fail → loud, named divergence — `brain.repairIndex()` rebuilds from canonical and
re-stamps.
The verifier is exported (`verifyFamilyStamp`) so every projection — TypeScript or native — runs
literally the same check.
## For plugin authors: the storage capability
Index providers receive the storage adapter, not the brain — so the host wires the log onto it.
Feature-detect and prefer the stream; fall back to enumeration:
```typescript
const scan = storage.scanFacts?.({ fromGeneration: stamp.sourceGeneration + 1 })
if (scan) {
// sequential catch-up from the log
} else {
// enumeration walk (older store or adapter)
}
const committed = storage.committedGeneration?.() // the watermark stamps compare against
```
Providers must never construct their own reader over the log's files — the open path belongs to
the single writer (it reconciles the log at open); the capability is the sanctioned seam.