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. |
||
|---|---|---|
| .. | ||
| biography.test.ts | ||
| biographyHarness.ts | ||
| README.md | ||
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 --harddoes NOT remove untracked files — a "clean" tree can still carry stray test stores; usegit clean -fd tests/lifecycle-tmpequivalents.silent: truepatchesconsoleprocess-wide — never assert narration throughconsolespies in this lane; the engine's always-on channel isprodLog.