feat(8.0): temporal range verbs — diff, history, since(gen|Date), asOf{exclusive}, transactionLog window

asOf() answers "state AT a point"; these answer "what happened BETWEEN two
points" and "one entity's whole history", all on the existing generation records.

- Extract resolveAsOfGeneration() as the shared gen|Date→{generation,timestamp}
  chokepoint (reachability + Date semantics identical everywhere). asOf()'s
  inclusive path is byte-identical — db-mvcc still 25/25.
- asOf(target, { exclusive }) — strict-before (resolved−1, clamped at gen 0,
  never a RangeError).
- db.since(Db | generation | Date) — overload; number/Date resolve via the shared
  resolver (new DbHost.resolveGeneration); EXCLUSIVE lower bound;
  since(db) === since(db.generation). Same-store guard via Db.belongsToStore.
- brain.diff(a, b) → { added, removed, modified } split by nouns/verbs. EARNS its
  name: candidate set is ONLY changedBetween(gLow,gHigh), each classified by
  existence at both endpoints + a key-order-insensitive value compare
  (new src/db/stableEqual.ts) — a touched-but-reverted / born-and-died id is in
  no bucket.
- brain.history(id, { from, to }) → every distinct version oldest→newest, each
  value === asOf(version.generation).get(id); null = removal; kind auto-detected
  (throws on UUID-space collision). New store helper generationsTouching().
- brain.transactionLog({ from, to, limit }) — INCLUSIVE generation/Date window
  (contrast since's exclusive lower bound); limit applied last.
- Compaction policy locked: diff/since THROW GenerationCompactedError below the
  horizon; history TRUNCATES to it.

Types DiffResult/HistoryVersion/EntityHistory exported from the package root.
Tests: tests/integration/db-temporal.test.ts (8 proofs incl. diff-earns-its-name,
history↔asOf cross-check, the composition proof, granularity, compaction contrast)
+ tests/unit/db/stableEqual.test.ts (6). docs/guides/snapshots-and-time-travel.md
+ RELEASES updated. 1477 unit + db-temporal 8 + db-mvcc 25 green.
This commit is contained in:
David Snelling 2026-06-19 13:21:02 -07:00
parent 373a48122d
commit 2c84f86815
10 changed files with 1069 additions and 37 deletions

View file

@ -51,9 +51,11 @@ What you get:
on conflict) — the big sibling of the per-entity `ifRev` CAS from 7.31.
`meta` is reified Datomic-style into an append-only transaction log,
readable via `brain.transactionLog()`.
- **`brain.asOf(generation | Date | snapshotPath)`** — time travel with the
**full query surface**: `get()`, `find()` in every mode, semantic search,
graph traversal, cursors, aggregation, all at the pinned past state.
- **`brain.asOf(generation | Date | snapshotPath, { exclusive? })`** — time
travel with the **full query surface**: `get()`, `find()` in every mode,
semantic search, graph traversal, cursors, aggregation, all at the pinned past
state. `exclusive: true` pins the generation immediately before the target
(strict-before).
- **`db.with(ops)`** — speculative writes applied in memory on top of a view.
Nothing touches disk, the generation counter, or the indexes. What-if
analysis, then `transact()` the same ops for real.
@ -63,10 +65,23 @@ What you get:
inodes). `brain.restore(path, { confirm: true })` replaces the whole store
from one; `Brainy.load(path)` opens one read-only with the full query
surface.
- **`db.since(olderDb)`** — exactly the entity and relationship ids that
committed transactions touched between two views.
- **Range verbs over history** — answer "what happened BETWEEN two points":
- **`db.since(Db | generation | Date)`** — the entity and relationship ids
committed transactions touched after an **exclusive** lower bound (now
accepts a generation or `Date`, not just a prior `Db`).
- **`brain.diff(a, b)`** — the touched ids CLASSIFIED as `{ added, removed,
modified }` (split by nouns/verbs) by resolving each at both endpoints; a
touched-but-reverted id lands in none of the buckets. Endpoints are a
generation, `Date`, or `Db`, in either order.
- **`brain.history(id, { from?, to? })`** — every distinct version of ONE
entity/relationship over a range, oldest first (`value: null` marks a
removal); each version equals `asOf(version.generation).get(id)`.
- **`brain.transactionLog({ from?, to?, limit? })`** — the commit log over an
**inclusive** generation/`Date` window (contrast `since`'s exclusive lower
bound); `limit` applies last.
- **`brain.compactHistory({ retainGenerations, retainMs })`** — explicit
retention. Compaction never breaks a pinned read.
retention. Compaction never breaks a pinned read. `diff`/`since` throw
`GenerationCompactedError` below the horizon; `history` truncates to it.
The precise guarantees (and the honest limits — history granularity is
`transact()` commits; single-operation writes advance the clock but produce no