brainy/src/plugin.ts

461 lines
18 KiB
TypeScript
Raw Normal View History

/**
* Brainy Plugin System
*
* Simple plugin architecture for two use cases:
* 1. Native acceleration (@soulcraft/cortex)
* 2. Custom storage adapters (e.g., Redis, DynamoDB, custom backends)
*
* Plugins are auto-detected by package name or registered manually.
*/
import type {
StorageAdapter,
Vector,
VectorDocument,
GraphVerb,
} from './coreTypes.js'
import type { NounType, VerbType } from './types/graphTypes.js'
import type { MetadataIndexStats } from './utils/metadataIndex.js'
import type { GraphIndexStats } from './graph/graphAdjacencyIndex.js'
// Re-export the provider contracts that already live closer to their
// implementations so a plugin author (Cortex) can import the *entire*
// provider surface from one stable entrypoint: `@soulcraft/brainy/plugin`.
export type { ColumnStoreProvider } from './indexes/columnStore/types.js'
export type {
AggregationProvider,
AggregateDefinition,
AggregateGroupState,
AggregateResult,
AggregateQueryParams,
GroupByDimension,
} from './types/brainy.types.js'
/**
* Plugin interface all brainy plugins must implement this.
*/
export interface BrainyPlugin {
/** Unique plugin name (typically the npm package name) */
name: string
/**
* Called by brainy during init() to activate the plugin.
* Return true if activation succeeded, false to skip.
*/
activate(context: BrainyPluginContext): Promise<boolean>
/**
* Called when brainy.close() is invoked. Optional cleanup.
*/
deactivate?(): Promise<void>
}
/**
* Context passed to plugins during activation.
*/
export interface BrainyPluginContext {
/**
* Register a provider for a named subsystem.
*
* Well-known provider keys (used by cortex):
* - 'metadataIndex' MetadataIndexManager replacement
* - 'graphIndex' GraphAdjacencyIndex replacement
* - 'entityIdMapper' EntityIdMapper replacement
* - 'cache' UnifiedCache replacement
* - 'vector' JsHnswVectorIndex replacement (vector index engine)
* - 'roaring' RoaringBitmap32 replacement
* - 'embeddings' Embedding engine replacement (single text)
* - 'embedBatch' Batch embedding engine (texts[] vectors[])
* - 'distance' Distance function overrides
* - 'msgpack' Msgpack encode/decode
* - 'aggregation' AggregationIndex replacement (incremental aggregates)
*
* Storage adapter keys:
* - 'storage:<name>' Custom storage adapter factory
*/
registerProvider(key: string, implementation: unknown): void
/** Brainy version for compatibility checks */
readonly version: string
}
// ===========================================================================
// Provider contracts — the EXACT surface Brainy calls on each registered
// provider.
//
// These interfaces are the type-level half of the provider-parity guarantee.
// They capture only what Brainy actually invokes, so a native accelerator
// (Cortex) that declares `implements MetadataIndexProvider` gets a compile
// error the moment a method Brainy depends on is dropped or its signature
// drifts. Brainy's own baseline classes (`MetadataIndexManager`,
// `GraphAdjacencyIndex`, `JsHnswVectorIndex`, `EntityIdMapper`, `UnifiedCache`)
// implement them too, so the contract can never silently diverge from the
// thing Brainy ships.
//
// Keep these in lockstep with the call sites in `brainy.ts` and the index
// classes. When Brainy starts calling a new member, add it here — every
// implementation then fails to compile until it provides the member.
// ===========================================================================
/**
* The `'metadataIndex'` provider a drop-in for `MetadataIndexManager`.
* Brainy calls this surface via `this.metadataIndex.*` (see `brainy.ts`) and
* the transactional add/remove operations.
*/
export interface MetadataIndexProvider {
init(): Promise<void>
flush(): Promise<void>
rebuild(): Promise<void>
addToIndex(id: string, entityOrMetadata: any, skipFlush?: boolean, deferWrites?: boolean): Promise<void>
removeFromIndex(id: string, metadata?: any): Promise<void>
getIds(field: string, value: any): Promise<string[]>
getIdsForFilter(filter: any): Promise<string[]>
getIdsForTextQuery(query: string): Promise<Array<{ id: string; matchCount: number }>>
getSortedIdsForFilter(filter: any, orderBy: string, order?: 'asc' | 'desc'): Promise<string[]>
getFilterValues(field: string): Promise<string[]>
getFilterFields(): Promise<string[]>
getFieldValueForEntity(entityId: string, field: string): Promise<any>
getFieldsForType(nounType: NounType): Promise<Array<{ field: string; affinity: number; occurrences: number; totalEntities: number }>>
getFieldStatistics(): Promise<Map<string, unknown>>
getFieldsWithCardinality(): Promise<Array<{ field: string; cardinality: number; distribution: string }>>
getOptimalQueryPlan(filters: Record<string, any>): Promise<unknown>
/** Report which index path a `where` clause on `field` will hit (drives `brain.explain()`). */
explainField(field: string): Promise<{ path: 'column-store' | 'sparse-chunked' | 'none'; notes?: string }>
getCountForCriteria(field: string, value: any): Promise<number>
getEntityCountByType(type: string): number
getEntityCountByTypeEnum(type: NounType): number
getTotalEntityCount(): number
getAllEntityCounts(): Map<string, number>
getTopNounTypes(n: number): NounType[]
getTopVerbTypes(n: number): VerbType[]
getAllNounTypeCounts(): Map<NounType, number>
getAllVerbTypeCounts(): Map<VerbType, number>
getAllVFSEntityCounts(): Promise<Map<string, number>>
detectAndRepairCorruption(): Promise<void>
validateConsistency(): Promise<{
healthy: boolean
avgEntriesPerEntity: number
entityCount: number
indexEntryCount: number
recommendation: string | null
}>
tokenize(text: string): string[]
extractTextContent(data: any): string
getStats(): Promise<MetadataIndexStats>
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).
2026-06-10 10:45:45 -07:00
/**
* The shared UUID int mapper the single source of truth for entity-int
* resolution at the provider boundary. The coordinator (`brainy.ts`) resolves
* UUID int exactly once before every graph-index call (`getOrAssign` on
* writes, `getInt` on reads `undefined` means "never mapped", i.e. the
* entity has no relations) and converts provider-returned ints back with
* `getUuid`. Ints are u32 today (the `EntityIdSpaceExceeded` guard enforces
* the ceiling on the JS path), so `Number(bigint)` narrowing is lossless.
*/
getIdMapper(): {
getOrAssign(uuid: string): number
getInt(uuid: string): number | undefined
getUuid(intId: number): string | undefined
}
/** The column store the coordinator delegates `where`/`orderBy` to. */
readonly columnStore: import('./indexes/columnStore/types.js').ColumnStoreProvider
}
/**
* The `'graphIndex'` provider a drop-in for `GraphAdjacencyIndex`.
* Brainy calls this surface via `this.graphIndex.*` (some optional-chained).
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).
2026-06-10 10:45:45 -07:00
*
* **8.0 u64 contract BigInt at the boundary.** Reads take entity ints
* (from the metadata index's idMapper) and return entity/verb ints as
* `bigint[]`. The coordinator owns ALL UUID int conversion: it resolves
* UUIDs to ints once at the `brainy.ts` boundary (`getOrAssign` on writes,
* `getInt` on reads, returning empty results for unmapped UUIDs without
* calling the provider) and maps returned ints back (`getUuid` for entities,
* {@link GraphIndexProvider.verbIntsToIds} for verbs). Implementations may
* stay u32 internally `Number(bigint)` narrowing is lossless under the
* shipped `EntityIdSpaceExceeded` u32 guard but must speak the bigint
* contract at this surface.
*/
export interface GraphIndexProvider {
/** `false` until the index has loaded; Brainy probes this before fast paths. */
readonly isInitialized: boolean
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).
2026-06-10 10:45:45 -07:00
/**
* @description Entity ints reachable from `id` (1 hop), deduped.
* @param id - The entity's interned int (from the shared idMapper).
* @param options - Direction (`'both'` default) and limit/offset pagination.
* @returns Neighbor entity ints. Empty when the entity has no edges.
*/
getNeighbors(
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).
2026-06-10 10:45:45 -07:00
id: bigint,
options?: { direction?: 'in' | 'out' | 'both'; limit?: number; offset?: number }
): Promise<bigint[]>
/**
* @description Verb ints for all edges originating at `sourceInt`.
* @param sourceInt - The source entity's interned int.
* @param options - Optional limit/offset pagination.
* @returns Verb ints, resolvable via {@link GraphIndexProvider.verbIntsToIds}.
*/
getVerbIdsBySource(sourceInt: bigint, options?: { limit?: number; offset?: number }): Promise<bigint[]>
/**
* @description Verb ints for all edges pointing at `targetInt`.
* @param targetInt - The target entity's interned int.
* @param options - Optional limit/offset pagination.
* @returns Verb ints, resolvable via {@link GraphIndexProvider.verbIntsToIds}.
*/
getVerbIdsByTarget(targetInt: bigint, options?: { limit?: number; offset?: number }): Promise<bigint[]>
/**
* @description Batch reverse resolver: verb ints verb-id strings. REQUIRED
* the provider owns the durable verb-int interning (Brainy keeps only a
* bounded in-memory warm cache fed by `addVerb` returns and this resolver;
* pure optimization, no durability role).
* @param verbInts - Verb ints as returned by the read methods.
* @returns One entry per input, order-preserving; `null` for unknown ints.
*/
verbIntsToIds(verbInts: bigint[]): Promise<(string | null)[]>
getVerbsBatchCached(verbIds: string[]): Promise<Map<string, GraphVerb>>
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).
2026-06-10 10:45:45 -07:00
/**
* @description Index one verb. The coordinator resolves both endpoint ints
* via `idMapper.getOrAssign` and mirrors them onto `verb.sourceInt` /
* `verb.targetInt` before the call.
* @param verb - The verb to index (endpoint UUIDs + derived `sourceInt`/`targetInt`).
* @param sourceInt - The source entity's interned int.
* @param targetInt - The target entity's interned int.
* @returns The interned verb int for `verb.id` (feeds Brainy's warm cache).
*/
addVerb(verb: GraphVerb, sourceInt: bigint, targetInt: bigint): Promise<bigint>
/**
* @description Remove one verb from the index by its id string. The verb's
* interned int stays reserved (ints are never recycled within a generation).
* @param verbId - The verb's UUID string.
* @returns Resolves once the verb no longer appears in reads.
*/
removeVerb(verbId: string): Promise<void>
rebuild(): Promise<void>
flush(): Promise<void>
close(): Promise<void>
size(): number
getStats(): GraphIndexStats
getRelationshipStats(): {
totalRelationships: number
relationshipsByType: Record<string, number>
uniqueSourceNodes: number
uniqueTargetNodes: number
totalNodes: number
}
getRelationshipCountByType(type: string): number
getTotalRelationshipCount(): number
getAllRelationshipCounts(): Map<string, number>
}
/**
refactor(8.0): rename HnswProvider → VectorIndexProvider (8.0 scaffold) Brainy 8.0 collapses the vector-index contract to algorithm-neutral names. "Vector index" describes the role; "HNSW" was the name of one specific implementation. The role name is what the public contract should carry; the algorithm name belongs to the concrete class. This is step 1 of the brainy 8.0 rename scaffolding tracked in handoff thread BRAINY-8.0-RENAME-COORDINATION § A.1 (cortex shipped the matching VectorIndexProvider in commit 0e4d637). CHANGES src/plugin.ts - Primary interface name flipped: HnswProvider → VectorIndexProvider. Same byte-for-byte shape (8 methods, no signatures changed). - HnswProvider kept as a deprecated type alias so call sites compile mid-migration. Removed in a later 8.0 commit. - DiskAnnProvider was already a type alias of HnswProvider; redeclared as alias of VectorIndexProvider with a deprecation note explaining the 'diskann' provider key folds into 'vector' in 8.0. - JSDoc updated to describe the implementation-neutral surface: Brainy's own JS HNSW + any native acceleration provider both satisfy it. src/hnsw/hnswIndex.ts - Internal class HNSWIndex now declares `implements VectorIndexProvider` instead of `implements HnswProvider`. Import + class comment updated. NO-OP scope No behavioural change. Every existing call site continues to work because HnswProvider remains as a temporary alias. Tests + build green. VERIFICATION - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit - npm run build: clean (verified at parent commit 89e4d81; this commit is type-only so doesn't change dist) NEXT IN SCAFFOLDING - Add 'vector' provider key registration alongside 'hnsw' (cache + cortex) - Add config.vector top-level path alongside config.hnsw with recall preset - Migrate brainy.ts call sites to the new names - Rename HNSWIndex class → JsHnswVectorIndex (per planning § 2.3) - Final cleanup commit: remove HnswProvider alias + config.hnsw + 'hnsw' key
2026-06-09 12:58:26 -07:00
* The object returned by the `'vector'` provider factory Brainy's vector
* index contract. Implementations include Brainy's own JS HNSW index and any
* native acceleration provider (e.g. cortex's Adaptive DiskANN).
*
* Brainy calls this surface via `this.index.*` plus the transactional add/remove
* operations. `enableCOW`, `getItem`, and `setPersistMode` are intentionally
* absent: Brainy guards each with feature-detection (`typeof x === 'function'`),
* so they are optional and not part of the required contract (Brainy's own JS
* HNSW index omits `setPersistMode`, for instance).
*
* **Provider key:** registered under `'vector'` the only key Brainy
* consults for the vector index. The pre-8.0 `'hnsw'` and `'diskann'` keys
* are retired and never looked up.
*/
refactor(8.0): rename HnswProvider → VectorIndexProvider (8.0 scaffold) Brainy 8.0 collapses the vector-index contract to algorithm-neutral names. "Vector index" describes the role; "HNSW" was the name of one specific implementation. The role name is what the public contract should carry; the algorithm name belongs to the concrete class. This is step 1 of the brainy 8.0 rename scaffolding tracked in handoff thread BRAINY-8.0-RENAME-COORDINATION § A.1 (cortex shipped the matching VectorIndexProvider in commit 0e4d637). CHANGES src/plugin.ts - Primary interface name flipped: HnswProvider → VectorIndexProvider. Same byte-for-byte shape (8 methods, no signatures changed). - HnswProvider kept as a deprecated type alias so call sites compile mid-migration. Removed in a later 8.0 commit. - DiskAnnProvider was already a type alias of HnswProvider; redeclared as alias of VectorIndexProvider with a deprecation note explaining the 'diskann' provider key folds into 'vector' in 8.0. - JSDoc updated to describe the implementation-neutral surface: Brainy's own JS HNSW + any native acceleration provider both satisfy it. src/hnsw/hnswIndex.ts - Internal class HNSWIndex now declares `implements VectorIndexProvider` instead of `implements HnswProvider`. Import + class comment updated. NO-OP scope No behavioural change. Every existing call site continues to work because HnswProvider remains as a temporary alias. Tests + build green. VERIFICATION - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit - npm run build: clean (verified at parent commit 89e4d81; this commit is type-only so doesn't change dist) NEXT IN SCAFFOLDING - Add 'vector' provider key registration alongside 'hnsw' (cache + cortex) - Add config.vector top-level path alongside config.hnsw with recall preset - Migrate brainy.ts call sites to the new names - Rename HNSWIndex class → JsHnswVectorIndex (per planning § 2.3) - Final cleanup commit: remove HnswProvider alias + config.hnsw + 'hnsw' key
2026-06-09 12:58:26 -07:00
export interface VectorIndexProvider {
addItem(item: VectorDocument): Promise<string>
removeItem(id: string): Promise<boolean>
search(
queryVector: Vector,
k?: number,
filter?: (id: string) => Promise<boolean>,
options?: { rerank?: { multiplier: number }; candidateIds?: string[] }
): Promise<Array<[string, number]>>
size(): number
clear(): void
rebuild(options?: any): Promise<void>
flush(): Promise<number>
getPersistMode(): 'immediate' | 'deferred'
}
/**
* The `'entityIdMapper'` provider a drop-in for `EntityIdMapper`. Injected
* into the TypeScript `MetadataIndexManager` when a native metadata index is
* not also registered; that coordinator calls this full surface (incl.
* `getAllIntIds`, the all-ids universe for negation / `exists:false` filters).
*/
export interface EntityIdMapperProvider {
init(): Promise<void>
getOrAssign(uuid: string): number
getUuid(intId: number): string | undefined
getInt(uuid: string): number | undefined
remove(uuid: string): boolean
flush(): Promise<void>
clear(): Promise<void>
getAllIntIds(): number[]
intsIterableToUuids(ints: Iterable<number>): string[]
readonly size: number
}
/**
* The `'cache'` provider a drop-in for `UnifiedCache`. Brainy installs it as
* the global cache (`setGlobalCache`) and calls this surface via
* `getGlobalCache()`.
*/
export interface CacheProvider {
getSync(key: string): any | undefined
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6) Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'- flavoured public name introduced as a compat shim in steps 1-5. The algorithm-neutral surface is now the only surface. REMOVED — PHASE A (type aliases) src/plugin.ts - Removed `export type HnswProvider = VectorIndexProvider` alias. - Removed `export type DiskAnnProvider = VectorIndexProvider` alias. src/index.ts - Removed `export const HNSWIndex = JsHnswVectorIndex` alias. src/types/brainy.types.ts - Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field carries the same boolean). src/brainy.ts - `stats()` no longer emits the legacy `hnsw` field on `indexHealth`. REMOVED — PHASE B (storage adapter rename, 8 adapters) Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and `getHNSWData` → `getVectorIndexData` across: - src/storage/adapters/baseStorageAdapter.ts (abstract declarations) - src/storage/adapters/fileSystemStorage.ts - src/storage/adapters/gcsStorage.ts - src/storage/adapters/r2Storage.ts - src/storage/adapters/s3CompatibleStorage.ts - src/storage/adapters/azureBlobStorage.ts - src/storage/adapters/opfsStorage.ts - src/storage/adapters/memoryStorage.ts - src/storage/adapters/historicalStorageAdapter.ts - src/brainy.ts (call sites) - src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites) The default-delegation wrappers added in scaffold step 4 are removed (would have been duplicate declarations after the rename). REMOVED — PHASE C (cache category 'hnsw') src/utils/unifiedCache.ts - Cache-category union narrowed from 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other' to 'vectors' | 'metadata' | 'embedding' | 'other' - typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios, and the fairness-check iterator all drop the 'hnsw' key. - Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory category. Cache is rebuildable; entries naturally don't exist after a restart, so no migration path is needed. src/hnsw/hnswIndex.ts - 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'. - Cache key prefix `hnsw:vector:` → `vector:`. - typeCounts/typeSizes/typeAccessCounts accessor renames `.hnsw` → `.vectors`. tests/unit/utils/unifiedCache-eviction.test.ts - Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...). - Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`. REMOVED — PHASE D (config.hnsw) src/types/brainy.types.ts - Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is `BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`. src/brainy.ts - normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>. - setupIndex() rewired: - Reads from `this.config.vector` (not `this.config.hnsw`). - Calls `resolveJsHnswConfig(this.config.vector)` to translate the `recall` preset into M / efConstruction / efSearch knobs (with `advanced.hnsw` overrides winning when supplied). - Imports `resolveJsHnswConfig` from './utils/recallPreset.js'. NOT IN THIS COMMIT (deliberately) - Persisted file path migration `_system/hnsw-*.json` → `_system/vector-index-*.json`. Requires dual-read logic on boot across 8 storage adapters. Per integration doc lines 531-539: "Reader accepts either spelling on load; writer emits the new spelling only; brains self-migrate on the next persist after upgrade." This is a separate body of work and lands in a follow-up commit before 8.0 GA. - `config.hnswPersistMode` top-level field is unchanged. It's not in the rename inventory; a future commit can fold it into `config.vector.persistMode` if desired. - strictConfig enforcement wiring. The field is accepted by normalizeConfig() with default 'warn'; the actual warning emission at knob-mismatch sites lands when the config-resolution layer is touched for the persisted-path migration. VERIFICATION - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit (one test fixture updated to use the new category name; assertion still passes after fixture rename) - npm run build: clean The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end: VectorIndexProvider, config.vector.recall, JsHnswVectorIndex, saveVectorIndexData, 'vectors' cache category, indexHealth.vector, strictConfig — all standalone. No legacy 'hnsw' name remains in any public type or method signature.
2026-06-09 13:28:53 -07:00
set(key: string, data: any, type: 'vectors' | 'metadata' | 'embedding' | 'other', size: number, rebuildCost?: number): void
delete(key: string): boolean
deleteByPrefix(prefix: string): number
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6) Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'- flavoured public name introduced as a compat shim in steps 1-5. The algorithm-neutral surface is now the only surface. REMOVED — PHASE A (type aliases) src/plugin.ts - Removed `export type HnswProvider = VectorIndexProvider` alias. - Removed `export type DiskAnnProvider = VectorIndexProvider` alias. src/index.ts - Removed `export const HNSWIndex = JsHnswVectorIndex` alias. src/types/brainy.types.ts - Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field carries the same boolean). src/brainy.ts - `stats()` no longer emits the legacy `hnsw` field on `indexHealth`. REMOVED — PHASE B (storage adapter rename, 8 adapters) Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and `getHNSWData` → `getVectorIndexData` across: - src/storage/adapters/baseStorageAdapter.ts (abstract declarations) - src/storage/adapters/fileSystemStorage.ts - src/storage/adapters/gcsStorage.ts - src/storage/adapters/r2Storage.ts - src/storage/adapters/s3CompatibleStorage.ts - src/storage/adapters/azureBlobStorage.ts - src/storage/adapters/opfsStorage.ts - src/storage/adapters/memoryStorage.ts - src/storage/adapters/historicalStorageAdapter.ts - src/brainy.ts (call sites) - src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites) The default-delegation wrappers added in scaffold step 4 are removed (would have been duplicate declarations after the rename). REMOVED — PHASE C (cache category 'hnsw') src/utils/unifiedCache.ts - Cache-category union narrowed from 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other' to 'vectors' | 'metadata' | 'embedding' | 'other' - typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios, and the fairness-check iterator all drop the 'hnsw' key. - Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory category. Cache is rebuildable; entries naturally don't exist after a restart, so no migration path is needed. src/hnsw/hnswIndex.ts - 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'. - Cache key prefix `hnsw:vector:` → `vector:`. - typeCounts/typeSizes/typeAccessCounts accessor renames `.hnsw` → `.vectors`. tests/unit/utils/unifiedCache-eviction.test.ts - Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...). - Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`. REMOVED — PHASE D (config.hnsw) src/types/brainy.types.ts - Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is `BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`. src/brainy.ts - normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>. - setupIndex() rewired: - Reads from `this.config.vector` (not `this.config.hnsw`). - Calls `resolveJsHnswConfig(this.config.vector)` to translate the `recall` preset into M / efConstruction / efSearch knobs (with `advanced.hnsw` overrides winning when supplied). - Imports `resolveJsHnswConfig` from './utils/recallPreset.js'. NOT IN THIS COMMIT (deliberately) - Persisted file path migration `_system/hnsw-*.json` → `_system/vector-index-*.json`. Requires dual-read logic on boot across 8 storage adapters. Per integration doc lines 531-539: "Reader accepts either spelling on load; writer emits the new spelling only; brains self-migrate on the next persist after upgrade." This is a separate body of work and lands in a follow-up commit before 8.0 GA. - `config.hnswPersistMode` top-level field is unchanged. It's not in the rename inventory; a future commit can fold it into `config.vector.persistMode` if desired. - strictConfig enforcement wiring. The field is accepted by normalizeConfig() with default 'warn'; the actual warning emission at knob-mismatch sites lands when the config-resolution layer is touched for the persisted-path migration. VERIFICATION - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit (one test fixture updated to use the new category name; assertion still passes after fixture rename) - npm run build: clean The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end: VectorIndexProvider, config.vector.recall, JsHnswVectorIndex, saveVectorIndexData, 'vectors' cache category, indexHealth.vector, strictConfig — all standalone. No legacy 'hnsw' name remains in any public type or method signature.
2026-06-09 13:28:53 -07:00
clear(type?: 'vectors' | 'metadata' | 'embedding' | 'other'): void
}
// The `'embeddings'` / `'embedBatch'` providers are function-shaped and already
// typed by the existing `EmbeddingFunction` (see `coreTypes.ts`), which Brainy
// uses at the `getProvider('embeddings')` call site. No separate interface is
// added here to avoid a duplicate, unwired contract.
feat: graph link compression — delta-varint connections (2.4.0 #3) Cortex registers a graph:compression provider exposing `encode`/`decode` for HNSW connection lists (delta-varint, ~4× smaller edges than the JSON-UUID-array shape brainy has persisted since the beginning). This wires the consumer side without changing the saveHNSWData/getHNSWData adapter signatures. Architecture: - NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer via the provider + stable EntityIdMapper. Custom wire format: [count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node regardless of level count, so the storage I/O is identical to the legacy path (one saveHNSWData call) plus a single saveBinaryBlob. - HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field with `setConnectionsCodec()` setter. THREE save sites (deferred flush, immediate-mode entity persist, immediate-mode neighbor updates) now go through a single `persistNodeConnections(nodeId, noun)` helper: when the codec is wired AND the storage adapter exposes saveBinaryBlob, it encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and records `connections: {}` in saveHNSWData as the marker. Otherwise the legacy JSON-array path is taken. TWO load sites use a matching `restoreNodeConnections` helper that tries loadBinaryBlob first and falls back to the legacy hnswData.connections field on miss / decode error. Format convergence is lazy: pre-2.4.0 nodes still load via the legacy path, then write the compressed form on next dirty save. - brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend during init. Activates when (a) the graph:compression provider is registered and (b) the metadata index exposes its idMapper. Unlike the mmap-vector backend, this layer engages on EVERY brainy 7.25.0 adapter — the blob primitive itself is universal; only the codec presence gates activation. - Provider interface GraphCompressionProvider in plugin.ts — encode + decode static signatures. Brainy depends on the interface; cortex's registered { encode: encodeConnections, decode: decodeConnections } satisfies it structurally. Tests (1437 total, +4 vs prior tip): - tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON mock provider: single-level round-trip, empty-Map round-trip (one-byte buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the decoding idMapper no longer knows. The real cross-language byte format is exercised when cortex 2.4.0 wires its delta-varint encode/decode in. This completes the brainy half of the 2.4.0 storage foundation: stable ids (#23), mmap vectors (#24), graph link compression (#25), and column-store interchange (#26). Coordinated release as brainy 7.26.0 + cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
/**
* The `'graph:compression'` provider pure-function encode/decode for HNSW
* connection lists as compact delta-varint byte sequences (cortex's
* `encodeConnections` / `decodeConnections`).
*
* Brainy's `JsHnswVectorIndex` consumes this via a `ConnectionsCodec` that translates
feat: graph link compression — delta-varint connections (2.4.0 #3) Cortex registers a graph:compression provider exposing `encode`/`decode` for HNSW connection lists (delta-varint, ~4× smaller edges than the JSON-UUID-array shape brainy has persisted since the beginning). This wires the consumer side without changing the saveHNSWData/getHNSWData adapter signatures. Architecture: - NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer via the provider + stable EntityIdMapper. Custom wire format: [count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node regardless of level count, so the storage I/O is identical to the legacy path (one saveHNSWData call) plus a single saveBinaryBlob. - HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field with `setConnectionsCodec()` setter. THREE save sites (deferred flush, immediate-mode entity persist, immediate-mode neighbor updates) now go through a single `persistNodeConnections(nodeId, noun)` helper: when the codec is wired AND the storage adapter exposes saveBinaryBlob, it encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and records `connections: {}` in saveHNSWData as the marker. Otherwise the legacy JSON-array path is taken. TWO load sites use a matching `restoreNodeConnections` helper that tries loadBinaryBlob first and falls back to the legacy hnswData.connections field on miss / decode error. Format convergence is lazy: pre-2.4.0 nodes still load via the legacy path, then write the compressed form on next dirty save. - brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend during init. Activates when (a) the graph:compression provider is registered and (b) the metadata index exposes its idMapper. Unlike the mmap-vector backend, this layer engages on EVERY brainy 7.25.0 adapter — the blob primitive itself is universal; only the codec presence gates activation. - Provider interface GraphCompressionProvider in plugin.ts — encode + decode static signatures. Brainy depends on the interface; cortex's registered { encode: encodeConnections, decode: decodeConnections } satisfies it structurally. Tests (1437 total, +4 vs prior tip): - tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON mock provider: single-level round-trip, empty-Map round-trip (one-byte buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the decoding idMapper no longer knows. The real cross-language byte format is exercised when cortex 2.4.0 wires its delta-varint encode/decode in. This completes the brainy half of the 2.4.0 storage foundation: stable ids (#23), mmap vectors (#24), graph link compression (#25), and column-store interchange (#26). Coordinated release as brainy 7.26.0 + cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
* UUIDs to stable int slots via the `EntityIdMapper`, encodes, and persists
* the compressed bytes through the binary-blob primitive. On load, the blob
* is fetched + decoded back into UUID sets `setConnectionsCodec()` on
* `JsHnswVectorIndex` is the injection point. Read path is dual-format: when no blob
feat: graph link compression — delta-varint connections (2.4.0 #3) Cortex registers a graph:compression provider exposing `encode`/`decode` for HNSW connection lists (delta-varint, ~4× smaller edges than the JSON-UUID-array shape brainy has persisted since the beginning). This wires the consumer side without changing the saveHNSWData/getHNSWData adapter signatures. Architecture: - NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer via the provider + stable EntityIdMapper. Custom wire format: [count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node regardless of level count, so the storage I/O is identical to the legacy path (one saveHNSWData call) plus a single saveBinaryBlob. - HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field with `setConnectionsCodec()` setter. THREE save sites (deferred flush, immediate-mode entity persist, immediate-mode neighbor updates) now go through a single `persistNodeConnections(nodeId, noun)` helper: when the codec is wired AND the storage adapter exposes saveBinaryBlob, it encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and records `connections: {}` in saveHNSWData as the marker. Otherwise the legacy JSON-array path is taken. TWO load sites use a matching `restoreNodeConnections` helper that tries loadBinaryBlob first and falls back to the legacy hnswData.connections field on miss / decode error. Format convergence is lazy: pre-2.4.0 nodes still load via the legacy path, then write the compressed form on next dirty save. - brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend during init. Activates when (a) the graph:compression provider is registered and (b) the metadata index exposes its idMapper. Unlike the mmap-vector backend, this layer engages on EVERY brainy 7.25.0 adapter — the blob primitive itself is universal; only the codec presence gates activation. - Provider interface GraphCompressionProvider in plugin.ts — encode + decode static signatures. Brainy depends on the interface; cortex's registered { encode: encodeConnections, decode: decodeConnections } satisfies it structurally. Tests (1437 total, +4 vs prior tip): - tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON mock provider: single-level round-trip, empty-Map round-trip (one-byte buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the decoding idMapper no longer knows. The real cross-language byte format is exercised when cortex 2.4.0 wires its delta-varint encode/decode in. This completes the brainy half of the 2.4.0 storage foundation: stable ids (#23), mmap vectors (#24), graph link compression (#25), and column-store interchange (#26). Coordinated release as brainy 7.26.0 + cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
* exists for a node, the connections fall back to the legacy JSON-array path
refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6) Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'- flavoured public name introduced as a compat shim in steps 1-5. The algorithm-neutral surface is now the only surface. REMOVED — PHASE A (type aliases) src/plugin.ts - Removed `export type HnswProvider = VectorIndexProvider` alias. - Removed `export type DiskAnnProvider = VectorIndexProvider` alias. src/index.ts - Removed `export const HNSWIndex = JsHnswVectorIndex` alias. src/types/brainy.types.ts - Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field carries the same boolean). src/brainy.ts - `stats()` no longer emits the legacy `hnsw` field on `indexHealth`. REMOVED — PHASE B (storage adapter rename, 8 adapters) Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and `getHNSWData` → `getVectorIndexData` across: - src/storage/adapters/baseStorageAdapter.ts (abstract declarations) - src/storage/adapters/fileSystemStorage.ts - src/storage/adapters/gcsStorage.ts - src/storage/adapters/r2Storage.ts - src/storage/adapters/s3CompatibleStorage.ts - src/storage/adapters/azureBlobStorage.ts - src/storage/adapters/opfsStorage.ts - src/storage/adapters/memoryStorage.ts - src/storage/adapters/historicalStorageAdapter.ts - src/brainy.ts (call sites) - src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites) The default-delegation wrappers added in scaffold step 4 are removed (would have been duplicate declarations after the rename). REMOVED — PHASE C (cache category 'hnsw') src/utils/unifiedCache.ts - Cache-category union narrowed from 'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other' to 'vectors' | 'metadata' | 'embedding' | 'other' - typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios, and the fairness-check iterator all drop the 'hnsw' key. - Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory category. Cache is rebuildable; entries naturally don't exist after a restart, so no migration path is needed. src/hnsw/hnswIndex.ts - 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'. - Cache key prefix `hnsw:vector:` → `vector:`. - typeCounts/typeSizes/typeAccessCounts accessor renames `.hnsw` → `.vectors`. tests/unit/utils/unifiedCache-eviction.test.ts - Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...). - Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`. REMOVED — PHASE D (config.hnsw) src/types/brainy.types.ts - Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is `BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`. src/brainy.ts - normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>. - setupIndex() rewired: - Reads from `this.config.vector` (not `this.config.hnsw`). - Calls `resolveJsHnswConfig(this.config.vector)` to translate the `recall` preset into M / efConstruction / efSearch knobs (with `advanced.hnsw` overrides winning when supplied). - Imports `resolveJsHnswConfig` from './utils/recallPreset.js'. NOT IN THIS COMMIT (deliberately) - Persisted file path migration `_system/hnsw-*.json` → `_system/vector-index-*.json`. Requires dual-read logic on boot across 8 storage adapters. Per integration doc lines 531-539: "Reader accepts either spelling on load; writer emits the new spelling only; brains self-migrate on the next persist after upgrade." This is a separate body of work and lands in a follow-up commit before 8.0 GA. - `config.hnswPersistMode` top-level field is unchanged. It's not in the rename inventory; a future commit can fold it into `config.vector.persistMode` if desired. - strictConfig enforcement wiring. The field is accepted by normalizeConfig() with default 'warn'; the actual warning emission at knob-mismatch sites lands when the config-resolution layer is touched for the persisted-path migration. VERIFICATION - npx tsc --noEmit: clean - npm test: 1468 / 1468 unit (one test fixture updated to use the new category name; assertion still passes after fixture rename) - npm run build: clean The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end: VectorIndexProvider, config.vector.recall, JsHnswVectorIndex, saveVectorIndexData, 'vectors' cache category, indexHealth.vector, strictConfig — all standalone. No legacy 'hnsw' name remains in any public type or method signature.
2026-06-09 13:28:53 -07:00
* embedded in `saveVectorIndexData`, so pre-2.4.0 indexes keep loading unchanged
feat: graph link compression — delta-varint connections (2.4.0 #3) Cortex registers a graph:compression provider exposing `encode`/`decode` for HNSW connection lists (delta-varint, ~4× smaller edges than the JSON-UUID-array shape brainy has persisted since the beginning). This wires the consumer side without changing the saveHNSWData/getHNSWData adapter signatures. Architecture: - NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer via the provider + stable EntityIdMapper. Custom wire format: [count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node regardless of level count, so the storage I/O is identical to the legacy path (one saveHNSWData call) plus a single saveBinaryBlob. - HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field with `setConnectionsCodec()` setter. THREE save sites (deferred flush, immediate-mode entity persist, immediate-mode neighbor updates) now go through a single `persistNodeConnections(nodeId, noun)` helper: when the codec is wired AND the storage adapter exposes saveBinaryBlob, it encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and records `connections: {}` in saveHNSWData as the marker. Otherwise the legacy JSON-array path is taken. TWO load sites use a matching `restoreNodeConnections` helper that tries loadBinaryBlob first and falls back to the legacy hnswData.connections field on miss / decode error. Format convergence is lazy: pre-2.4.0 nodes still load via the legacy path, then write the compressed form on next dirty save. - brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend during init. Activates when (a) the graph:compression provider is registered and (b) the metadata index exposes its idMapper. Unlike the mmap-vector backend, this layer engages on EVERY brainy 7.25.0 adapter — the blob primitive itself is universal; only the codec presence gates activation. - Provider interface GraphCompressionProvider in plugin.ts — encode + decode static signatures. Brainy depends on the interface; cortex's registered { encode: encodeConnections, decode: decodeConnections } satisfies it structurally. Tests (1437 total, +4 vs prior tip): - tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON mock provider: single-level round-trip, empty-Map round-trip (one-byte buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the decoding idMapper no longer knows. The real cross-language byte format is exercised when cortex 2.4.0 wires its delta-varint encode/decode in. This completes the brainy half of the 2.4.0 storage foundation: stable ids (#23), mmap vectors (#24), graph link compression (#25), and column-store interchange (#26). Coordinated release as brainy 7.26.0 + cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
* and convergence to the compressed form happens lazily on next save.
*
* Activated only when the storage adapter exposes the binary-blob primitive
* AND the metadata index resolves a stable idMapper. Cloud adapters that
* lack a real local-path resolution still benefit, since the blob primitive
* itself works across every adapter as of brainy 7.25.0.
*/
export interface GraphCompressionProvider {
/** Encode a list of u32 ints to compact delta-varint bytes. Sorts internally. */
encode(ids: number[]): Buffer
/** Decode delta-varint bytes back to a u32 list. */
decode(data: Buffer): number[]
}
/**
* Storage adapter factory plugins register these to provide
* new storage backends that users reference by name.
*
* Example: A Redis plugin registers 'storage:redis', then users
* can use `new Brainy({ storage: 'redis', redis: { host: '...' } })`
*/
export interface StorageAdapterFactory {
create(config: Record<string, unknown>): StorageAdapter | Promise<StorageAdapter>
name: string
}
/**
* Plugin registry manages plugin lifecycle and provider resolution.
*/
export class PluginRegistry {
private plugins: Map<string, BrainyPlugin> = new Map()
private providers: Map<string, unknown> = new Map()
private activated: Set<string> = new Set()
/**
* Register a plugin manually.
*/
register(plugin: BrainyPlugin): void {
this.plugins.set(plugin.name, plugin)
}
/**
* Activate all registered plugins.
*/
async activateAll(context: BrainyPluginContext): Promise<string[]> {
const activated: string[] = []
for (const [name, plugin] of this.plugins) {
if (this.activated.has(name)) continue
try {
const success = await plugin.activate(context)
if (success) {
this.activated.add(name)
activated.push(name)
}
} catch (error) {
console.warn(`[brainy] Plugin ${name} failed to activate:`, error)
}
}
return activated
}
/**
* Deactivate all plugins (called during close()).
*/
async deactivateAll(): Promise<void> {
for (const [name, plugin] of this.plugins) {
if (!this.activated.has(name)) continue
try {
await plugin.deactivate?.()
this.activated.delete(name)
} catch {
// Non-fatal
}
}
}
/**
* Get a registered provider by key.
*/
getProvider<T = unknown>(key: string): T | undefined {
return this.providers.get(key) as T | undefined
}
/**
* Check if a provider is registered.
*/
hasProvider(key: string): boolean {
return this.providers.has(key)
}
/**
* Register a provider (called by plugins via BrainyPluginContext).
*/
registerProvider(key: string, implementation: unknown): void {
this.providers.set(key, implementation)
}
/**
* Get a storage adapter factory by name.
*/
getStorageFactory(name: string): StorageAdapterFactory | undefined {
return this.providers.get(`storage:${name}`) as StorageAdapterFactory | undefined
}
/** Get active plugin names */
getActivePlugins(): string[] {
return [...this.activated]
}
/** Check if any plugins are active */
hasActivePlugins(): boolean {
return this.activated.size > 0
}
}