334 lines
14 KiB
TypeScript
334 lines
14 KiB
TypeScript
|
|
/**
|
||
|
|
* @module tests/integration/enumeration-population-law
|
||
|
|
* @description THE POPULATION LAW (ADR-008 G1): the unfiltered noun/verb walk
|
||
|
|
* and the canonical ALL scalar must agree on the population — a row's
|
||
|
|
* IDENTITY RECORD (metadata.json) is what defines membership; the vector leg
|
||
|
|
* is optional data, never a gate on visibility. Before this fix, the walk
|
||
|
|
* (getNounsWithPagination / getNounIdsWithPagination / getVerbsWithPagination)
|
||
|
|
* enumerated by keying on the VECTOR leg (`vectors.json`), so a row with
|
||
|
|
* metadata and no vector file was counted by the ledger (already
|
||
|
|
* metadata.json-keyed — see `rebuildTypeCounts`) but never yielded by the
|
||
|
|
* walk: a permanent "counted but invisible" phantom for any downstream
|
||
|
|
* consumer (a health-coverage row, an index-fill walk) that iterates the walk
|
||
|
|
* to account for the ledger's total.
|
||
|
|
*
|
||
|
|
* Two legs are pinned here:
|
||
|
|
* (a)/(b) LEG 1 — the walk re-keys on metadata.json. A fold-born
|
||
|
|
* metadata-only row (the exact shape `GenerationStore.replayFact` can
|
||
|
|
* leave behind, and the exact shape `writeNounRaw`/`writeVerbRaw` accept)
|
||
|
|
* must be YIELDED, hydrated with the sanctioned unvectored shape
|
||
|
|
* (`vector: []`) — not merely counted.
|
||
|
|
*
|
||
|
|
* For VERBS this closes only PARTIALLY: `sourceId`/`targetId` are
|
||
|
|
* HNSWVerb's structural core and live ONLY in the vector leg (never in
|
||
|
|
* metadata — see `RESERVED_RELATION_FIELDS` in reservedFields.ts, which
|
||
|
|
* does not include them). A metadata-only verb row therefore cannot be
|
||
|
|
* safely reconstructed without FABRICATING an edge's endpoints — which
|
||
|
|
* would silently create a phantom relationship, strictly worse than the
|
||
|
|
* original defect. The walk recovers the row when its metadata happens
|
||
|
|
* to carry `sourceId`/`targetId` (a defensive, forward-compatible
|
||
|
|
* fallback — never true for a CURRENT production write, but not
|
||
|
|
* disallowed either); otherwise it counts the row (ledger, unchanged)
|
||
|
|
* but loudly skips yielding it, logging the gap instead of hiding it.
|
||
|
|
* Closing this fully requires persisting `sourceId`/`targetId` in verb
|
||
|
|
* metadata — a schema change out of this task's scope; see the session
|
||
|
|
* report for the explicit call-out.
|
||
|
|
*
|
||
|
|
* (c)/(d) LEG 2 — the recovery fold's preserve-if-absent contract, exercised
|
||
|
|
* directly against `GenerationStore`/`FactLog` (below the `Brainy` API):
|
||
|
|
* a metadata-only after-image replayed over an already-vectored row must
|
||
|
|
* PRESERVE the existing vector leg (never delete it); a genuine tombstone
|
||
|
|
* (both legs absent) still removes both legs.
|
||
|
|
*/
|
||
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
|
|
import * as fs from 'node:fs'
|
||
|
|
import * as os from 'node:os'
|
||
|
|
import * as path from 'node:path'
|
||
|
|
import { randomUUID } from 'node:crypto'
|
||
|
|
import { Brainy } from '../../src/index.js'
|
||
|
|
import { GenerationStore } from '../../src/db/generationStore.js'
|
||
|
|
import { MemoryStorage } from '../../src/storage/adapters/memoryStorage.js'
|
||
|
|
import { LOG_AUTHORITY_PATH } from '../../src/db/logAuthority.js'
|
||
|
|
import type { CommitFact } from '../../src/db/factLog.js'
|
||
|
|
|
||
|
|
describe('enumeration population law — LEG 1 (identity-keyed walk)', () => {
|
||
|
|
let dir: string
|
||
|
|
let brain: any
|
||
|
|
|
||
|
|
const open = async () => {
|
||
|
|
const b: any = new Brainy({
|
||
|
|
requireSubtype: false,
|
||
|
|
storage: { type: 'filesystem', path: dir },
|
||
|
|
silent: true,
|
||
|
|
dimensions: 384
|
||
|
|
})
|
||
|
|
await b.init()
|
||
|
|
return b
|
||
|
|
}
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true'
|
||
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-population-law-'))
|
||
|
|
brain = await open()
|
||
|
|
})
|
||
|
|
afterEach(async () => {
|
||
|
|
await brain.close?.().catch(() => {})
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
||
|
|
})
|
||
|
|
|
||
|
|
it('(a) nouns.all equals the unfiltered walk-yield count with a fold-born metadata-only row present', async () => {
|
||
|
|
// Ordinary, fully-vectored background population.
|
||
|
|
await brain.add({ data: 'one', type: 'document' })
|
||
|
|
await brain.add({ data: 'two', type: 'document' })
|
||
|
|
await brain.flush()
|
||
|
|
|
||
|
|
// THE EXACT PRE-FIX SHAPE: a metadata-only row against a FRESH id — no
|
||
|
|
// vector ever existed for it. Written through the raw primitive directly,
|
||
|
|
// exactly as `GenerationStore.replayFact` (the recovery fold) applies a
|
||
|
|
// replayed after-image whose vector leg came back null.
|
||
|
|
const freshId = randomUUID()
|
||
|
|
await brain.storage.writeNounRaw(freshId, {
|
||
|
|
metadata: { noun: 'document', createdAt: Date.now(), updatedAt: Date.now(), _rev: 1 },
|
||
|
|
vector: null
|
||
|
|
})
|
||
|
|
|
||
|
|
// writeNounRaw bypasses count bookkeeping on purpose (its own JSDoc) — the
|
||
|
|
// sanctioned recount brings the ledger scalar to ground truth. This walk
|
||
|
|
// was ALREADY metadata.json-keyed before this fix (rebuildTypeCounts), so
|
||
|
|
// the recount's answer does not depend on today's change.
|
||
|
|
await brain.repairIndex()
|
||
|
|
|
||
|
|
const ledger = await brain.storage.getCanonicalCounts()
|
||
|
|
const walk = await brain.storage.getNouns({ pagination: { limit: 1000, offset: 0 } })
|
||
|
|
|
||
|
|
expect(walk.items.length).toBe(ledger.nouns.all)
|
||
|
|
expect(walk.totalCount).toBe(ledger.nouns.all)
|
||
|
|
|
||
|
|
const yielded = walk.items.find((n: any) => n.id === freshId)
|
||
|
|
expect(yielded, 'the metadata-only row must be YIELDED, not merely counted').toBeDefined()
|
||
|
|
expect(yielded.vector).toEqual([])
|
||
|
|
})
|
||
|
|
|
||
|
|
it('(a-ids) getNounIdsWithPagination (the zero-read unfiltered enumerator) also yields the metadata-only row', async () => {
|
||
|
|
await brain.add({ data: 'one', type: 'document' })
|
||
|
|
await brain.flush()
|
||
|
|
|
||
|
|
const freshId = randomUUID()
|
||
|
|
await brain.storage.writeNounRaw(freshId, {
|
||
|
|
metadata: { noun: 'document', createdAt: Date.now(), updatedAt: Date.now(), _rev: 1 },
|
||
|
|
vector: null
|
||
|
|
})
|
||
|
|
await brain.repairIndex()
|
||
|
|
|
||
|
|
const ledger = await brain.storage.getCanonicalCounts()
|
||
|
|
const page = await brain.storage.getNounIdsWithPagination({ limit: 1000, offset: 0 })
|
||
|
|
expect(page.ids.length).toBe(ledger.nouns.all)
|
||
|
|
expect(page.ids).toContain(freshId)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('(b) verbs.all counts a fold-born metadata-only row; the walk yields it when endpoints are recoverable from metadata, and loudly skips (never fabricates) when they are not', async () => {
|
||
|
|
const a = await brain.add({ data: 'a', type: 'document' })
|
||
|
|
const b = await brain.add({ data: 'b', type: 'document' })
|
||
|
|
await brain.relate({ from: a, to: b, type: 'relatedTo' })
|
||
|
|
await brain.flush()
|
||
|
|
|
||
|
|
// Case 1 — the REALISTIC production shape: metadata carries the verb
|
||
|
|
// type (a reserved field, kept for backward compat) but never
|
||
|
|
// sourceId/targetId — those are HNSWVerb's structural core and live
|
||
|
|
// ONLY in the vector leg. The walk cannot safely fabricate them (an
|
||
|
|
// empty-string endpoint would silently create a phantom edge), so this
|
||
|
|
// row is counted by the ledger but not yielded — a documented,
|
||
|
|
// loudly-logged gap, not a silent one.
|
||
|
|
const gapId = randomUUID()
|
||
|
|
await brain.storage.writeVerbRaw(gapId, {
|
||
|
|
metadata: { verb: 'relatedTo', createdAt: Date.now(), updatedAt: Date.now(), weight: 1 },
|
||
|
|
vector: null
|
||
|
|
})
|
||
|
|
|
||
|
|
// Case 2 — endpoints ARE recoverable from metadata (never true for a
|
||
|
|
// current production write; modeled here as what a repair tool or a
|
||
|
|
// future schema could supply): the walk reconstructs and yields it.
|
||
|
|
const recoveredId = randomUUID()
|
||
|
|
await brain.storage.writeVerbRaw(recoveredId, {
|
||
|
|
metadata: {
|
||
|
|
verb: 'relatedTo',
|
||
|
|
sourceId: a,
|
||
|
|
targetId: b,
|
||
|
|
createdAt: Date.now(),
|
||
|
|
updatedAt: Date.now(),
|
||
|
|
weight: 1
|
||
|
|
},
|
||
|
|
vector: null
|
||
|
|
})
|
||
|
|
|
||
|
|
await brain.repairIndex()
|
||
|
|
const ledger = await brain.storage.getCanonicalCounts()
|
||
|
|
const walk = await brain.storage.getVerbs({ pagination: { limit: 1000, offset: 0 } })
|
||
|
|
|
||
|
|
// The ledger counts every identity record — the real edge plus both
|
||
|
|
// synthetic metadata-only rows — unaffected by whether the walk can
|
||
|
|
// safely hydrate them.
|
||
|
|
expect(ledger.verbs.all).toBe(3)
|
||
|
|
|
||
|
|
const recovered = walk.items.find((v: any) => v.id === recoveredId)
|
||
|
|
expect(recovered, 'endpoints recoverable from metadata must be yielded').toBeDefined()
|
||
|
|
expect(recovered.sourceId).toBe(a)
|
||
|
|
expect(recovered.targetId).toBe(b)
|
||
|
|
expect(recovered.vector).toEqual([])
|
||
|
|
|
||
|
|
// The documented gap: counted, not yielded — this is the one corner of
|
||
|
|
// the population law this task does NOT close (see the session report).
|
||
|
|
const gapped = walk.items.find((v: any) => v.id === gapId)
|
||
|
|
expect(gapped).toBeUndefined()
|
||
|
|
expect(walk.items.length).toBeLessThan(ledger.verbs.all)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('enumeration population law — LEG 2 (fold preserve-if-absent, below the Brainy API)', () => {
|
||
|
|
/** A GenerationStore whose brain has already flipped to log authority — the
|
||
|
|
* precondition for `replayFact` (the recovery fold) to run at open(). */
|
||
|
|
async function openLogAuthorityStore(): Promise<{ storage: MemoryStorage; store: GenerationStore }> {
|
||
|
|
const storage = new MemoryStorage()
|
||
|
|
await storage.init()
|
||
|
|
await storage.writeRawObject(LOG_AUTHORITY_PATH, { authority: 'log' })
|
||
|
|
const store = new GenerationStore(storage)
|
||
|
|
await store.open()
|
||
|
|
return { storage, store }
|
||
|
|
}
|
||
|
|
|
||
|
|
it('(c) nouns: a metadata-only after-image replayed over a vectored row PRESERVES the vector; it stays readable and the vectored ledger is untouched either way', async () => {
|
||
|
|
const { storage, store } = await openLogAuthorityStore()
|
||
|
|
const id = randomUUID()
|
||
|
|
const vectorRecord = { id, vector: [0.1, 0.2, 0.3], connections: {}, level: 0 }
|
||
|
|
|
||
|
|
// Generation 1 — a real, honest commit: both legs land together.
|
||
|
|
await store.commitTransaction({
|
||
|
|
touched: { nouns: [id], verbs: [] },
|
||
|
|
execute: async () => {
|
||
|
|
await storage.writeNounRaw(id, {
|
||
|
|
metadata: { noun: 'document', createdAt: 1000, updatedAt: 1000, _rev: 1 },
|
||
|
|
vector: vectorRecord
|
||
|
|
})
|
||
|
|
}
|
||
|
|
})
|
||
|
|
const beforeVectoredCount = (await storage.getCanonicalCounts()).vectors.all
|
||
|
|
|
||
|
|
// THE ANOMALOUS FACT, crafted directly (bypassing commitTransaction,
|
||
|
|
// whose honest read-after-write could never produce this on its own):
|
||
|
|
// metadata changed, vector leg null, while the row is STILL vectored on
|
||
|
|
// disk. This is exactly the shape the recovery fold must tolerate —
|
||
|
|
// modeling the confirmed production defect at the replay boundary.
|
||
|
|
const factLog = store.getFactLog()!
|
||
|
|
const anomalousFact: CommitFact = {
|
||
|
|
generation: 2,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
ops: [
|
||
|
|
{
|
||
|
|
kind: 'noun',
|
||
|
|
id,
|
||
|
|
record: {
|
||
|
|
metadata: { noun: 'document', createdAt: 1000, updatedAt: 2000, _rev: 2 },
|
||
|
|
vector: null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
await factLog.append(anomalousFact)
|
||
|
|
await factLog.sync()
|
||
|
|
|
||
|
|
// Reopen — a fresh GenerationStore over the SAME storage. Generation 2's
|
||
|
|
// fact sits above the (still generation-1) manifest, so it replays
|
||
|
|
// through the recovery fold — `replayFact`'s own call site.
|
||
|
|
const store2 = new GenerationStore(storage)
|
||
|
|
await store2.open()
|
||
|
|
|
||
|
|
const after = await storage.readNounRaw(id)
|
||
|
|
expect(after.vector, 'the vector leg must survive the metadata-only replay').not.toBeNull()
|
||
|
|
expect((after.vector as { vector: number[] }).vector).toEqual([0.1, 0.2, 0.3])
|
||
|
|
expect((after.metadata as { updatedAt: number }).updatedAt).toBe(2000) // the new metadata DID apply
|
||
|
|
|
||
|
|
// writeNounRaw bypasses ledger bookkeeping either way (by design — see
|
||
|
|
// its JSDoc), so this scalar is unaffected by the replay regardless of
|
||
|
|
// outcome; asserted for completeness against the task's exact wording.
|
||
|
|
const afterVectoredCount = (await storage.getCanonicalCounts()).vectors.all
|
||
|
|
expect(afterVectoredCount).toBe(beforeVectoredCount)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('(c-verb) verbs: a metadata-only after-image replayed over a vectored edge PRESERVES the vector leg (sourceId/targetId/verb intact)', async () => {
|
||
|
|
const { storage, store } = await openLogAuthorityStore()
|
||
|
|
const id = randomUUID()
|
||
|
|
const sourceId = randomUUID()
|
||
|
|
const targetId = randomUUID()
|
||
|
|
const vectorRecord = { id, vector: [0.7, 0.8], connections: {}, verb: 'relatedTo', sourceId, targetId }
|
||
|
|
|
||
|
|
await store.commitTransaction({
|
||
|
|
touched: { nouns: [], verbs: [id] },
|
||
|
|
execute: async () => {
|
||
|
|
await storage.writeVerbRaw(id, {
|
||
|
|
metadata: { verb: 'relatedTo', createdAt: 1000, updatedAt: 1000, weight: 1 },
|
||
|
|
vector: vectorRecord
|
||
|
|
})
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
const factLog = store.getFactLog()!
|
||
|
|
const anomalousFact: CommitFact = {
|
||
|
|
generation: 2,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
ops: [
|
||
|
|
{
|
||
|
|
kind: 'verb',
|
||
|
|
id,
|
||
|
|
record: {
|
||
|
|
metadata: { verb: 'relatedTo', createdAt: 1000, updatedAt: 2000, weight: 2 },
|
||
|
|
vector: null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
await factLog.append(anomalousFact)
|
||
|
|
await factLog.sync()
|
||
|
|
|
||
|
|
const store2 = new GenerationStore(storage)
|
||
|
|
await store2.open()
|
||
|
|
|
||
|
|
const after = await storage.readVerbRaw(id)
|
||
|
|
expect(after.vector, 'the vector leg must survive the metadata-only replay').not.toBeNull()
|
||
|
|
expect((after.vector as { sourceId: string }).sourceId).toBe(sourceId)
|
||
|
|
expect((after.vector as { targetId: string }).targetId).toBe(targetId)
|
||
|
|
expect((after.metadata as { weight: number }).weight).toBe(2)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('(d) a genuine tombstone replay removes BOTH legs (never preserved)', async () => {
|
||
|
|
const { storage, store } = await openLogAuthorityStore()
|
||
|
|
const id = randomUUID()
|
||
|
|
const vectorRecord = { id, vector: [0.4, 0.5, 0.6], connections: {}, level: 0 }
|
||
|
|
|
||
|
|
await store.commitTransaction({
|
||
|
|
touched: { nouns: [id], verbs: [] },
|
||
|
|
execute: async () => {
|
||
|
|
await storage.writeNounRaw(id, {
|
||
|
|
metadata: { noun: 'document', createdAt: 1000, updatedAt: 1000, _rev: 1 },
|
||
|
|
vector: vectorRecord
|
||
|
|
})
|
||
|
|
}
|
||
|
|
})
|
||
|
|
expect((await storage.readNounRaw(id)).vector).not.toBeNull() // sanity: it landed
|
||
|
|
|
||
|
|
const factLog = store.getFactLog()!
|
||
|
|
await factLog.append({
|
||
|
|
generation: 2,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
ops: [{ kind: 'noun', id, record: null }] // a genuine tombstone — both legs absent
|
||
|
|
})
|
||
|
|
await factLog.sync()
|
||
|
|
|
||
|
|
const store2 = new GenerationStore(storage)
|
||
|
|
await store2.open()
|
||
|
|
|
||
|
|
const after = await storage.readNounRaw(id)
|
||
|
|
expect(after.metadata, 'a genuine delete removes the metadata leg').toBeNull()
|
||
|
|
expect(after.vector, 'a genuine delete removes the vector leg too — preserve-if-absent never applies to a tombstone').toBeNull()
|
||
|
|
})
|
||
|
|
})
|