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({

View file

@ -151,8 +151,12 @@ describe('Duplicate Check Optimization', () => {
type: VerbType.RelatesTo
})
// Access GraphIndex to ensure verb is cached
const verbIds = await (brain as any).graphIndex.getVerbIdsBySource(entityA)
// Access GraphIndex to ensure verb is cached (8.0 BigInt boundary:
// UUID → entity int in, verb ints out, resolved back via verbIntsToIds)
const idMapper = (brain as any).metadataIndex.getIdMapper()
const sourceInt = BigInt(idMapper.getInt(entityA)!)
const verbInts: bigint[] = await (brain as any).graphIndex.getVerbIdsBySource(sourceInt)
const verbIds = await (brain as any).graphIndex.verbIntsToIds(verbInts)
expect(verbIds).toContain(relationId1)
// Attempt duplicate (should use cached verb)

View file

@ -0,0 +1,230 @@
/**
* @module bigint-contract.test
* @description 8.0 u64 provider contract BigInt boundary of the JS
* GraphAdjacencyIndex and the coordinator that drives it.
*
* Covers:
* - bigint round-trip through the JS graph index: `addVerb` returns a stable
* verb int, `getVerbIdsBySource`/`getVerbIdsByTarget` return that int, and
* `verbIntsToIds` maps it back to the verb-id string (null for unknown ints)
* - `getNeighbors` speaks entity ints both ways via the shared idMapper
* - unmapped entity ints / UUIDs produce empty reads, never errors
* - a missing idMapper fails loudly (wiring bug, not a silent fallback)
* - coordinator-level behavior: relate getRelations/neighbors unrelate
* works end-to-end over the BigInt boundary (public API unchanged)
*/
import { describe, it, expect, beforeEach, afterEach } 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 { NounType, VerbType } from '../../../src/types/graphTypes.js'
import type { GraphVerb } from '../../../src/coreTypes.js'
// Verb ids are UUID-shaped by contract in 8.0 (storage enforces the shape on
// every save; cortex keys verb-int interning on the raw UUID bytes).
const VERB_UUID_1 = '11111111-1111-4111-8111-111111111111'
const VERB_UUID_2 = '22222222-2222-4222-8222-222222222222'
const VERB_UUID_3 = '33333333-3333-4333-8333-333333333333'
/** Build a minimal GraphVerb for direct index-level tests. */
function makeVerb(id: string, sourceId: string, targetId: string): GraphVerb {
return {
id,
sourceId,
targetId,
vector: [],
type: VerbType.RelatedTo,
verb: VerbType.RelatedTo
}
}
describe('GraphAdjacencyIndex — BigInt boundary (JS implementation)', () => {
let storage: MemoryStorage
let idMapper: EntityIdMapper
let index: GraphAdjacencyIndex
beforeEach(async () => {
storage = new MemoryStorage()
await storage.init()
idMapper = new EntityIdMapper({ storage, storageKey: 'test:graph:idMapper' })
await idMapper.init()
index = new GraphAdjacencyIndex(storage, {}, idMapper)
})
afterEach(async () => {
await index.close()
})
it('addVerb returns a stable verb int; reads return it; verbIntsToIds maps back', async () => {
const sourceInt = BigInt(idMapper.getOrAssign('uuid-source'))
const targetInt = BigInt(idMapper.getOrAssign('uuid-target'))
const verb = makeVerb(VERB_UUID_1, 'uuid-source', 'uuid-target')
verb.sourceInt = sourceInt
verb.targetInt = targetInt
const verbInt = await index.addVerb(verb, sourceInt, targetInt)
expect(typeof verbInt).toBe('bigint')
// Re-adding the same verb id returns the SAME interned int (stable).
const verbIntAgain = await index.addVerb(verb, sourceInt, targetInt)
expect(verbIntAgain).toBe(verbInt)
// Both directed verb-int reads surface the int from addVerb.
const bySource = await index.getVerbIdsBySource(sourceInt)
expect(bySource).toContain(verbInt)
const byTarget = await index.getVerbIdsByTarget(targetInt)
expect(byTarget).toContain(verbInt)
// Batch reverse resolution is order-preserving with null for unknowns.
const resolved = await index.verbIntsToIds([verbInt, 999_999n])
expect(resolved).toEqual([VERB_UUID_1, null])
})
it('getNeighbors speaks entity ints in both directions', async () => {
const aInt = BigInt(idMapper.getOrAssign('uuid-a'))
const bInt = BigInt(idMapper.getOrAssign('uuid-b'))
const verb = makeVerb(VERB_UUID_2, 'uuid-a', 'uuid-b')
await index.addVerb(verb, aInt, bInt)
const out = await index.getNeighbors(aInt, { direction: 'out' })
expect(out).toEqual([bInt])
const incoming = await index.getNeighbors(bInt, { direction: 'in' })
expect(incoming).toEqual([aInt])
const both = await index.getNeighbors(aInt)
expect(both).toContain(bInt)
})
it('reads for unmapped entity ints return empty without error', async () => {
// 424242 was never assigned by the mapper — no UUID behind it.
const ghost = 424_242n
expect(await index.getNeighbors(ghost)).toEqual([])
expect(await index.getVerbIdsBySource(ghost)).toEqual([])
expect(await index.getVerbIdsByTarget(ghost)).toEqual([])
})
it('verb ints stay resolvable after removeVerb (interning is append-only)', async () => {
const sInt = BigInt(idMapper.getOrAssign('uuid-s'))
const tInt = BigInt(idMapper.getOrAssign('uuid-t'))
const verb = makeVerb(VERB_UUID_3, 'uuid-s', 'uuid-t')
const verbInt = await index.addVerb(verb, sInt, tInt)
// Persist the verb (vector + metadata — getVerb requires both) so
// removeVerb can resolve its type for count updates.
await storage.saveVerb({
id: verb.id,
vector: [],
connections: new Map<number, Set<string>>(),
verb: VerbType.RelatedTo,
sourceId: verb.sourceId,
targetId: verb.targetId
})
await storage.saveVerbMetadata(verb.id, {
verb: VerbType.RelatedTo,
createdAt: Date.now()
})
await index.removeVerb(verb.id)
// Removed verb no longer appears in reads…
expect(await index.getVerbIdsBySource(sInt)).toEqual([])
// …but its int still reverse-resolves (append-only interning).
expect(await index.verbIntsToIds([verbInt])).toEqual([VERB_UUID_3])
})
it('fails loudly when the entityIdMapper is not wired', async () => {
const bare = new GraphAdjacencyIndex(storage)
await expect(bare.getNeighbors(1n)).rejects.toThrow(/entityIdMapper not wired/)
await bare.close()
})
})
describe('Coordinator — relate/related end-to-end over the BigInt boundary', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
await brain.init()
})
afterEach(async () => {
await brain.close()
})
it('relate → getRelations → neighbors round-trips (public API unchanged)', async () => {
const personId = await brain.add({
data: { name: 'Ada' },
type: NounType.Person
})
const projectId = await brain.add({
data: { name: 'Analytical Engine' },
type: NounType.Thing
})
const relId = await brain.relate({
from: personId,
to: projectId,
type: VerbType.WorksWith
})
// getRelations resolves verb ints back to verb-id strings internally.
const relations = await brain.getRelations({ from: personId })
expect(relations).toHaveLength(1)
expect(relations[0].id).toBe(relId)
expect(relations[0].to).toBe(projectId)
// neighbors() routes through getNeighbors(bigint) + int → UUID mapping.
const out = await brain.neighbors(personId, { direction: 'outgoing' })
expect(out).toContain(projectId)
const incoming = await brain.neighbors(projectId, { direction: 'incoming' })
expect(incoming).toContain(personId)
})
it('relate feeds the verb-int warm cache; duplicate detection still works', async () => {
const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing })
const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing })
const first = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
// The AddToGraphIndexOperation callback recorded the verb int → id pair.
const warmCache: Map<bigint, string> = (brain as any).verbIntWarmCache
expect([...warmCache.values()]).toContain(first)
// Duplicate check runs through getVerbIdsBySource(bigint) + resolution.
const duplicate = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
expect(duplicate).toBe(first)
})
it('unrelate removes the edge from BigInt-boundary reads', async () => {
const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing })
const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing })
const relId = await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
await brain.unrelate(relId)
const relations = await brain.getRelations({ from: a })
expect(relations).toHaveLength(0)
})
it('neighbors() of an unknown UUID returns empty without error', async () => {
const result = await brain.neighbors('00000000-0000-4000-8000-000000000000')
expect(result).toEqual([])
})
it('relate() rejects a caller-supplied verb id with a teaching error (8.0 contract)', async () => {
const a = await brain.add({ data: { name: 'A' }, type: NounType.Thing })
const b = await brain.add({ data: { name: 'B' }, type: NounType.Thing })
// RelateParams has no `id` field — untyped callers passing one used to be
// silently ignored. 8.0 teaches: verb ids are brainy-generated UUIDs.
await expect(
brain.relate({ from: a, to: b, type: VerbType.RelatedTo, id: 'my-custom-id' } as any)
).rejects.toThrow(/Verb ids are UUIDs by contract in 8\.0/)
})
})

View file

@ -1,12 +1,14 @@
/**
* GraphAdjacencyIndex Pagination Tests (v5.8.0)
* GraphAdjacencyIndex Pagination Tests (v5.8.0, updated for the 8.0 u64 contract)
*
* Tests for pagination support in GraphIndex methods:
* - getNeighbors() with limit/offset
* - getVerbIdsBySource() with limit/offset
* - getVerbIdsByTarget() with limit/offset
*
* Verifies backward compatibility and new pagination features
* 8.0 BigInt boundary: entity ints in (resolved via the metadata index's
* idMapper), entity/verb ints out (`bigint[]`). Entity ints map back to UUIDs
* via `idMapper.getUuid(Number(int))`; verb ints via `verbIntsToIds()`.
*/
import { describe, it, expect, beforeEach } from 'vitest'
@ -18,6 +20,25 @@ describe('GraphAdjacencyIndex Pagination', () => {
let centralId: string
let neighborIds: string[]
/** The brain's graph index (BigInt provider contract). */
const graphIndex = () => (brain as any).graphIndex
/** The shared UUID ↔ int resolver. */
const idMapper = () => (brain as any).metadataIndex.getIdMapper()
/** Resolve a UUID to its entity int (must already be mapped). */
const entityInt = (uuid: string): bigint => {
const intId = idMapper().getInt(uuid)
expect(intId).toBeDefined()
return BigInt(intId!)
}
/** Map returned entity ints back to UUIDs. */
const intsToUuids = (ints: bigint[]): string[] =>
ints
.map((i) => idMapper().getUuid(Number(i)))
.filter((u: string | undefined): u is string => u !== undefined)
beforeEach(async () => {
brain = new Brainy({ requireSubtype: false })
await brain.init()
@ -47,50 +68,42 @@ describe('GraphAdjacencyIndex Pagination', () => {
})
describe('getNeighbors() Pagination', () => {
it('should return all neighbors without pagination (backward compatible)', async () => {
const graphIndex = (brain as any).graphIndex
const neighbors = await graphIndex.getNeighbors(centralId)
it('should return all neighbors without pagination', async () => {
const neighborInts = await graphIndex().getNeighbors(entityInt(centralId))
const neighbors = intsToUuids(neighborInts)
// Should return all 50 neighbors
expect(neighbors).toHaveLength(50)
expect(neighbors.every((id: string) => neighborIds.includes(id))).toBe(true)
})
it('should return all outgoing neighbors with direction parameter (backward compatible)', async () => {
const graphIndex = (brain as any).graphIndex
// Old API: direction as string
const neighbors = await graphIndex.getNeighbors(centralId, 'out')
it('should return all outgoing neighbors with direction option', async () => {
const neighbors = await graphIndex().getNeighbors(entityInt(centralId), { direction: 'out' })
expect(neighbors).toHaveLength(50)
})
it('should paginate with limit only', async () => {
const graphIndex = (brain as any).graphIndex
const page1 = await graphIndex.getNeighbors(centralId, { limit: 10 })
const page1Ints = await graphIndex().getNeighbors(entityInt(centralId), { limit: 10 })
const page1 = intsToUuids(page1Ints)
expect(page1).toHaveLength(10)
expect(page1.every((id: string) => neighborIds.includes(id))).toBe(true)
})
it('should paginate with offset only', async () => {
const graphIndex = (brain as any).graphIndex
const all = await graphIndex.getNeighbors(centralId)
const offsetResults = await graphIndex.getNeighbors(centralId, { offset: 10 })
const all = await graphIndex().getNeighbors(entityInt(centralId))
const offsetResults = await graphIndex().getNeighbors(entityInt(centralId), { offset: 10 })
// Offset 10 should return all after first 10
expect(offsetResults).toHaveLength(all.length - 10)
})
it('should paginate with both limit and offset', async () => {
const graphIndex = (brain as any).graphIndex
const page1 = await graphIndex.getNeighbors(centralId, { limit: 10, offset: 0 })
const page2 = await graphIndex.getNeighbors(centralId, { limit: 10, offset: 10 })
const page3 = await graphIndex.getNeighbors(centralId, { limit: 10, offset: 20 })
const central = entityInt(centralId)
const page1: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 0 })
const page2: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 10 })
const page3: bigint[] = await graphIndex().getNeighbors(central, { limit: 10, offset: 20 })
// Each page should have 10 results
expect(page1).toHaveLength(10)
@ -98,39 +111,32 @@ describe('GraphAdjacencyIndex Pagination', () => {
expect(page3).toHaveLength(10)
// Pages should not overlap
const page1Set = new Set(page1)
const page2Set = new Set(page2)
const page3Set = new Set(page3)
const overlap12 = page1.filter(id => page2Set.has(id))
const overlap23 = page2.filter(id => page3Set.has(id))
const overlap12 = page1.filter((id) => page2Set.has(id))
const overlap23 = page2.filter((id) => page3Set.has(id))
expect(overlap12).toHaveLength(0)
expect(overlap23).toHaveLength(0)
})
it('should handle offset beyond total count', async () => {
const graphIndex = (brain as any).graphIndex
const results = await graphIndex.getNeighbors(centralId, { offset: 100 })
const results = await graphIndex().getNeighbors(entityInt(centralId), { offset: 100 })
// Offset 100 > 50 total → empty array
expect(results).toHaveLength(0)
})
it('should handle limit = 0', async () => {
const graphIndex = (brain as any).graphIndex
const results = await graphIndex.getNeighbors(centralId, { limit: 0 })
const results = await graphIndex().getNeighbors(entityInt(centralId), { limit: 0 })
expect(results).toHaveLength(0)
})
it('should paginate with direction filter', async () => {
const graphIndex = (brain as any).graphIndex
// Get first 10 outgoing neighbors
const page1 = await graphIndex.getNeighbors(centralId, {
const page1 = await graphIndex().getNeighbors(entityInt(centralId), {
direction: 'out',
limit: 10,
offset: 0
@ -140,8 +146,6 @@ describe('GraphAdjacencyIndex Pagination', () => {
})
it('should handle pagination with incoming direction', async () => {
const graphIndex = (brain as any).graphIndex
// Create some incoming relationships
const sourceId = await brain.add({
data: { name: 'Source' },
@ -155,7 +159,7 @@ describe('GraphAdjacencyIndex Pagination', () => {
})
// Get incoming neighbors with pagination
const incoming = await graphIndex.getNeighbors(centralId, {
const incoming = await graphIndex().getNeighbors(entityInt(centralId), {
direction: 'in',
limit: 10
})
@ -165,38 +169,36 @@ describe('GraphAdjacencyIndex Pagination', () => {
})
describe('getVerbIdsBySource() Pagination', () => {
it('should return all verb IDs without pagination (backward compatible)', async () => {
const graphIndex = (brain as any).graphIndex
it('should return all verb ints without pagination and resolve them back to ids', async () => {
const verbInts: bigint[] = await graphIndex().getVerbIdsBySource(entityInt(centralId))
const verbIds = await graphIndex.getVerbIdsBySource(centralId)
// Should return all 50 verb ints
expect(verbInts).toHaveLength(50)
// Should return all 50 verb IDs
// Every int must resolve back to a verb-id string
const verbIds: (string | null)[] = await graphIndex().verbIntsToIds(verbInts)
expect(verbIds).toHaveLength(50)
expect(verbIds.every((id) => typeof id === 'string')).toBe(true)
})
it('should paginate verb IDs with limit', async () => {
const graphIndex = (brain as any).graphIndex
const page1 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10 })
it('should paginate verb ints with limit', async () => {
const page1 = await graphIndex().getVerbIdsBySource(entityInt(centralId), { limit: 10 })
expect(page1).toHaveLength(10)
})
it('should paginate verb IDs with offset', async () => {
const graphIndex = (brain as any).graphIndex
const all = await graphIndex.getVerbIdsBySource(centralId)
const offsetResults = await graphIndex.getVerbIdsBySource(centralId, { offset: 10 })
it('should paginate verb ints with offset', async () => {
const all = await graphIndex().getVerbIdsBySource(entityInt(centralId))
const offsetResults = await graphIndex().getVerbIdsBySource(entityInt(centralId), { offset: 10 })
expect(offsetResults).toHaveLength(all.length - 10)
})
it('should paginate verb IDs with limit and offset', async () => {
const graphIndex = (brain as any).graphIndex
const page1 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10, offset: 0 })
const page2 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10, offset: 10 })
const page3 = await graphIndex.getVerbIdsBySource(centralId, { limit: 10, offset: 20 })
it('should paginate verb ints with limit and offset', async () => {
const central = entityInt(centralId)
const page1: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 0 })
const page2: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 10 })
const page3: bigint[] = await graphIndex().getVerbIdsBySource(central, { limit: 10, offset: 20 })
// Each page should have 10 results
expect(page1).toHaveLength(10)
@ -209,34 +211,28 @@ describe('GraphAdjacencyIndex Pagination', () => {
expect(unique.size).toBe(30) // All unique
})
it('should handle pagination edge cases for verb IDs', async () => {
const graphIndex = (brain as any).graphIndex
it('should handle pagination edge cases for verb ints', async () => {
// Offset beyond total
const beyondTotal = await graphIndex.getVerbIdsBySource(centralId, { offset: 100 })
const beyondTotal = await graphIndex().getVerbIdsBySource(entityInt(centralId), { offset: 100 })
expect(beyondTotal).toHaveLength(0)
// Limit = 0
const zeroLimit = await graphIndex.getVerbIdsBySource(centralId, { limit: 0 })
const zeroLimit = await graphIndex().getVerbIdsBySource(entityInt(centralId), { limit: 0 })
expect(zeroLimit).toHaveLength(0)
})
})
describe('getVerbIdsByTarget() Pagination', () => {
it('should return all verb IDs targeting an entity (backward compatible)', async () => {
const graphIndex = (brain as any).graphIndex
it('should return all verb ints targeting an entity', async () => {
// Pick a neighbor that's a target of relationships
const targetId = neighborIds[0]
const verbIds = await graphIndex.getVerbIdsByTarget(targetId)
const verbInts = await graphIndex().getVerbIdsByTarget(entityInt(targetId))
// Should have at least 1 (the relationship from central)
expect(verbIds.length).toBeGreaterThanOrEqual(1)
expect(verbInts.length).toBeGreaterThanOrEqual(1)
})
it('should paginate verb IDs by target with limit', async () => {
const graphIndex = (brain as any).graphIndex
it('should paginate verb ints by target with limit', async () => {
// Create entity with many incoming relationships
const popularTarget = await brain.add({
data: { name: 'Popular Target' },
@ -257,28 +253,29 @@ describe('GraphAdjacencyIndex Pagination', () => {
}
// Paginate
const page1 = await graphIndex.getVerbIdsByTarget(popularTarget, { limit: 10 })
const page2 = await graphIndex.getVerbIdsByTarget(popularTarget, { limit: 10, offset: 10 })
const target = entityInt(popularTarget)
const page1: bigint[] = await graphIndex().getVerbIdsByTarget(target, { limit: 10 })
const page2: bigint[] = await graphIndex().getVerbIdsByTarget(target, { limit: 10, offset: 10 })
expect(page1).toHaveLength(10)
expect(page2).toHaveLength(10)
// No overlap
const overlap = page1.filter((id: string) => page2.includes(id))
const overlap = page1.filter((id) => page2.includes(id))
expect(overlap).toHaveLength(0)
})
})
describe('Performance with Pagination', () => {
it('should maintain sub-5ms performance with pagination', async () => {
const graphIndex = (brain as any).graphIndex
const central = entityInt(centralId)
// Multiple paginated queries should be fast
const startTime = performance.now()
await graphIndex.getNeighbors(centralId, { limit: 10, offset: 0 })
await graphIndex.getNeighbors(centralId, { limit: 10, offset: 10 })
await graphIndex.getNeighbors(centralId, { limit: 10, offset: 20 })
await graphIndex().getNeighbors(central, { limit: 10, offset: 0 })
await graphIndex().getNeighbors(central, { limit: 10, offset: 10 })
await graphIndex().getNeighbors(central, { limit: 10, offset: 20 })
const elapsed = performance.now() - startTime
@ -310,12 +307,11 @@ describe('GraphAdjacencyIndex Pagination', () => {
})
}
const graphIndex = (brain as any).graphIndex
// Paginate in chunks of 25
const chunks: string[][] = []
const hubInt = entityInt(hub)
const chunks: bigint[][] = []
for (let offset = 0; offset < 100; offset += 25) {
const chunk = await graphIndex.getNeighbors(hub, {
const chunk = await graphIndex().getNeighbors(hubInt, {
direction: 'out',
limit: 25,
offset

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()
})

View file

@ -35,48 +35,48 @@ describe('ColumnStore', () => {
describe('sortTopK', () => {
it('returns entities sorted by numeric field ascending', async () => {
store.addEntity(idMapper.getOrAssign('a'), { createdAt: 300 })
store.addEntity(idMapper.getOrAssign('b'), { createdAt: 100 })
store.addEntity(idMapper.getOrAssign('c'), { createdAt: 200 })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 300 })
store.addEntity(BigInt(idMapper.getOrAssign('b')), { createdAt: 100 })
store.addEntity(BigInt(idMapper.getOrAssign('c')), { createdAt: 200 })
await store.flush()
const result = await store.sortTopK('createdAt', 'asc', 10)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['b', 'c', 'a'])
})
it('returns entities sorted by numeric field descending', async () => {
store.addEntity(idMapper.getOrAssign('a'), { createdAt: 300 })
store.addEntity(idMapper.getOrAssign('b'), { createdAt: 100 })
store.addEntity(idMapper.getOrAssign('c'), { createdAt: 200 })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 300 })
store.addEntity(BigInt(idMapper.getOrAssign('b')), { createdAt: 100 })
store.addEntity(BigInt(idMapper.getOrAssign('c')), { createdAt: 200 })
await store.flush()
const result = await store.sortTopK('createdAt', 'desc', 10)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['a', 'c', 'b'])
})
it('respects limit K', async () => {
for (let i = 0; i < 50; i++) {
store.addEntity(idMapper.getOrAssign(`e${i}`), { createdAt: i * 1000 })
store.addEntity(BigInt(idMapper.getOrAssign(`e${i}`)), { createdAt: i * 1000 })
}
await store.flush()
const result = await store.sortTopK('createdAt', 'desc', 5)
expect(result).toHaveLength(5)
// Should be the 5 highest timestamps
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['e49', 'e48', 'e47', 'e46', 'e45'])
})
it('sorts string fields lexicographically', async () => {
store.addEntity(idMapper.getOrAssign('a'), { status: 'pending' })
store.addEntity(idMapper.getOrAssign('b'), { status: 'active' })
store.addEntity(idMapper.getOrAssign('c'), { status: 'inactive' })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { status: 'pending' })
store.addEntity(BigInt(idMapper.getOrAssign('b')), { status: 'active' })
store.addEntity(BigInt(idMapper.getOrAssign('c')), { status: 'inactive' })
await store.flush()
const result = await store.sortTopK('status', 'asc', 10)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['b', 'c', 'a']) // active, inactive, pending
})
@ -97,16 +97,16 @@ describe('ColumnStore', () => {
const idC = idMapper.getOrAssign('c')
const idD = idMapper.getOrAssign('d')
store.addEntity(idA, { createdAt: 400 })
store.addEntity(idB, { createdAt: 100 })
store.addEntity(idC, { createdAt: 300 })
store.addEntity(idD, { createdAt: 200 })
store.addEntity(BigInt(idA), { createdAt: 400 })
store.addEntity(BigInt(idB), { createdAt: 100 })
store.addEntity(BigInt(idC), { createdAt: 300 })
store.addEntity(BigInt(idD), { createdAt: 200 })
await store.flush()
// Filter: only B and C
const bitmap = new RoaringBitmap32([idB, idC])
const result = await store.filteredSortTopK(bitmap, 'createdAt', 'desc', 10)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['c', 'b']) // 300, 100
})
})
@ -121,9 +121,9 @@ describe('ColumnStore', () => {
const idB = idMapper.getOrAssign('b')
const idC = idMapper.getOrAssign('c')
store.addEntity(idA, { status: 'active' })
store.addEntity(idB, { status: 'inactive' })
store.addEntity(idC, { status: 'active' })
store.addEntity(BigInt(idA), { status: 'active' })
store.addEntity(BigInt(idB), { status: 'inactive' })
store.addEntity(BigInt(idC), { status: 'active' })
await store.flush()
const bitmap = await store.filter('status', 'active')
@ -133,7 +133,7 @@ describe('ColumnStore', () => {
})
it('returns empty bitmap for no match', async () => {
store.addEntity(idMapper.getOrAssign('a'), { status: 'active' })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { status: 'active' })
await store.flush()
const bitmap = await store.filter('status', 'deleted')
@ -150,7 +150,7 @@ describe('ColumnStore', () => {
const ids: number[] = []
for (let i = 0; i < 10; i++) {
ids.push(idMapper.getOrAssign(`e${i}`))
store.addEntity(ids[i], { price: i * 10 })
store.addEntity(BigInt(ids[i]), { price: i * 10 })
}
await store.flush()
@ -174,16 +174,16 @@ describe('ColumnStore', () => {
const idB = idMapper.getOrAssign('b')
const idC = idMapper.getOrAssign('c')
store.addEntity(idA, { createdAt: 100 })
store.addEntity(idB, { createdAt: 200 })
store.addEntity(idC, { createdAt: 300 })
store.addEntity(BigInt(idA), { createdAt: 100 })
store.addEntity(BigInt(idB), { createdAt: 200 })
store.addEntity(BigInt(idC), { createdAt: 300 })
await store.flush()
store.removeEntity(idB)
store.removeEntity(BigInt(idB))
await store.flush()
const result = await store.sortTopK('createdAt', 'asc', 10)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['a', 'c'])
})
})
@ -196,12 +196,12 @@ describe('ColumnStore', () => {
it('sorts correctly across multiple L0 segments', async () => {
// Flush threshold is 10, so 25 entities creates 3 segments
for (let i = 0; i < 25; i++) {
store.addEntity(idMapper.getOrAssign(`e${i}`), { ts: i })
store.addEntity(BigInt(idMapper.getOrAssign(`e${i}`)), { ts: i })
}
await store.flush()
const result = await store.sortTopK('ts', 'desc', 5)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['e24', 'e23', 'e22', 'e21', 'e20'])
})
})
@ -212,9 +212,9 @@ describe('ColumnStore', () => {
describe('persistence', () => {
it('survives close + reload', async () => {
store.addEntity(idMapper.getOrAssign('a'), { createdAt: 300 })
store.addEntity(idMapper.getOrAssign('b'), { createdAt: 100 })
store.addEntity(idMapper.getOrAssign('c'), { createdAt: 200 })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 300 })
store.addEntity(BigInt(idMapper.getOrAssign('b')), { createdAt: 100 })
store.addEntity(BigInt(idMapper.getOrAssign('c')), { createdAt: 200 })
await store.flush()
await store.close()
@ -223,7 +223,7 @@ describe('ColumnStore', () => {
await store2.init(storage, idMapper)
const result = await store2.sortTopK('createdAt', 'desc', 10)
const uuids = result.map(id => idMapper.getUuid(id))
const uuids = result.map(id => idMapper.getUuid(Number(id)))
expect(uuids).toEqual(['a', 'c', 'b'])
await store2.close()
@ -239,8 +239,8 @@ describe('ColumnStore', () => {
const idA = idMapper.getOrAssign('docA')
const idB = idMapper.getOrAssign('docB')
store.addEntity(idA, { __words__: ['machine', 'learning', 'algorithm'] })
store.addEntity(idB, { __words__: ['neural', 'network', 'machine'] })
store.addEntity(BigInt(idA), { __words__: ['machine', 'learning', 'algorithm'] })
store.addEntity(BigInt(idB), { __words__: ['neural', 'network', 'machine'] })
await store.flush()
// Point filter: "machine" should match both
@ -267,10 +267,10 @@ describe('ColumnStore', () => {
describe('getFilterValues', () => {
it('returns distinct values sorted', async () => {
store.addEntity(idMapper.getOrAssign('a'), { status: 'pending' })
store.addEntity(idMapper.getOrAssign('b'), { status: 'active' })
store.addEntity(idMapper.getOrAssign('c'), { status: 'active' })
store.addEntity(idMapper.getOrAssign('d'), { status: 'inactive' })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { status: 'pending' })
store.addEntity(BigInt(idMapper.getOrAssign('b')), { status: 'active' })
store.addEntity(BigInt(idMapper.getOrAssign('c')), { status: 'active' })
store.addEntity(BigInt(idMapper.getOrAssign('d')), { status: 'inactive' })
await store.flush()
const values = await store.getFilterValues('status')
@ -288,7 +288,7 @@ describe('ColumnStore', () => {
})
it('returns true after adding data', async () => {
store.addEntity(idMapper.getOrAssign('a'), { createdAt: 100 })
store.addEntity(BigInt(idMapper.getOrAssign('a')), { createdAt: 100 })
expect(store.hasField('createdAt')).toBe(true)
})
})