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:
parent
62f6472fa0
commit
2427bb7960
17 changed files with 1164 additions and 328 deletions
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { GraphAdjacencyIndex } from '../graph/graphAdjacencyIndex.js'
|
||||
import type { GraphEntityIdResolver } from '../graph/graphAdjacencyIndex.js'
|
||||
|
||||
import {
|
||||
GraphVerb,
|
||||
|
|
@ -176,6 +177,12 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
protected isInitialized = false
|
||||
protected graphIndex?: GraphAdjacencyIndex
|
||||
protected graphIndexPromise?: Promise<GraphAdjacencyIndex>
|
||||
/**
|
||||
* Shared UUID ↔ int resolver for the graph index's BigInt boundary.
|
||||
* Wired by Brainy via {@link setGraphEntityIdResolver}; until then the verb
|
||||
* read paths fall back to shard iteration.
|
||||
*/
|
||||
protected graphEntityIdResolver?: GraphEntityIdResolver
|
||||
protected readOnly = false
|
||||
|
||||
// Write-through cache for read-after-write consistency
|
||||
|
|
@ -442,6 +449,26 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
this.graphIndexPromise = Promise.resolve(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Wire the shared UUID ↔ int resolver used at the graph index's
|
||||
* BigInt boundary (8.0 u64 contract). Brainy calls this with
|
||||
* `metadataIndex.getIdMapper()` after the graph index is resolved (init,
|
||||
* fork, and checkout paths). The storage layer needs it to convert UUIDs to
|
||||
* entity ints before `getVerbIdsBySource`/`getVerbIdsByTarget` calls and to
|
||||
* resolve returned verb ints back to verb-id strings. Until it's wired, the
|
||||
* verb read paths fall back to shard iteration (correct, just slower).
|
||||
* @param resolver - The shared entity-id resolver.
|
||||
* @returns Nothing.
|
||||
*/
|
||||
public setGraphEntityIdResolver(resolver: GraphEntityIdResolver): void {
|
||||
this.graphEntityIdResolver = resolver
|
||||
// Thread the resolver into the JS graph index too, when present — native
|
||||
// providers carry their own mapper and don't expose this setter.
|
||||
if (this.graphIndex && typeof (this.graphIndex as GraphAdjacencyIndex).setEntityIdMapper === 'function') {
|
||||
(this.graphIndex as GraphAdjacencyIndex).setEntityIdMapper(resolver)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the storage adapter is initialized
|
||||
*/
|
||||
|
|
@ -2107,7 +2134,9 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
*/
|
||||
private async _initializeGraphIndex(): Promise<GraphAdjacencyIndex> {
|
||||
prodLog.info('Initializing GraphAdjacencyIndex...')
|
||||
this.graphIndex = new GraphAdjacencyIndex(this)
|
||||
// Thread the shared entity-id resolver when already wired (re-init after
|
||||
// invalidateGraphIndex); on first init Brainy wires it right after.
|
||||
this.graphIndex = new GraphAdjacencyIndex(this, {}, this.graphEntityIdResolver)
|
||||
|
||||
// Check if we need to rebuild from existing data
|
||||
const sampleVerbs = await this.getVerbs({ pagination: { limit: 1 } })
|
||||
|
|
@ -3610,10 +3639,19 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
|
||||
prodLog.debug(`[BaseStorage] getVerbsBySource_internal: sourceId=${sourceId}, graphIndex=${!!this.graphIndex}, isInitialized=${this.graphIndex?.isInitialized}`)
|
||||
|
||||
// Fast path - use GraphAdjacencyIndex if available (lazy-loaded)
|
||||
if (this.graphIndex && this.graphIndex.isInitialized) {
|
||||
// Fast path - use GraphAdjacencyIndex if available (lazy-loaded).
|
||||
// 8.0 BigInt boundary: convert the UUID to an entity int up front and
|
||||
// resolve returned verb ints back to verb-id strings.
|
||||
if (this.graphIndex && this.graphIndex.isInitialized && this.graphEntityIdResolver) {
|
||||
try {
|
||||
const verbIds = await this.graphIndex.getVerbIdsBySource(sourceId)
|
||||
const sourceInt = this.graphEntityIdResolver.getInt(sourceId)
|
||||
if (sourceInt === undefined) {
|
||||
// Never-mapped UUID — the entity has no relations by definition.
|
||||
return []
|
||||
}
|
||||
const verbInts = await this.graphIndex.getVerbIdsBySource(BigInt(sourceInt))
|
||||
const verbIds = (await this.graphIndex.verbIntsToIds(verbInts))
|
||||
.filter((id): id is string => id !== null)
|
||||
prodLog.debug(`[BaseStorage] GraphAdjacencyIndex found ${verbIds.length} verb IDs for sourceId=${sourceId}`)
|
||||
|
||||
// PERFORMANCE FIX - Batch fetch verbs + metadata (eliminates N+1 pattern)
|
||||
|
|
@ -3860,10 +3898,19 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
): Promise<HNSWVerbWithMetadata[]> {
|
||||
await this.ensureInitialized()
|
||||
|
||||
// Fast path - use GraphAdjacencyIndex if available (lazy-loaded)
|
||||
if (this.graphIndex && this.graphIndex.isInitialized) {
|
||||
// Fast path - use GraphAdjacencyIndex if available (lazy-loaded).
|
||||
// 8.0 BigInt boundary: convert the UUID to an entity int up front and
|
||||
// resolve returned verb ints back to verb-id strings.
|
||||
if (this.graphIndex && this.graphIndex.isInitialized && this.graphEntityIdResolver) {
|
||||
try {
|
||||
const verbIds = await this.graphIndex.getVerbIdsByTarget(targetId)
|
||||
const targetInt = this.graphEntityIdResolver.getInt(targetId)
|
||||
if (targetInt === undefined) {
|
||||
// Never-mapped UUID — the entity has no relations by definition.
|
||||
return []
|
||||
}
|
||||
const verbInts = await this.graphIndex.getVerbIdsByTarget(BigInt(targetInt))
|
||||
const verbIds = (await this.graphIndex.verbIntsToIds(verbInts))
|
||||
.filter((id): id is string => id !== null)
|
||||
const results: HNSWVerbWithMetadata[] = []
|
||||
|
||||
for (const verbId of verbIds) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue