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

@ -15,6 +15,7 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { GraphAdjacencyIndex } from '../../src/graph/graphAdjacencyIndex.js'
import { EntityIdMapper } from '../../src/utils/entityIdMapper.js'
import { MemoryStorage } from '../../src/storage/adapters/memoryStorage.js'
import { performance } from 'perf_hooks'
@ -112,8 +113,18 @@ describe('🧠 Graph Scale Performance Benchmarks', () => {
let brain: Brainy
let graphIndex: GraphAdjacencyIndex
let storage: MemoryStorage
let idMapper: EntityIdMapper
const scale = getTestScale()
/** Resolve a UUID to its entity int for the 8.0 BigInt boundary. */
const entityInt = (uuid: string): bigint => BigInt(idMapper.getOrAssign(uuid))
/** Map returned entity ints back to UUIDs. */
const intsToUuids = (ints: bigint[]): string[] =>
ints
.map((i) => idMapper.getUuid(Number(i)))
.filter((u): u is string => u !== undefined)
// Performance tracking
const lookupStats = new PerformanceStats()
const updateStats = new PerformanceStats()
@ -130,10 +141,13 @@ describe('🧠 Graph Scale Performance Benchmarks', () => {
storage = new MemoryStorage()
await storage.init()
idMapper = new EntityIdMapper({ storage })
await idMapper.init()
graphIndex = new GraphAdjacencyIndex(storage, {
maxIndexSize: scale.nodes,
autoOptimize: true
})
}, idMapper)
// Initialize Brainy for unified testing
brain = new Brainy({ requireSubtype: false,
@ -226,7 +240,7 @@ describe('🧠 Graph Scale Performance Benchmarks', () => {
// Warm up the index
await graphIndex.rebuild()
await graphIndex.getNeighbors('node-100') // Warm up
await graphIndex.getNeighbors(entityInt('node-100')) // Warm up
// Test random lookups
const testIterations = Math.min(1000, scale.nodes / 10)
@ -236,7 +250,7 @@ describe('🧠 Graph Scale Performance Benchmarks', () => {
for (const nodeId of sampleNodes) {
const startTime = performance.now()
const neighbors = await graphIndex.getNeighbors(nodeId)
const neighbors = await graphIndex.getNeighbors(entityInt(nodeId))
const elapsed = performance.now() - startTime
lookupStats.addSample(elapsed)
@ -408,9 +422,9 @@ describe('🧠 Graph Scale Performance Benchmarks', () => {
visited.add(id)
nodesTraversed++
// Get neighbors
const neighbors = await graphIndex.getNeighbors(id, 'out')
for (const neighbor of neighbors) {
// Get neighbors (BigInt boundary: ints out, mapped back to UUIDs)
const neighborInts = await graphIndex.getNeighbors(entityInt(id), { direction: 'out' })
for (const neighbor of intsToUuids(neighborInts)) {
if (!visited.has(neighbor)) {
queue.push({ id: neighbor, depth: depth + 1 })
}
@ -687,7 +701,7 @@ describe('🧠 Graph Scale Performance Benchmarks', () => {
switch (operationType) {
case 0: // Neighbor lookup
operations.push(graphIndex.getNeighbors(`node-${Math.floor(Math.random() * scale.nodes)}`))
operations.push(graphIndex.getNeighbors(entityInt(`node-${Math.floor(Math.random() * scale.nodes)}`)))
break
case 1: // Unified find
operations.push(brain.find({