open-brainy/tests/lifecycle
David Snelling 8fc553b126
Some checks failed
CI / Node 22 (push) Successful in 12m14s
CI / Node 24 (push) Successful in 12m15s
CI / Integration + conformance (Node 22) (push) Failing after 14m55s
CI / Bun (latest) (push) Successful in 12m24s
fix(hnsw): skip unvectored rows on rebuild; refuse empty vectors in the index
A canonical row persisted with vector: [] (a system row, a deferred embed
not yet landed, or any other legitimately-unvectored record) is a normal,
enumerable row -- but rebuild()'s storage walk had no guard against it.
storage.getVectorIndexData() derives its answer from the row's own record,
so it returns non-null for any existing noun whether or not that noun was
ever actually indexed -- rebuild() admitted such rows into the live graph
with a length-0 vector. A vector-less node could become the entry point (or
occupy any graph position); the next real insert then ran a distance
calculation against it and blew up with a dimension mismatch.

Fix at two layers in src/hnsw/hnswIndex.ts:
- rebuild() now skips any row whose vector.length === 0 before it ever
  becomes a graph node (one summary count line, never per-row spam), and
  restores the pinned dimension from the first real vector it loads --
  previously the pin stayed null across a restart, since addItem/updateItem
  are the only sites that set it and rebuild() never goes through either.
- addItem/updateItem now refuse a length-0 vector with a typed
  EmptyVectorIndexError instead of ever pinning dimension to 0 or storing a
  vector-less node, so no future fill/rebuild/load path can poison the index
  silently. getVectorSafe's lazy-load "not found" check also missed that an
  empty array is truthy -- tightened to catch it.

IndexOperations.ts's ReplaceInVectorIndexOperation rollback paths now skip
re-adding an oldVector of length 0 (never a legal index member) instead of
attempting an illegal empty re-insert on rollback.

biography.test.ts's final ledger-exactness assertion assumed every noun the
lane creates is vectored, including the VFS root counted in
vfsBaselineNouns -- but the root is deliberately persisted unvectored.
Corrected the expected formula to exclude it.

Adds tests/integration/index-skips-unvectored.test.ts pinning: rebuild()
indexes only vectored rows with the dimension pinned correctly; clear()
then real adds never trip a dimension mismatch; addItem/updateItem refuse a
length-0 vector; and a crash/repair cycle stays dimension-consistent.
2026-08-27 13:29:11 -07:00
..
biography.test.ts fix(hnsw): skip unvectored rows on rebuild; refuse empty vectors in the index 2026-08-27 13:29:11 -07:00
biographyHarness.ts feat(health): the gate reads the named report — reads refuse loudly, never rebuild; open serves before it returns; the ceremony door 2026-08-24 12:45:51 -07:00
README.md docs(release): the 10.4.0 entry, the index-health concept doc, and the API surfaces — written from the tree, not the plan 2026-08-25 10:02:25 -07:00

The Lifecycle Lane

One brain, driven through founding, a working day, a clean restart, a crash, a repair, and a second life, checked chapter by chapter against an independent shadow-model referee (biographyHarness.ts). It catches COMPOSITION regressions unit tests miss — a store fine in one process but broken across a restart/crash/repair. Runs on the plain JS engine, so it gates every commit.

Run it: npx vitest run tests/lifecycle --pool=forks

A red names the chapter label, the id, and expected-vs-actual — diagnosable from the message alone. biography.test.ts is split into two it blocks (Ch1-3, then Ch4-6) purely for reporting; it is still ONE fixed-order story. Chapters must never be reordered, skipped, or made conditional, and a failing chapter's assertion must never be weakened to force green.

Lab notes (hard-won, keep):

  • git reset --hard does NOT remove untracked files — a "clean" tree can still carry stray test stores; use git clean -fd tests/lifecycle-tmp equivalents.
  • silent: true patches console process-wide — never assert narration through console spies in this lane; the engine's always-on channel is prodLog.