brainy/src/plugin.ts

458 lines
17 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
* - 'hnsw' JsHnswVectorIndex replacement
* - '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>
/** The shared UUID ↔ int mapper. Brainy reads `.getUuid(intId)` off the result. */
getIdMapper(): { 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).
*/
export interface GraphIndexProvider {
/** `false` until the index has loaded; Brainy probes this before fast paths. */
readonly isInitialized: boolean
getNeighbors(
id: string,
optionsOrDirection?: { direction?: 'in' | 'out' | 'both'; limit?: number; offset?: number } | 'in' | 'out' | 'both'
): Promise<string[]>
getVerbIdsBySource(sourceId: string, options?: { limit?: number; offset?: number }): Promise<string[]>
getVerbIdsByTarget(targetId: string, options?: { limit?: number; offset?: number }): Promise<string[]>
getVerbsBatchCached(verbIds: string[]): Promise<Map<string, GraphVerb>>
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 legacy `'hnsw'` key is
* accepted as a compat shim in 8.0 and retired in a future release.
*/
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'
}
/**
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
* @deprecated Renamed to {@link VectorIndexProvider} in Brainy 8.0. The
* interface shape is byte-for-byte identical; only the name changed because
* "vector index" describes the role, while "HNSW" was the name of one
* specific implementation. This alias is provided as a compatibility shim
* for code mid-migration; new code should import `VectorIndexProvider`.
*/
export type HnswProvider = VectorIndexProvider
/**
* @deprecated The `'diskann'` provider key has been folded into `'vector'` in
* Brainy 8.0. The vector index contract is implementation-neutral: any provider
* registered under `'vector'` may be HNSW, DiskANN, or a future algorithm. The
* type alias is preserved so existing imports compile while consumers migrate
* to `VectorIndexProvider`.
*/
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 type DiskAnnProvider = VectorIndexProvider
/**
* 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
set(key: string, data: any, type: 'hnsw' | 'metadata' | 'embedding' | 'other', size: number, rebuildCost?: number): void
delete(key: string): boolean
deleteByPrefix(prefix: string): number
clear(type?: 'hnsw' | '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
* embedded in `saveHNSWData`, so pre-2.4.0 indexes keep loading unchanged
* 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[]
}
feat: mmap-vector backend wiring — HNSWIndex consumes vectorStore:mmap (2.4.0 #2) Cortex already registers the vectorStore:mmap provider (its Rust NativeMmapVectorStore), but brainy has never consumed it — preloadVectors and getVectorSafe still go straight to storage.getNounVector for every id, even when an mmap layer is available. This wires the consumer end. Architecture: - NEW MmapVectorBackend (src/hnsw/mmapVectorBackend.ts) — bridges brainy's UUID-keyed vector reads to a int-slot mmap file via the vectorStore:mmap provider. Slots are addressed by the stable int id from the post-2.4.0 #1 EntityIdMapper (the foundation this depends on). Auto-grows the file (doubling) when a write lands beyond capacity, so HNSWIndex never has to think about sizing. The class never touches per-entity storage — it owns only the mmap layer. - HNSWIndex changes — adds a vectorBackend field + a setVectorBackend setter. The vector read paths (preloadVectors, getVectorSafe) try the mmap layer first; on a storage fallback hit, they LAZILY write back into the mmap slot. An upgraded install converges to the zero-copy fast path under live traffic — no big-bang migration step. The legacy per-entity path is preserved and still used when no backend is set. - brainy.ts wiring — a new private wireMmapVectorBackend() runs once during init, after plugin activation + metadataIndex setup. It activates the backend only when (a) the vectorStore:mmap provider is registered, (b) the storage adapter resolves a real local path via getBinaryBlobPath(), and (c) the metadata index exposes its idMapper. Cloud adapters return null on (b) and the backend is silently skipped; HNSWIndex's behaviour is then identical to pre-2.4.0. - Provider interfaces in plugin.ts — VectorStoreMmapProvider and VectorStoreMmapInstance document the contract cortex's class fulfils (the class IS the provider — static factory methods). Brainy depends on the interfaces, not on cortex; the structural match is verified when cortex 2.4.0 picks up this brainy release. Tests (1428 total, +11 vs pre-2.4.0): - tests/unit/hnsw/mmap-vector-backend.test.ts — 6 unit tests with an in-memory mock provider. Covers round-trip, batch reads with interleaved misses, slot stability (no re-slotting on overwrite), file growth without data loss, idempotent open, and null returns for unwritten slots. The real perf integration with cortex's NativeMmapVectorStore is exercised when cortex 2.4.0 wires this in. - tests/unit/utils/entity-id-mapper-stability.test.ts — moved here from tests/regression/ (which is NOT in the unit-config include glob, so the five #23 tests were not actually being run by npm test). The unit config matches tests/unit/**/*.test.ts. The 2.4.0 #2 follow-up will be the chunked-segment layout for remote storage adapters (S3 / R2 / GCS) where a single growing file doesn't fit immutable objects. For 2.4.0 release: local-FS only.
2026-05-28 10:10:05 -07:00
/**
* The `'vectorStore:mmap'` provider a static factory class for an mmap-backed
* vector file. Brainy's JsHnswVectorIndex uses the static `.create()` / `.open()`
feat: mmap-vector backend wiring — HNSWIndex consumes vectorStore:mmap (2.4.0 #2) Cortex already registers the vectorStore:mmap provider (its Rust NativeMmapVectorStore), but brainy has never consumed it — preloadVectors and getVectorSafe still go straight to storage.getNounVector for every id, even when an mmap layer is available. This wires the consumer end. Architecture: - NEW MmapVectorBackend (src/hnsw/mmapVectorBackend.ts) — bridges brainy's UUID-keyed vector reads to a int-slot mmap file via the vectorStore:mmap provider. Slots are addressed by the stable int id from the post-2.4.0 #1 EntityIdMapper (the foundation this depends on). Auto-grows the file (doubling) when a write lands beyond capacity, so HNSWIndex never has to think about sizing. The class never touches per-entity storage — it owns only the mmap layer. - HNSWIndex changes — adds a vectorBackend field + a setVectorBackend setter. The vector read paths (preloadVectors, getVectorSafe) try the mmap layer first; on a storage fallback hit, they LAZILY write back into the mmap slot. An upgraded install converges to the zero-copy fast path under live traffic — no big-bang migration step. The legacy per-entity path is preserved and still used when no backend is set. - brainy.ts wiring — a new private wireMmapVectorBackend() runs once during init, after plugin activation + metadataIndex setup. It activates the backend only when (a) the vectorStore:mmap provider is registered, (b) the storage adapter resolves a real local path via getBinaryBlobPath(), and (c) the metadata index exposes its idMapper. Cloud adapters return null on (b) and the backend is silently skipped; HNSWIndex's behaviour is then identical to pre-2.4.0. - Provider interfaces in plugin.ts — VectorStoreMmapProvider and VectorStoreMmapInstance document the contract cortex's class fulfils (the class IS the provider — static factory methods). Brainy depends on the interfaces, not on cortex; the structural match is verified when cortex 2.4.0 picks up this brainy release. Tests (1428 total, +11 vs pre-2.4.0): - tests/unit/hnsw/mmap-vector-backend.test.ts — 6 unit tests with an in-memory mock provider. Covers round-trip, batch reads with interleaved misses, slot stability (no re-slotting on overwrite), file growth without data loss, idempotent open, and null returns for unwritten slots. The real perf integration with cortex's NativeMmapVectorStore is exercised when cortex 2.4.0 wires this in. - tests/unit/utils/entity-id-mapper-stability.test.ts — moved here from tests/regression/ (which is NOT in the unit-config include glob, so the five #23 tests were not actually being run by npm test). The unit config matches tests/unit/**/*.test.ts. The 2.4.0 #2 follow-up will be the chunked-segment layout for remote storage adapters (S3 / R2 / GCS) where a single growing file doesn't fit immutable objects. For 2.4.0 release: local-FS only.
2026-05-28 10:10:05 -07:00
* factories to open a single-file store at a derived path on disk-resident
* storage adapters; non-FS adapters with no local-path support skip the mmap
* layer and fall back to per-entity vector reads.
*
* Cortex registers `NativeMmapVectorStore` as this provider a Rust mmap'd
* f32 array with a header, batch read/write APIs, and madvise prefetch. Slot
* indices are the stable ints from `EntityIdMapper`; lazy migration on read
* miss writes back from per-entity storage into the slot, converging an
* upgraded install to the fast path without a big-bang step.
*/
export interface VectorStoreMmapProvider {
/** Create a new vector file with pre-allocated slot capacity. */
create(path: string, dim: number, capacity: number): VectorStoreMmapInstance
/** Open an existing vector file for read + write. */
open(path: string): VectorStoreMmapInstance
/** Open an existing vector file for read-only access. */
openReadOnly(path: string): VectorStoreMmapInstance
}
/**
* An open mmap-vector-store instance the surface brainy's JsHnswVectorIndex consumes
feat: mmap-vector backend wiring — HNSWIndex consumes vectorStore:mmap (2.4.0 #2) Cortex already registers the vectorStore:mmap provider (its Rust NativeMmapVectorStore), but brainy has never consumed it — preloadVectors and getVectorSafe still go straight to storage.getNounVector for every id, even when an mmap layer is available. This wires the consumer end. Architecture: - NEW MmapVectorBackend (src/hnsw/mmapVectorBackend.ts) — bridges brainy's UUID-keyed vector reads to a int-slot mmap file via the vectorStore:mmap provider. Slots are addressed by the stable int id from the post-2.4.0 #1 EntityIdMapper (the foundation this depends on). Auto-grows the file (doubling) when a write lands beyond capacity, so HNSWIndex never has to think about sizing. The class never touches per-entity storage — it owns only the mmap layer. - HNSWIndex changes — adds a vectorBackend field + a setVectorBackend setter. The vector read paths (preloadVectors, getVectorSafe) try the mmap layer first; on a storage fallback hit, they LAZILY write back into the mmap slot. An upgraded install converges to the zero-copy fast path under live traffic — no big-bang migration step. The legacy per-entity path is preserved and still used when no backend is set. - brainy.ts wiring — a new private wireMmapVectorBackend() runs once during init, after plugin activation + metadataIndex setup. It activates the backend only when (a) the vectorStore:mmap provider is registered, (b) the storage adapter resolves a real local path via getBinaryBlobPath(), and (c) the metadata index exposes its idMapper. Cloud adapters return null on (b) and the backend is silently skipped; HNSWIndex's behaviour is then identical to pre-2.4.0. - Provider interfaces in plugin.ts — VectorStoreMmapProvider and VectorStoreMmapInstance document the contract cortex's class fulfils (the class IS the provider — static factory methods). Brainy depends on the interfaces, not on cortex; the structural match is verified when cortex 2.4.0 picks up this brainy release. Tests (1428 total, +11 vs pre-2.4.0): - tests/unit/hnsw/mmap-vector-backend.test.ts — 6 unit tests with an in-memory mock provider. Covers round-trip, batch reads with interleaved misses, slot stability (no re-slotting on overwrite), file growth without data loss, idempotent open, and null returns for unwritten slots. The real perf integration with cortex's NativeMmapVectorStore is exercised when cortex 2.4.0 wires this in. - tests/unit/utils/entity-id-mapper-stability.test.ts — moved here from tests/regression/ (which is NOT in the unit-config include glob, so the five #23 tests were not actually being run by npm test). The unit config matches tests/unit/**/*.test.ts. The 2.4.0 #2 follow-up will be the chunked-segment layout for remote storage adapters (S3 / R2 / GCS) where a single growing file doesn't fit immutable objects. For 2.4.0 release: local-FS only.
2026-05-28 10:10:05 -07:00
* for batch vector reads + lazy migration from per-entity storage on miss.
*/
export interface VectorStoreMmapInstance {
/** Number of vectors written (highest written index + 1). */
readonly count: number
/** Maximum slot capacity before resize is needed. */
readonly capacity: number
/** Vector dimensionality. */
readonly dim: number
/** Write a single vector at `index`. Vector is f64 in JS, stored as f32. */
writeVector(index: number, vector: number[]): void
/** Write a flat batch starting at `startIndex`. Returns vectors written. */
writeVectorsBatch(startIndex: number, vectorsFlat: number[]): number
/** Read a single vector at `index`. Returns f64[]. */
readVector(index: number): number[]
/** Read multiple vectors by index. Returns flat f64[] of length n × dim. */
readVectorsBatch(indices: number[]): number[]
/** Prefetch via madvise(WILLNEED); no-op on unsupported platforms. */
prefetch(indices: number[]): void
/** Grow the vector file to a larger slot capacity. Re-maps the file. */
resize(newCapacity: number): void
/** msync — flush pending writes to disk. */
flush(): void
}
/**
* 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
}
}