feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
/**
|
|
|
|
|
* Brainy Plugin System
|
|
|
|
|
*
|
|
|
|
|
* Simple plugin architecture for two use cases:
|
2026-02-01 08:22:07 -08:00
|
|
|
* 1. Native acceleration (@soulcraft/cortex)
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
* 2. Custom storage adapters (e.g., Redis, DynamoDB, custom backends)
|
|
|
|
|
*
|
2026-06-11 14:51:00 -07:00
|
|
|
* Plugins are loaded from an explicit `plugins: [...]` config list or
|
|
|
|
|
* registered manually via `brain.use()` — there is no implicit detection.
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
*/
|
|
|
|
|
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
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'
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2026-06-10 11:29:05 -07:00
|
|
|
* - 'vector' — JsHnswVectorIndex replacement (vector index engine)
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
* - 'roaring' — RoaringBitmap32 replacement
|
2026-02-01 13:03:15 -08:00
|
|
|
* - 'embeddings' — Embedding engine replacement (single text)
|
|
|
|
|
* - 'embedBatch' — Batch embedding engine (texts[] → vectors[])
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
* - 'distance' — Distance function overrides
|
|
|
|
|
* - 'msgpack' — Msgpack encode/decode
|
feat: add aggregation engine with incremental SUM/COUNT/AVG/MIN/MAX, GROUP BY, and time windows
Add a write-time incremental aggregation engine that maintains running
totals on every add/update/delete for O(1) read performance. Integrates
into brain.find({ aggregate }) for a unified query API.
Core features:
- AggregationIndex with defineAggregate()/removeAggregate() API
- Five aggregation operations: SUM, COUNT, AVG, MIN, MAX
- GROUP BY with multiple dimensions including time windows
- Time window bucketing: hour, day, week, month, quarter, year, custom
- Materialization of results as NounType.Measurement entities
- Debounced persistence of definitions and state to storage
- Definition change detection via FNV-1a hashing with auto-rebuild
- Infinite loop prevention for materialized entities
- 'aggregation' plugin provider key for native acceleration
- Lazy initialization (created on first defineAggregate() call)
- 73 tests (unit + integration) covering all functionality
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:57:53 -08:00
|
|
|
* - 'aggregation' — AggregationIndex replacement (incremental aggregates)
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
*
|
|
|
|
|
* Storage adapter keys:
|
|
|
|
|
* - 'storage:<name>' — Custom storage adapter factory
|
|
|
|
|
*/
|
|
|
|
|
registerProvider(key: string, implementation: unknown): void
|
|
|
|
|
|
|
|
|
|
/** Brainy version for compatibility checks */
|
|
|
|
|
readonly version: string
|
|
|
|
|
}
|
|
|
|
|
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
// ===========================================================================
|
|
|
|
|
// 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`,
|
2026-06-09 13:07:56 -07:00
|
|
|
// `GraphAdjacencyIndex`, `JsHnswVectorIndex`, `EntityIdMapper`, `UnifiedCache`)
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
// 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
|
|
|
|
|
}
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
|
|
|
|
|
/** 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.
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
*/
|
|
|
|
|
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.
|
|
|
|
|
*/
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
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)[]>
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
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>
|
|
|
|
|
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
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>
|
|
|
|
|
}
|
|
|
|
|
|
feat(8.0): generational MVCC storage + Datomic-style Db API (now/transact/asOf/with/persist)
One mechanism replaces the COW and versioning subsystems: immutable
generation-stamped records behind a Datomic-style database value.
Record layer (src/db/generationStore.ts):
- Monotonic generation counter in _system/generation.json; bumped once per
transact() commit and once per single-operation write (storage hook), so
brain.generation() is always a meaningful watermark.
- Commit protocol: stage before-images + tx.json delta -> fsync -> execute
batch via TransactionManager -> atomic tmp+rename of _system/manifest.json
(the rename IS the commit point) -> append _system/tx-log.jsonl.
- Crash recovery on open rolls uncommitted generations back byte-identically
and forces an index rebuild; refcounted pins gate compactHistory(), which
records a horizon (asOf below it throws GenerationCompactedError).
Db API:
- Db: get/find/search/related pinned at a generation, with() speculative
overlays, since() diffs, persist() hard-link-farm snapshots, timestamp,
generation, release() + FinalizationRegistry backstop.
- Brainy: now() O(1) pin, transact(ops, {meta, ifAtGeneration}) atomic batch
(GenerationConflictError CAS), asOf(generation|Date|path), restore(path,
{confirm}), compactHistory(), generation(), static open() + load().
- get()/metadata find()/related() are fully correct at any reachable pinned
generation; index-accelerated queries at historical generations throw
NotYetSupportedAtHistoricalGenerationError - never silently-wrong results.
- VersionedIndexProvider (generation/isGenerationVisible/pin/release) in
plugin.ts: feature-detected, balanced pin/release in lockstep with Db
lifecycle, post-commit applier + replay-gap model documented.
Storage primitives (BaseStorage + filesystem/memory adapters): raw-object
read/write/list/remove, fsync barrier, noun/verb raw before-image capture,
tx-log append, snapshotToDirectory (hard-link farm; byte-copy fallback and
append-in-place exceptions), restoreFromDirectory + derived-state reload.
Proof suite (tests/integration/db-mvcc.test.ts + tests/unit/db/): isolation
across 200 mutations, batch atomicity under injected execution failure,
ifAtGeneration CAS, snapshot immunity to source mutation, compaction safety
under pins, with() overlay isolation, generation monotonicity across
reopen, crash consistency through the real recovery path, and versioned
provider pin/release balance. Design record in
docs/ADR-001-generational-mvcc.md.
2026-06-10 14:14:07 -07:00
|
|
|
/**
|
|
|
|
|
* Optional capability interface for index providers that maintain versioned
|
|
|
|
|
* (generation-aware) internal state — the provider-side half of Brainy 8.0's
|
|
|
|
|
* generational MVCC contract. Implemented by native providers whose storage
|
|
|
|
|
* engines keep immutable versions (e.g. LSM snapshots); Brainy's own JS
|
|
|
|
|
* indexes do not implement it (they are rebuilt from the storage records,
|
|
|
|
|
* which carry the versioning).
|
|
|
|
|
*
|
|
|
|
|
* **Detection.** Brainy feature-detects this interface on every registered
|
|
|
|
|
* index provider (`'vector'`, `'metadataIndex'`, `'graphIndex'`): when a
|
|
|
|
|
* provider exposes `pin`/`release` functions, Brainy calls them in lockstep
|
|
|
|
|
* with `Db` lifecycle — `pin(g)` when a `Db` value pins generation `g`
|
|
|
|
|
* (`brain.now()`, `brain.transact()`, `brain.asOf()`), `release(g)` when that
|
|
|
|
|
* `Db` is released (explicitly or via the GC backstop). Pins are refcounted
|
|
|
|
|
* on the Brainy side; a provider may receive multiple `pin(g)` calls for the
|
|
|
|
|
* same generation and will receive exactly one matching `release(g)` per pin.
|
|
|
|
|
*
|
|
|
|
|
* **Consistency model (locked cross-team design).** Index providers are
|
|
|
|
|
* *post-commit appliers*: the storage-record commit (atomic manifest rename)
|
|
|
|
|
* is the source of truth, and provider index state is derived, applied after
|
|
|
|
|
* the commit point. On open, a provider compares its own persisted
|
|
|
|
|
* `generation()` against the store's committed generation and replays the
|
|
|
|
|
* gap from the storage records (or requests a rebuild) — there are no
|
|
|
|
|
* provider rollback hooks, because an uncommitted transaction is repaired at
|
|
|
|
|
* the storage layer before any index is opened. The explicit pin/release
|
|
|
|
|
* lifetime OVERRIDES any time-based snapshot retention the provider has
|
|
|
|
|
* (e.g. an LSM snapshot TTL): a pinned generation must stay readable until
|
|
|
|
|
* released, regardless of age.
|
|
|
|
|
*
|
|
|
|
|
* **Speculative reads.** `db.with(txData)` overlays are Brainy-side only —
|
|
|
|
|
* providers are never asked to read uncommitted/speculative state; they
|
|
|
|
|
* always serve committed generations.
|
|
|
|
|
*/
|
|
|
|
|
export interface VersionedIndexProvider {
|
|
|
|
|
/**
|
|
|
|
|
* @description The newest generation this provider's persisted index state
|
|
|
|
|
* reflects. Brainy compares it against the storage layer's committed
|
|
|
|
|
* generation on open to detect a replay gap (index behind storage after a
|
|
|
|
|
* crash between commit and index apply).
|
|
|
|
|
* @returns The provider's current generation as a `bigint` (u64 at the
|
|
|
|
|
* native boundary; Brainy's generation counter is a safe integer today).
|
|
|
|
|
*/
|
|
|
|
|
generation(): bigint
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description True ⇒ the provider can serve consistent reads at
|
|
|
|
|
* `generation` (segments retained). Combined with pin: pinning a VISIBLE
|
|
|
|
|
* generation guarantees it stays servable until release. Pinning an
|
|
|
|
|
* invisible generation is permitted (refcount-only) — Brainy serves that
|
|
|
|
|
* generation from canonical storage instead.
|
|
|
|
|
*
|
|
|
|
|
* Brainy consults this at pin time (the read-routing rule: a `Db` at
|
|
|
|
|
* generation `g` uses provider-accelerated reads when
|
|
|
|
|
* `isGenerationVisible(g)` was true at pin time; otherwise canonical
|
|
|
|
|
* generation records).
|
|
|
|
|
* @param generation - The generation a `Db` value is about to pin.
|
|
|
|
|
*/
|
|
|
|
|
isGenerationVisible(generation: bigint): boolean
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description Pin a generation: the provider must keep index state for
|
|
|
|
|
* `generation` readable until the matching {@link VersionedIndexProvider.release}
|
|
|
|
|
* call, overriding any time-based snapshot retention. Called once per
|
|
|
|
|
* Brainy-side pin (refcounted upstream — expect balanced pin/release pairs).
|
|
|
|
|
* @param generation - The generation a live `Db` value just pinned.
|
|
|
|
|
*/
|
|
|
|
|
pin(generation: bigint): void
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description Release one pin on `generation`. After the last release the
|
|
|
|
|
* provider may reclaim resources for that generation at its discretion.
|
|
|
|
|
* @param generation - The generation being released (matches a prior
|
|
|
|
|
* {@link VersionedIndexProvider.pin} call).
|
|
|
|
|
*/
|
|
|
|
|
release(generation: bigint): void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @description Feature-detection guard for {@link VersionedIndexProvider}.
|
|
|
|
|
* Brainy applies it to every registered index provider (vector, metadata,
|
|
|
|
|
* graph) when a `Db` value pins or releases a generation: providers exposing
|
|
|
|
|
* all four capability methods receive lockstep `pin`/`release` calls;
|
|
|
|
|
* everything else (including Brainy's own JS indexes) is skipped.
|
|
|
|
|
*
|
|
|
|
|
* @param candidate - A registered index provider instance.
|
|
|
|
|
* @returns Whether the candidate implements the versioned capability.
|
|
|
|
|
*/
|
|
|
|
|
export function isVersionedIndexProvider(
|
|
|
|
|
candidate: unknown
|
|
|
|
|
): candidate is VersionedIndexProvider {
|
|
|
|
|
if (candidate === null || typeof candidate !== 'object') {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const c = candidate as Record<string, unknown>
|
|
|
|
|
return (
|
|
|
|
|
typeof c.generation === 'function' &&
|
|
|
|
|
typeof c.isGenerationVisible === 'function' &&
|
|
|
|
|
typeof c.pin === 'function' &&
|
|
|
|
|
typeof c.release === 'function'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
/**
|
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).
|
|
|
|
|
*
|
2026-06-10 11:29:05 -07:00
|
|
|
* **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.
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
*/
|
2026-06-09 12:58:26 -07:00
|
|
|
export interface VectorIndexProvider {
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
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'
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 14:29:12 -07:00
|
|
|
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
/**
|
|
|
|
|
* 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
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
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
|
feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.
This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
brainy calls (optional/feature-detected members like HNSW setPersistMode and
enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
types) from the same entrypoint so a plugin author can import the whole
provider surface from one place.
Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.
The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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`).
|
|
|
|
|
*
|
2026-06-09 13:07:56 -07:00
|
|
|
* 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
|
2026-06-09 13:07:56 -07:00
|
|
|
* `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[]
|
|
|
|
|
}
|
|
|
|
|
|
feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)
Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().
Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
}
|
|
|
|
|
}
|