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:
parent
373a48122d
commit
2c84f86815
10 changed files with 1069 additions and 37 deletions
53
tests/unit/db/stableEqual.test.ts
Normal file
53
tests/unit/db/stableEqual.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @module tests/unit/db/stableEqual
|
||||
* @description Unit proof for the key-order-insensitive deep-equal that
|
||||
* {@link Brainy.diff} uses to decide whether an id present at both endpoints
|
||||
* actually changed. The risk it guards: a naive JSON.stringify comparison is
|
||||
* key-order-sensitive and would report a no-op re-write as `modified`.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { stableDeepEqual } from '../../../src/db/stableEqual.js'
|
||||
|
||||
describe('stableDeepEqual', () => {
|
||||
it('ignores object key order (the core diff requirement)', () => {
|
||||
expect(stableDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
|
||||
expect(stableDeepEqual({ a: 1, b: 2, c: { x: 1, y: 2 } }, { c: { y: 2, x: 1 }, b: 2, a: 1 })).toBe(true)
|
||||
})
|
||||
|
||||
it('honors array order (arrays are order-significant)', () => {
|
||||
expect(stableDeepEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
||||
expect(stableDeepEqual([1, 2, 3], [3, 2, 1])).toBe(false)
|
||||
expect(stableDeepEqual([1, 2], [1, 2, 3])).toBe(false)
|
||||
})
|
||||
|
||||
it('recurses into nested objects and arrays', () => {
|
||||
expect(
|
||||
stableDeepEqual(
|
||||
{ tags: ['a', 'b'], meta: { n: 1, deep: { z: [1, { q: 2 }] } } },
|
||||
{ meta: { deep: { z: [1, { q: 2 }] }, n: 1 }, tags: ['a', 'b'] }
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
stableDeepEqual({ meta: { deep: { z: [1, { q: 2 }] } } }, { meta: { deep: { z: [1, { q: 3 }] } } })
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('distinguishes primitives, null, and types', () => {
|
||||
expect(stableDeepEqual(1, 1)).toBe(true)
|
||||
expect(stableDeepEqual(1, '1')).toBe(false)
|
||||
expect(stableDeepEqual(null, null)).toBe(true)
|
||||
expect(stableDeepEqual(null, {})).toBe(false)
|
||||
expect(stableDeepEqual({ a: 1 }, { a: 1, b: undefined })).toBe(false) // extra key
|
||||
expect(stableDeepEqual(true, false)).toBe(false)
|
||||
})
|
||||
|
||||
it('treats NaN as equal to NaN (stable value comparison)', () => {
|
||||
expect(stableDeepEqual(NaN, NaN)).toBe(true)
|
||||
expect(stableDeepEqual({ x: NaN }, { x: NaN })).toBe(true)
|
||||
expect(stableDeepEqual(NaN, 0)).toBe(false)
|
||||
})
|
||||
|
||||
it('an object and an array of the same length are not equal', () => {
|
||||
expect(stableDeepEqual({ 0: 'a', 1: 'b' }, ['a', 'b'])).toBe(false)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue