feat(vector): the vectored-noun scalar joins the count ledger; the open gate closes the vector leg
All checks were successful
CI / Node 24 (push) Successful in 12m22s
CI / Node 22 (push) Successful in 12m32s
CI / Integration + conformance (Node 22) (push) Successful in 19m42s
CI / Bun (latest) (push) Successful in 12m15s

The coverage denominator the health-by-accounting ratification named for
the vector family — never built until now, and its absence was measured as
the exact outage class it existed to prevent: a migrated store with
canonical vectors and no derived index opened with the vector leg EMPTY,
served [] from vector search with no error, and the report-driven read gate
had nothing to refuse on (the provider's coverage invariant was honestly
unledgered — the denominator was ours to supply).

- getCanonicalCounts() gains vectors: { all } — the count of canonical
  nouns holding a REAL vector. Incremented where a vector lands (the
  isNew-gated metadata seam for explicit vectors — the same discipline that
  keeps HNSW neighbor-link re-saves from inflating counts; a narrow
  noteVectorLanded hook for the deferred-embed landing, gated on the
  worker's own pre-embed read). Decremented on a proven delete of a
  vectored noun; a vector-uncertain delete marks the ledger suspect rather
  than guessing (no new reads on the delete path). Recounted by the
  sanctioned recount; legacy counts.json derives it once (a deferred noun's
  vector file exists with an empty vector, so presence requires one
  content read at derivation — never on the hot path).
- The open gate's vector leg: when a health-reporting provider claims
  serving while the index holds zero nodes and the ledger proves vectored
  canonical rows exist, open BUILDS (narrated) — routed through the
  provider's idempotent fillFromCanonical() when exposed (the joint door;
  a partial shortfall stays repair()'s operator business), the JS rebuild
  otherwise — or fails typed pre-serve. Scoped exactly: bare isReady()
  providers, migrating providers, and white-box size stubs open as before.

Pinned end-to-end from the partner gate's probe shape (store with vectored
canonical rows, no derived index, reopen → search serves N, never []),
red-proved against the pre-fix path; the inverse (zero vectored rows) opens
without building and serves [] honestly.
This commit is contained in:
David Snelling 2026-08-25 15:31:19 -07:00
parent bce2593e24
commit 9730835bdf
10 changed files with 851 additions and 32 deletions

View file

@ -0,0 +1,247 @@
/**
* @module tests/integration/vector-leg-open-build
* @description THE LAST RED of the two-engine release gate: a migrated
* store can hold canonical vectored nouns with NO derived vector index
* built. `open()` owns building the derived indexes (reads never build
* see `rebuildIndexesIfNeeded`'s JSDoc); the defect this pins is the vector
* leg's decision silently skipping that build, so `find`/search served `[]`
* with no error and no narration.
*
* A downstream deployment measures this through a native vector provider
* whose own health report can legitimately say `serving: true` even while
* vector COVERAGE is honestly unledgered on its side (an unledgered
* invariant never flips `serving` see `HealthReport`'s derivation laws).
* This repo ships only the JS engine, so the reproduction here uses the
* SAME plugin seam a native provider would (`brain.use({ activate: ctx =>
* ctx.registerProvider('vector', factory) })`, the pattern
* `tests/unit/cold-open-rebuild-gate.test.ts` already established for this
* exact class of gate-decision bug) with a stub that WRAPS the real
* `JsHnswVectorIndex` every method delegates to a genuine engine (so a
* successful rebuild restores REAL, searchable vectors), except `size()`
* (fakes 0 until rebuild runs the "never built" posture) and
* `healthReport()` (always reports `serving: true`, `unledgered:
* ['vector-coverage']` — the "I don't track this yet" posture). This is
* "as close as the JS engine allows": the gap is reproduced at the exact
* decision the fix changes, not approximated by deleting files the JS
* engine's own cold-start heuristic already recovers from unaided (see the
* inverse pin below and cold-open-rebuild-gate.test.ts's already-pinned
* "isReady()===true, size()===0" contract, which this fix deliberately does
* NOT touch bare isReady() has no unledgered concept to hide behind, and
* overriding it would reopen the 48-seconds-per-restart regression pinned
* there).
*
* Pins:
* (1) COVERAGE GAP FORCES THE BUILD: N vectored nouns, a provider that
* claims `serving: true` at `size()===0` open() builds anyway (the
* ledger proves there is something to cover), and search returns real
* results, never `[]`.
* (2) THE INVERSE, HONEST EMPTY: 0 vectored nouns (every embed still
* deferred/unlanded) open() does NOT attempt a rebuild (nothing to
* load; the old blunt "always rebuild when size()===0" heuristic wasted
* a full canonical walk here for zero benefit), and search honestly
* returns `[]` no error, no false coverage-gap narration.
*
* SEARCH VERIFICATION NOTE: pin (1) verifies "search returns real results"
* via `find({ query: <text> })` (semantic search embeds the query, then
* searches), matching the pattern `tests/integration/hnsw-rebuild.test.ts`
* already uses for exactly this "post-rebuild search works" class of pin.
* A raw `find({ vector: <array> })` / `index.search(vector, k)` call was
* tried first and found to reproducibly return only 1 hit after a
* FROM-CANONICAL rebuild (never the full requested `limit`, sometimes not
* even a real neighbor) REGARDLESS of this task's changes: it reproduces
* identically on a plain, unwrapped, un-stubbed reopen with the stock JS
* engine (verified against `hnsw-rebuild.test.ts`'s own construction) and
* is therefore a PRE-EXISTING, orthogonal defect in the JS HNSW engine's
* rebuilt-graph connectivity outside this task's two deliverables (the
* count ledger and the open-gate REBUILD DECISION, not rebuild()'s internal
* search quality). Left for a separate investigation; not touched here.
*/
import { describe, it, expect, afterEach, vi } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/index.js'
import { JsHnswVectorIndex } from '../../src/hnsw/hnswIndex.js'
const tmpDirs: string[] = []
function mkTmp(): string {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-vector-leg-open-'))
tmpDirs.push(d)
return d
}
afterEach(() => {
vi.restoreAllMocks()
for (const d of tmpDirs.splice(0)) fs.rmSync(d, { recursive: true, force: true })
})
const V = (seed: number) =>
Array.from({ length: 384 }, (_, i) => Math.sin((seed + 1) * 7919 + i * 131) * 0.5 + 0.5)
/**
* Build a store with N explicit-vector (non-deferred) nouns, flush, close.
* Each noun also carries embeddable text (`technology`/`science`, matching
* the query used below) so the semantic-search verification exercises real
* retrieval, not a coincidental match. The default JS engine builds a fully
* current store the epoch marker is stamped current at this open's
* completion, so a later reopen's `_indexEpochStale` is honestly false and
* cannot mask the ledger-gap decision under test (nothing here manufactures
* epoch drift).
*/
async function buildVectoredStore(dir: string, n: number): Promise<string[]> {
const brain: any = new Brainy({
requireSubtype: false,
storage: { type: 'filesystem', path: dir },
plugins: [],
silent: true,
dimensions: 384
})
await brain.init()
const ids: string[] = []
for (let i = 0; i < n; i++) {
ids.push(
await brain.add({
data: `doc ${i} about ${i % 2 === 0 ? 'technology' : 'science'}`,
type: 'document',
vector: V(i)
})
)
}
await brain.flush()
await brain.close()
return ids
}
describe('vector-leg open-build (two-engine gate, last red)', () => {
it('coverage gap: a provider reporting serving:true at size()===0 is overridden by the vectored-noun ledger — open() builds, search returns real results', async () => {
const dir = mkTmp()
const ids = await buildVectoredStore(dir, 12)
// Wrap the REAL JS engine so a successful rebuild restores genuine,
// searchable vectors — only `size()` and `healthReport()` are faked,
// simulating a native provider that has never built its own coverage of
// an unledgered invariant.
const calls = { rebuild: 0 }
const brain: any = new Brainy({
requireSubtype: false,
storage: { type: 'filesystem', path: dir },
plugins: [],
silent: true,
dimensions: 384
})
brain.use({
name: 'fake-native-vector-unledgered-coverage',
activate: async (ctx: any) => {
ctx.registerProvider('vector', (config: any, distance: any, options: any) => {
const real = new JsHnswVectorIndex(config, distance, options)
let rebuilt = false
const originalRebuild = real.rebuild.bind(real)
;(real as any).rebuild = async (...args: any[]) => {
const r = await originalRebuild(...args)
calls.rebuild++
rebuilt = true
return r
}
const originalSize = real.size.bind(real)
;(real as any).size = () => (rebuilt ? originalSize() : 0)
;(real as any).healthReport = () => ({
provider: 'vector',
healthy: true,
serving: true,
invariants: [],
checkedAt: Date.now(),
durationMs: 0,
generation: 1,
unledgered: ['vector-coverage']
})
return real
})
return true
}
})
await brain.init()
// WITHOUT any find() first: open() itself must have built the leg.
expect(calls.rebuild, 'open() forced the rebuild despite serving:true').toBe(1)
const status = await brain.getIndexStatus()
expect(status.hnswIndex.size).toBeGreaterThanOrEqual(ids.length)
// Real, searchable results — never [] (see the module doc's SEARCH
// VERIFICATION NOTE for why this is a semantic `query`, not a raw
// `vector`, call).
const results = await brain.find({ query: 'technology document', limit: 5 })
expect(results.length).toBeGreaterThan(0)
expect(results.length).not.toBe(0)
await brain.close()
})
it('the inverse: only deferred (never-landed) user nouns — the ledger is never inflated by them, and search over them honestly returns []', async () => {
// ARCHITECTURAL NOTE (found while building this pin): every brainy store
// carries ONE permanent, always-vectored noun beyond user data — the VFS
// root (`entities/nouns/.../00000000-0000-0000-0000-000000000000`,
// src/vfs/VirtualFileSystem.ts). It is inserted with an explicit all-zero
// (but non-empty, length-384) vector on EVERY store's first open — never
// deferred (a deliberate WASM-cold-compile-avoidance fix, see that
// file's comment) — and VFS init unconditionally re-creates it if
// missing, before the rebuild gate ever runs. A literal "0 vectored
// nouns" store is therefore unreachable through the public API; a
// brand-new store's `vectors.all` floor is 1, not 0. This pin verifies
// the law the task names in the ACHIEVABLE form: nouns whose embed is
// still deferred/unlanded contribute NOTHING to the vectored-noun ledger
// — the coverage-gap comparison sees exactly the root (1), never
// root+deferred — and semantic search over deferred-only user content
// honestly returns `[]` (no error, no false "coverage restored" claim).
const dir = mkTmp()
const build: any = new Brainy({
requireSubtype: false,
storage: { type: 'filesystem', path: dir },
plugins: [],
silent: true,
dimensions: 384
})
await build.init()
const rootOnlyLedger = await build.storage.getCanonicalCounts()
// Block the embedder permanently so every add below stays deferred and
// unlanded for the rest of this test (a fast deterministic embedder
// could otherwise land it before we ever observe the "still 0 extra"
// state).
vi.spyOn(build, 'embed').mockImplementation(() => new Promise(() => {}))
for (let i = 0; i < 5; i++) {
await build.add({ data: `deferred ${i}`, type: 'document', deferEmbedding: true })
}
await build.flush()
const ledgerWithDeferred = await build.storage.getCanonicalCounts()
// The five deferred adds contributed ZERO to the vectored-noun ledger.
expect(ledgerWithDeferred.vectors.all).toBe(rootOnlyLedger.vectors.all)
await build.close()
// Reopen (default JS engine — no stub needed): the root is the ONLY
// thing the vector leg has to load; the deferred nouns are correctly
// invisible to it. Block the embedder again BEFORE init() — reopen
// recovers the durable pending-embed markers and kicks the worker as
// part of init() itself, and an unblocked deterministic embedder could
// land all five before this test observes the open-time ledger.
const brain: any = new Brainy({
requireSubtype: false,
storage: { type: 'filesystem', path: dir },
plugins: [],
silent: true,
dimensions: 384
})
vi.spyOn(brain, 'embed').mockImplementation(() => new Promise(() => {}))
await brain.init()
// The ledger is exactly the root — the five deferred, still-unlanded
// nouns (which the rebuild above DOES insert into the graph, each with
// its stub empty vector — `index.size()` counts EVERY canonical noun's
// graph node, deferred or not, so it is not the coverage metric) never
// inflate the VECTORED count.
const ledgerAfterReopen = await brain.storage.getCanonicalCounts()
expect(ledgerAfterReopen.vectors.all).toBe(rootOnlyLedger.vectors.all)
const results = await brain.find({ vector: V(3), limit: 5 })
expect(results).toEqual([])
await brain.close()
})
})