feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h

GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
  return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
  identity-fingerprint design — verb ids are UUIDs by contract, so the
  provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
  removeVerb(verbId) joins the contract

Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.

JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].

relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.

Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
This commit is contained in:
David Snelling 2026-06-10 10:45:45 -07:00
parent 62f6472fa0
commit 2427bb7960
17 changed files with 1164 additions and 328 deletions

View file

@ -44,7 +44,7 @@ describe('ColumnStore — JS↔native interchange (2.4.0 #4)', () => {
it('flushBuffer writes segment bytes as a raw blob at the cortex-shared key', async () => {
for (let i = 0; i < 5; i++) {
store.addEntity(idMapper.getOrAssign(`u-${i}`), { score: i * 10 })
store.addEntity(BigInt(idMapper.getOrAssign(`u-${i}`)), { score: i * 10 })
}
await store.flush()
@ -63,9 +63,9 @@ describe('ColumnStore — JS↔native interchange (2.4.0 #4)', () => {
it('flushBuffer writes the DELETED bitmap as a raw blob too (not an envelope)', async () => {
for (let i = 0; i < 3; i++) {
store.addEntity(idMapper.getOrAssign(`u-${i}`), { score: i })
store.addEntity(BigInt(idMapper.getOrAssign(`u-${i}`)), { score: i })
}
store.removeEntity(idMapper.getInt('u-1')!)
store.removeEntity(BigInt(idMapper.getInt('u-1')!))
await store.flush()
const rawDel = await (storage as any).loadBinaryBlob('_column_index/score/DELETED')
@ -135,7 +135,7 @@ describe('ColumnStore — JS↔native interchange (2.4.0 #4)', () => {
const readStore = new ColumnStore()
await readStore.init(storage, idMapper)
const sortedInts = await readStore.sortTopK('score', 'asc', 10)
const sortedUuids = sortedInts.map(i => idMapper.getUuid(i))
const sortedUuids = sortedInts.map(i => idMapper.getUuid(Number(i)))
expect(sortedUuids).toEqual(['legacy-a', 'legacy-b', 'legacy-c'])
await readStore.close()
})
@ -153,15 +153,15 @@ describe('ColumnStore — JS↔native interchange (2.4.0 #4)', () => {
// Add a couple of live entities so a manifest exists (init only loads
// DELETED for fields with a manifest on disk).
store.addEntity(idMapper.getOrAssign('alive-1'), { score: 100 })
store.addEntity(idMapper.getOrAssign('alive-2'), { score: 200 })
store.addEntity(BigInt(idMapper.getOrAssign('alive-1')), { score: 100 })
store.addEntity(BigInt(idMapper.getOrAssign('alive-2')), { score: 200 })
await store.flush()
await store.close()
const readStore = new ColumnStore()
await readStore.init(storage, idMapper)
const sortedInts = await readStore.sortTopK('score', 'asc', 10)
const sortedUuids = sortedInts.map(i => idMapper.getUuid(i))
const sortedUuids = sortedInts.map(i => idMapper.getUuid(Number(i)))
// The dead entity must NOT appear; the alive ones must.
expect(sortedUuids).not.toContain('dead')
@ -176,7 +176,7 @@ describe('ColumnStore — JS↔native interchange (2.4.0 #4)', () => {
it('write (new format) → re-init → read returns the same data', async () => {
for (let i = 0; i < 5; i++) {
store.addEntity(idMapper.getOrAssign(`u-${i}`), { score: i * 10 })
store.addEntity(BigInt(idMapper.getOrAssign(`u-${i}`)), { score: i * 10 })
}
await store.flush()
await store.close()
@ -184,7 +184,7 @@ describe('ColumnStore — JS↔native interchange (2.4.0 #4)', () => {
const readStore = new ColumnStore()
await readStore.init(storage, idMapper)
const sortedInts = await readStore.sortTopK('score', 'asc', 10)
const sortedUuids = sortedInts.map(i => idMapper.getUuid(i))
const sortedUuids = sortedInts.map(i => idMapper.getUuid(Number(i)))
expect(sortedUuids).toEqual(['u-0', 'u-1', 'u-2', 'u-3', 'u-4'])
await readStore.close()
})