@soulcraft/cortex 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,97 +1,121 @@
1
- # @soulcraft/cortex
1
+ # Cortex
2
2
 
3
- Native Rust acceleration for [Brainy](https://github.com/soulcraftlabs/brainy). Drop-in performance upgrade — zero configuration required.
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/soulcraftlabs/brainy/main/brainy.png" alt="Brainy Logo" width="200">
5
+ </p>
4
6
 
5
- ## Pricing
7
+ <p align="center">
8
+ <strong>Native Rust acceleration for <a href="https://github.com/soulcraftlabs/brainy">Brainy</a></strong>
9
+ </p>
6
10
 
7
- Cortex is a commercial add-on for Brainy. A license key is required after the 14-day trial.
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@soulcraft/cortex"><img src="https://badge.fury.io/js/%40soulcraft%2Fcortex.svg" alt="npm version"></a>
13
+ <a href="https://soulcraft.com/product"><img src="https://img.shields.io/badge/info-soulcraft.com-blue.svg" alt="Product Info"></a>
14
+ </p>
8
15
 
9
- | Plan | Price | For |
10
- |------|-------|-----|
11
- | **Standard** | $199/year | Individuals and small teams |
12
- | **Enterprise** | $999/year | Companies, priority support |
16
+ ---
13
17
 
14
- **[Purchase a license at soulcraft.com/pricing](https://soulcraft.com/pricing?focus=pro)**
18
+ ## What is Cortex?
15
19
 
16
- **14-day free trial** starts automatically on first use, no signup required.
20
+ Cortex replaces Brainy's JavaScript internals with native Rust implementations. Same API, same data, dramatically faster.
17
21
 
18
- ## Installation
22
+ Install it, and Brainy automatically uses Rust for distance calculations, embeddings, metadata queries, graph operations, and more. No code changes needed.
19
23
 
20
24
  ```bash
21
25
  npm install @soulcraft/cortex
22
26
  ```
23
27
 
24
- That's it. Brainy auto-detects cortex during `init()` and uses native implementations where available.
28
+ ```typescript
29
+ import Brainy from '@soulcraft/brainy'
25
30
 
26
- ## License Setup
31
+ const brain = new Brainy()
32
+ await brain.init()
33
+ // Cortex is auto-detected. That's it.
27
34
 
28
- After purchasing, set your license key via environment variable or file:
35
+ // Verify what's accelerated
36
+ const diag = brain.diagnostics()
37
+ console.log(diag.providers)
38
+ // { distance: { source: 'plugin' }, embeddings: { source: 'plugin' }, ... }
39
+ ```
29
40
 
30
- ```bash
31
- # Option 1: Environment variable
32
- export SOULCRAFT_LICENSE=sc_cortex_<your-key>
41
+ ## Why Cortex?
33
42
 
34
- # Option 2: License file
35
- echo "sc_cortex_<your-key>" > ~/.soulcraft/license
36
- ```
43
+ Brainy works great on its own with pure JavaScript and WASM. Cortex is for when you need more:
44
+
45
+ - **SIMD distance calculations** — every vector comparison gets faster
46
+ - **Native ML inference** — Candle engine replaces WASM embeddings, with CPU, CUDA, and Metal support
47
+ - **Rust metadata engine** — queries and mutations run in Rust with CRoaring bitmaps
48
+ - **Native graph index** — 4 LSM-trees with verb tracking, all in Rust
49
+ - **Batch embeddings** — single Rust forward pass for bulk operations
50
+ - **Zero-copy storage** — mmap-backed SSTables for disk-heavy workloads
37
51
 
38
- License validation is offline-onlyno network calls, no telemetry.
52
+ Brainy's plugin system means cortex acceleration flows through every operation `add()`, `find()`, `update()`, `delete()`, `similar()`, VFS path resolution, neural APIs, and everything in between.
39
53
 
40
54
  ## What Gets Accelerated
41
55
 
42
- | Subsystem | Native Implementation | Impact |
43
- |-----------|----------------------|--------|
44
- | Distance functions | SIMD cosine/euclidean/manhattan/dot product | Every vector search uses native distance |
45
- | Vector quantization | SIMD SQ8/SQ4 distance on compressed vectors | 4-8x memory reduction |
46
- | Product quantization | Native PQ codebook training and ADC lookup | 16-32x compression for large datasets |
47
- | Graph compression | Delta-varint encoded connection lists | 2-3x smaller graph storage |
48
- | Metadata index | Full Rust query/mutation engine with bitmap operations | Faster metadata filtering and mutations |
49
- | Graph adjacency | 4 LSM-trees with verb tracking in Rust | Faster relationship queries |
50
- | Embeddings | Candle ML (CPU, CUDA, Metal) | Native inference, no WASM compilation |
51
- | Roaring bitmaps | CRoaring bindings (binary-compatible) | Faster set operations on entity IDs |
52
- | Msgpack | Native encode/decode | Faster serialization |
53
- | Storage | MmapFileSystemStorage with zero-copy SSTables | Faster disk I/O for graph data |
56
+ | Subsystem | What Cortex Provides | Impact |
57
+ |-----------|---------------------|--------|
58
+ | **Distance** | SIMD cosine, euclidean, manhattan, dot product | Every vector comparison |
59
+ | **Embeddings** | Candle ML engine (CPU/CUDA/Metal) | No WASM compilation, GPU support |
60
+ | **Batch embeddings** | Single forward pass for multiple texts | Bulk import and reindex |
61
+ | **Metadata index** | Rust query/mutation engine with CRoaring | Faster filtering and type queries |
62
+ | **Graph adjacency** | 4 LSM-trees with verb tracking | Faster relationship operations |
63
+ | **Entity ID mapper** | Rust O(1) HashMap for UUID-integer mapping | Bitmap index operations |
64
+ | **Vector quantization** | SIMD SQ8/SQ4 distance on compressed vectors | 4-8x memory reduction |
65
+ | **Product quantization** | Native PQ codebook training and ADC | 16-32x compression |
66
+ | **Graph compression** | Delta-varint encoded connections | 2-3x smaller graph storage |
67
+ | **Roaring bitmaps** | CRoaring bindings | Faster set operations |
68
+ | **Msgpack** | Native encode/decode | Faster serialization |
69
+ | **Storage** | MmapFileSystemStorage with zero-copy SSTables | Faster disk I/O |
70
+
71
+ ## Getting Started
72
+
73
+ ### 1. Install
54
74
 
55
- ## Platform Support
75
+ ```bash
76
+ npm install @soulcraft/brainy @soulcraft/cortex
77
+ ```
56
78
 
57
- | Platform | Architecture | Status |
58
- |----------|-------------|--------|
59
- | Linux | x64 (glibc) | Supported |
60
- | Linux | arm64 (glibc) | Supported |
61
- | macOS | arm64 (Apple Silicon) | Supported |
62
- | macOS | x64 (Intel) | Supported |
63
- | Windows | x64 | Supported |
79
+ ### 2. Set up your license
80
+
81
+ ```bash
82
+ # Environment variable
83
+ export SOULCRAFT_LICENSE=sc_cortex_<your-key>
64
84
 
65
- ## Usage
85
+ # Or license file
86
+ echo "sc_cortex_<your-key>" > ~/.soulcraft/license
87
+ ```
88
+
89
+ License validation is offline — no network calls, no telemetry.
90
+
91
+ ### 3. Use Brainy as normal
66
92
 
67
93
  ```typescript
68
94
  import Brainy from '@soulcraft/brainy'
69
95
 
70
- // cortex is auto-detected — no import or config needed
71
96
  const brain = new Brainy({ storage: { type: 'filesystem', rootDirectory: './data' } })
72
97
  await brain.init()
73
-
74
- // Check if native acceleration is active
75
- console.log(brain.getActivePlugins())
76
- // → ['@soulcraft/cortex']
98
+ // [brainy] Plugin activated: @soulcraft/cortex
99
+ // [brainy] Providers: 10/10 native (@soulcraft/cortex)
77
100
  ```
78
101
 
79
- ### Manual Registration
102
+ ### 4. Verify in production
80
103
 
81
- If auto-detection doesn't suit your setup, register the plugin manually:
104
+ Use `requireProviders()` to fail fast if cortex isn't providing expected acceleration:
82
105
 
83
106
  ```typescript
84
- import Brainy from '@soulcraft/brainy'
85
- import cortexPlugin from '@soulcraft/cortex'
107
+ brain.requireProviders(['distance', 'embeddings', 'metadataIndex', 'graphIndex'])
108
+ ```
86
109
 
87
- const brain = new Brainy({ storage: { type: 'memory' } })
88
- brain.use(cortexPlugin)
89
- await brain.init()
110
+ Or inspect the full picture with diagnostics:
111
+
112
+ ```typescript
113
+ console.log(brain.diagnostics())
90
114
  ```
91
115
 
92
- ### MmapFileSystemStorage
116
+ ## MmapFileSystemStorage
93
117
 
94
- When cortex is installed, a new storage adapter becomes available:
118
+ Cortex includes a storage adapter with mmap-backed binary blob support:
95
119
 
96
120
  ```typescript
97
121
  const brain = new Brainy({
@@ -99,17 +123,26 @@ const brain = new Brainy({
99
123
  })
100
124
  ```
101
125
 
102
- This extends the standard FileSystemStorage with binary blob support for zero-copy mmap access to LSM-tree SSTables.
126
+ Zero-copy access to LSM-tree SSTables useful for large graph datasets where disk I/O is the bottleneck.
127
+
128
+ ## Platform Support
129
+
130
+ | Platform | Architecture | Status |
131
+ |----------|-------------|--------|
132
+ | Linux | x64 (glibc) | Supported |
133
+ | Linux | arm64 (glibc) | Supported |
134
+ | macOS | arm64 (Apple Silicon) | Supported |
135
+ | macOS | x64 (Intel) | Supported |
136
+ | Windows | x64 | Supported |
103
137
 
104
138
  ## Requirements
105
139
 
106
140
  - `@soulcraft/brainy` >= 7.0.0
107
141
  - Node.js >= 22 or Bun >= 1.3.0
108
- - Platform: Linux/macOS/Windows (see table above)
109
142
 
110
- ## Direct Access
143
+ ## Direct Native Access
111
144
 
112
- For advanced use cases, you can access native bindings directly:
145
+ For advanced use cases, access the Rust bindings directly:
113
146
 
114
147
  ```typescript
115
148
  import { loadNativeModule, isNativeAvailable } from '@soulcraft/cortex'
@@ -120,6 +153,13 @@ if (isNativeAvailable()) {
120
153
  }
121
154
  ```
122
155
 
156
+ ## Learn More
157
+
158
+ - **[Product info and licensing](https://soulcraft.com/product)**
159
+ - **[Brainy documentation](https://soulcraft.com/docs)**
160
+ - **[Brainy on GitHub](https://github.com/soulcraftlabs/brainy)**
161
+ - **[Plugin development guide](https://github.com/soulcraftlabs/brainy/blob/main/docs/PLUGINS.md)**
162
+
123
163
  ## License
124
164
 
125
165
  Commercial. See [LICENSE](./LICENSE) for details.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * NativeHNSWWrapper — Thin TS wrapper around the Rust HNSW graph engine
3
+ *
4
+ * Bridges brainy's HNSWIndex API to the native Rust NativeHNSWIndex.
5
+ * The Rust engine handles all graph operations (insert, search, remove) in-memory
6
+ * while this wrapper handles async storage I/O for persistence.
7
+ *
8
+ * Key design:
9
+ * - Uses addItemFull() for single-FFI-call inserts (returns node + neighbors + system)
10
+ * - Persistence modes: 'immediate' (safe) or 'deferred' (30-50x faster for cloud)
11
+ * - COW via native fork() (Arc-based copy-on-write in Rust)
12
+ * - rebuild() restores pre-computed graph from storage (O(N) vs O(N log N) rebuild)
13
+ */
14
+ import type { HNSWConfig, HNSWNoun, Vector, VectorDocument, DistanceFunction, StorageAdapter } from '@soulcraft/brainy';
15
+ export declare class NativeHNSWWrapper {
16
+ private native;
17
+ private config;
18
+ private distanceFunction;
19
+ private storage;
20
+ private persistMode;
21
+ private dirtyNodes;
22
+ private dirtySystem;
23
+ private useParallelization;
24
+ private unifiedCache;
25
+ private cowEnabled;
26
+ constructor(config: (Partial<HNSWConfig> & {
27
+ distanceFunction?: DistanceFunction;
28
+ }) | undefined, distanceFunction: DistanceFunction, options?: {
29
+ storage?: StorageAdapter | null;
30
+ persistMode?: 'immediate' | 'deferred';
31
+ });
32
+ addItem(item: VectorDocument): Promise<string>;
33
+ removeItem(id: string): Promise<boolean>;
34
+ search(queryVector: Vector, k?: number, filter?: (id: string) => Promise<boolean>, options?: {
35
+ rerank?: {
36
+ multiplier: number;
37
+ };
38
+ candidateIds?: string[];
39
+ }): Promise<Array<[string, number]>>;
40
+ flush(): Promise<number>;
41
+ getDirtyNodeCount(): number;
42
+ getPersistMode(): 'immediate' | 'deferred';
43
+ rebuild(options?: {
44
+ lazy?: boolean;
45
+ batchSize?: number;
46
+ onProgress?: (loaded: number, total: number) => void;
47
+ }): Promise<void>;
48
+ enableCOW(parent: NativeHNSWWrapper): void;
49
+ setUseParallelization(useParallelization: boolean): void;
50
+ getUseParallelization(): boolean;
51
+ size(): number;
52
+ clear(): void;
53
+ getEntryPointId(): string | null;
54
+ getMaxLevel(): number;
55
+ getDimension(): number | null;
56
+ getConfig(): HNSWConfig;
57
+ getDistanceFunction(): DistanceFunction;
58
+ /**
59
+ * Get all nouns — builds HNSWNoun objects from native data.
60
+ * @deprecated Use getNounsPaginated() instead
61
+ */
62
+ getNouns(): Map<string, HNSWNoun>;
63
+ getNounsPaginated(options?: {
64
+ offset?: number;
65
+ limit?: number;
66
+ filter?: (noun: HNSWNoun) => boolean;
67
+ }): {
68
+ items: Map<string, HNSWNoun>;
69
+ totalCount: number;
70
+ hasMore: boolean;
71
+ };
72
+ getNodesAtLevel(level: number): HNSWNoun[];
73
+ getLevelStats(): Array<{
74
+ level: number;
75
+ nodeCount: number;
76
+ avgConnections: number;
77
+ }>;
78
+ getIndexHealth(): {
79
+ averageConnections: number;
80
+ layerDistribution: number[];
81
+ maxLayer: number;
82
+ totalNodes: number;
83
+ };
84
+ getCacheStats(): {
85
+ cachingStrategy: 'preloaded' | 'on-demand';
86
+ autoDetection: {
87
+ entityCount: number;
88
+ estimatedVectorMemoryMB: number;
89
+ availableCacheMB: number;
90
+ threshold: number;
91
+ rationale: string;
92
+ };
93
+ unifiedCache: {
94
+ totalSize: number;
95
+ maxSize: number;
96
+ utilizationPercent: number;
97
+ itemCount: number;
98
+ hitRatePercent: number;
99
+ totalAccessCount: number;
100
+ };
101
+ hnswCache: {
102
+ vectorsInCache: number;
103
+ cacheKeyPrefix: string;
104
+ estimatedMemoryMB: number;
105
+ };
106
+ fairness: {
107
+ hnswAccessCount: number;
108
+ hnswAccessPercent: number;
109
+ totalAccessCount: number;
110
+ fairnessViolation: boolean;
111
+ };
112
+ recommendations: string[];
113
+ };
114
+ }
115
+ //# sourceMappingURL=NativeHNSWWrapper.d.ts.map
@@ -0,0 +1,515 @@
1
+ /**
2
+ * NativeHNSWWrapper — Thin TS wrapper around the Rust HNSW graph engine
3
+ *
4
+ * Bridges brainy's HNSWIndex API to the native Rust NativeHNSWIndex.
5
+ * The Rust engine handles all graph operations (insert, search, remove) in-memory
6
+ * while this wrapper handles async storage I/O for persistence.
7
+ *
8
+ * Key design:
9
+ * - Uses addItemFull() for single-FFI-call inserts (returns node + neighbors + system)
10
+ * - Persistence modes: 'immediate' (safe) or 'deferred' (30-50x faster for cloud)
11
+ * - COW via native fork() (Arc-based copy-on-write in Rust)
12
+ * - rebuild() restores pre-computed graph from storage (O(N) vs O(N log N) rebuild)
13
+ */
14
+ import { loadNativeModule } from '../native/index.js';
15
+ import { getGlobalCache, prodLog } from '@soulcraft/brainy/internals';
16
+ const DEFAULT_CONFIG = {
17
+ M: 16,
18
+ efConstruction: 200,
19
+ efSearch: 50,
20
+ ml: 16,
21
+ };
22
+ export class NativeHNSWWrapper {
23
+ native;
24
+ config;
25
+ distanceFunction;
26
+ storage;
27
+ persistMode;
28
+ dirtyNodes = new Set();
29
+ dirtySystem = false;
30
+ useParallelization = true;
31
+ unifiedCache;
32
+ // COW support
33
+ cowEnabled = false;
34
+ constructor(config = {}, distanceFunction, options = {}) {
35
+ this.config = { ...DEFAULT_CONFIG, ...config };
36
+ this.distanceFunction = distanceFunction;
37
+ this.storage = options.storage || null;
38
+ this.persistMode = options.persistMode || 'immediate';
39
+ this.unifiedCache = getGlobalCache();
40
+ const bindings = loadNativeModule();
41
+ const nativeConfig = {
42
+ m: this.config.M,
43
+ efConstruction: this.config.efConstruction,
44
+ efSearch: this.config.efSearch,
45
+ ml: this.config.ml,
46
+ };
47
+ this.native = new bindings.NativeHNSWIndex(nativeConfig);
48
+ prodLog.info(`NativeHNSWWrapper initialized (M=${this.config.M}, ef=${this.config.efSearch}, ` +
49
+ `persist=${this.persistMode})`);
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Core CRUD
53
+ // ---------------------------------------------------------------------------
54
+ async addItem(item) {
55
+ if (!item)
56
+ throw new Error('Item is undefined or null');
57
+ if (!item.vector)
58
+ throw new Error('Vector is undefined or null');
59
+ const { id, vector } = item;
60
+ // Use addItemFull — single FFI call returns all persistence data
61
+ const result = this.native.addItemFull(id, vector);
62
+ if (this.persistMode === 'immediate' && this.storage) {
63
+ // Persist new node
64
+ await this.storage.saveHNSWData(result.id, result.nodeData).catch((e) => {
65
+ console.error(`[HNSW native] Failed to persist node ${result.id}:`, e);
66
+ });
67
+ // Persist modified neighbors
68
+ const neighborPromises = result.modifiedNeighbors.map(neighbor => this.storage.saveHNSWData(neighbor.id, {
69
+ level: neighbor.level,
70
+ connections: neighbor.connections,
71
+ }).catch((e) => {
72
+ console.error(`[HNSW native] Failed to persist neighbor ${neighbor.id}:`, e);
73
+ }));
74
+ await Promise.allSettled(neighborPromises);
75
+ // Persist system data
76
+ await this.storage.saveHNSWSystem(result.systemData).catch((e) => {
77
+ console.error('[HNSW native] Failed to persist system data:', e);
78
+ });
79
+ }
80
+ else if (this.persistMode === 'deferred') {
81
+ this.dirtyNodes.add(result.id);
82
+ for (const neighbor of result.modifiedNeighbors) {
83
+ this.dirtyNodes.add(neighbor.id);
84
+ }
85
+ this.dirtySystem = true;
86
+ }
87
+ return result.id;
88
+ }
89
+ async removeItem(id) {
90
+ if (!this.native.hasNode(id))
91
+ return false;
92
+ // Get neighbors before removal (they'll be modified)
93
+ const nodeData = this.native.getNodeData(id);
94
+ const neighborIds = new Set();
95
+ if (nodeData) {
96
+ for (const ids of Object.values(nodeData.connections)) {
97
+ for (const nid of ids) {
98
+ neighborIds.add(nid);
99
+ }
100
+ }
101
+ }
102
+ const removed = this.native.removeItem(id);
103
+ if (!removed)
104
+ return false;
105
+ if (this.persistMode === 'immediate' && this.storage) {
106
+ // Persist updated neighbors (their connections changed)
107
+ const promises = Array.from(neighborIds).map(async (nid) => {
108
+ const nData = this.native.getNodeData(nid);
109
+ if (nData) {
110
+ await this.storage.saveHNSWData(nid, nData).catch((e) => {
111
+ console.error(`[HNSW native] Failed to persist neighbor ${nid} after removal:`, e);
112
+ });
113
+ }
114
+ });
115
+ await Promise.allSettled(promises);
116
+ // Persist system data (entry point may have changed)
117
+ const sysData = this.native.getSystemData();
118
+ await this.storage.saveHNSWSystem(sysData).catch((e) => {
119
+ console.error('[HNSW native] Failed to persist system data after removal:', e);
120
+ });
121
+ }
122
+ else if (this.persistMode === 'deferred') {
123
+ for (const nid of neighborIds) {
124
+ this.dirtyNodes.add(nid);
125
+ }
126
+ // Remove the deleted node from dirty set (no need to persist it)
127
+ this.dirtyNodes.delete(id);
128
+ this.dirtySystem = true;
129
+ }
130
+ return true;
131
+ }
132
+ async search(queryVector, k = 10, filter, options) {
133
+ if (this.native.size() === 0)
134
+ return [];
135
+ if (!queryVector)
136
+ throw new Error('Query vector is undefined or null');
137
+ let results;
138
+ if (options?.candidateIds && options.candidateIds.length > 0) {
139
+ // Pre-filtered search: use native searchWithCandidates for optimal performance
140
+ // The Rust engine only traverses candidate nodes, avoiding unnecessary distance calculations
141
+ results = this.native.searchWithCandidates(queryVector, k, this.config.efSearch, options.candidateIds);
142
+ }
143
+ else if (filter) {
144
+ // Fallback: search with larger k and post-filter asynchronously
145
+ const overRetrieve = k * 5;
146
+ const rawResults = this.native.search(queryVector, overRetrieve, this.config.efSearch);
147
+ const filtered = [];
148
+ for (const r of rawResults) {
149
+ if (await filter(r.id)) {
150
+ filtered.push(r);
151
+ if (filtered.length >= k)
152
+ break;
153
+ }
154
+ }
155
+ results = filtered;
156
+ }
157
+ else {
158
+ results = this.native.search(queryVector, k, this.config.efSearch);
159
+ }
160
+ // Convert NativeSearchResult[] to [string, number][] tuples
161
+ return results.map(r => [r.id, r.distance]);
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Persistence
165
+ // ---------------------------------------------------------------------------
166
+ async flush() {
167
+ if (!this.storage)
168
+ return 0;
169
+ if (this.dirtyNodes.size === 0 && !this.dirtySystem)
170
+ return 0;
171
+ const startTime = Date.now();
172
+ const nodeCount = this.dirtyNodes.size;
173
+ // Batch persist dirty nodes
174
+ if (this.dirtyNodes.size > 0) {
175
+ const batchSize = 50;
176
+ const nodeIds = Array.from(this.dirtyNodes);
177
+ for (let i = 0; i < nodeIds.length; i += batchSize) {
178
+ const batch = nodeIds.slice(i, i + batchSize);
179
+ const promises = batch.map(nodeId => {
180
+ const nodeData = this.native.getNodeData(nodeId);
181
+ if (!nodeData)
182
+ return Promise.resolve(); // Node was deleted
183
+ return this.storage.saveHNSWData(nodeId, nodeData).catch((e) => {
184
+ console.error(`[HNSW native flush] Failed to persist node ${nodeId}:`, e);
185
+ });
186
+ });
187
+ await Promise.allSettled(promises);
188
+ }
189
+ this.dirtyNodes.clear();
190
+ }
191
+ // Persist system data
192
+ if (this.dirtySystem) {
193
+ const sysData = this.native.getSystemData();
194
+ await this.storage.saveHNSWSystem(sysData).catch((e) => {
195
+ console.error('[HNSW native flush] Failed to persist system data:', e);
196
+ });
197
+ this.dirtySystem = false;
198
+ }
199
+ const duration = Date.now() - startTime;
200
+ if (nodeCount > 0) {
201
+ prodLog.info(`[HNSW native] Flushed ${nodeCount} dirty nodes in ${duration}ms`);
202
+ }
203
+ return nodeCount;
204
+ }
205
+ getDirtyNodeCount() {
206
+ return this.dirtyNodes.size;
207
+ }
208
+ getPersistMode() {
209
+ return this.persistMode;
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Rebuild from storage
213
+ // ---------------------------------------------------------------------------
214
+ async rebuild(options = {}) {
215
+ if (!this.storage) {
216
+ prodLog.warn('NativeHNSWWrapper rebuild skipped: no storage adapter configured');
217
+ return;
218
+ }
219
+ const batchSize = options.batchSize || 1000;
220
+ try {
221
+ // Step 1: Clear native index
222
+ this.native.clear();
223
+ // Step 2: Load system data
224
+ const systemData = await this.storage.getHNSWSystem();
225
+ if (systemData && systemData.entryPointId) {
226
+ this.native.setSystemData(systemData.entryPointId, systemData.maxLevel);
227
+ }
228
+ // Step 3: Load nouns from storage and restore into native engine
229
+ const storageType = this.storage?.constructor.name || '';
230
+ const isLocalStorage = storageType === 'FileSystemStorage' ||
231
+ storageType === 'MemoryStorage' ||
232
+ storageType === 'OPFSStorage' ||
233
+ storageType === 'MmapFileSystemStorage';
234
+ let loadedCount = 0;
235
+ let totalCount;
236
+ if (isLocalStorage) {
237
+ // Local: load all at once
238
+ const result = await this.storage.getNounsWithPagination({
239
+ limit: 10000000
240
+ });
241
+ totalCount = result.totalCount || result.items.length;
242
+ for (const nounData of result.items) {
243
+ try {
244
+ const hnswData = await this.storage.getHNSWData(nounData.id);
245
+ if (!hnswData)
246
+ continue;
247
+ this.native.restoreNode(nounData.id, nounData.vector, hnswData.level, hnswData.connections);
248
+ loadedCount++;
249
+ }
250
+ catch (error) {
251
+ console.error(`Failed to rebuild HNSW data for ${nounData.id}:`, error);
252
+ }
253
+ }
254
+ if (options.onProgress && totalCount !== undefined) {
255
+ options.onProgress(loadedCount, totalCount);
256
+ }
257
+ prodLog.info(`NativeHNSW: Loaded ${loadedCount.toLocaleString()} nodes at once (local storage)`);
258
+ }
259
+ else {
260
+ // Cloud: paginated loading
261
+ let hasMore = true;
262
+ let offset = 0;
263
+ while (hasMore) {
264
+ const result = await this.storage.getNounsWithPagination({
265
+ limit: batchSize,
266
+ offset,
267
+ });
268
+ if (totalCount === undefined && result.totalCount !== undefined) {
269
+ totalCount = result.totalCount;
270
+ }
271
+ for (const nounData of result.items) {
272
+ try {
273
+ const hnswData = await this.storage.getHNSWData(nounData.id);
274
+ if (!hnswData)
275
+ continue;
276
+ this.native.restoreNode(nounData.id, nounData.vector, hnswData.level, hnswData.connections);
277
+ loadedCount++;
278
+ }
279
+ catch (error) {
280
+ console.error(`Failed to rebuild HNSW data for ${nounData.id}:`, error);
281
+ }
282
+ }
283
+ if (options.onProgress && totalCount !== undefined) {
284
+ options.onProgress(loadedCount, totalCount);
285
+ }
286
+ hasMore = result.hasMore;
287
+ offset += batchSize;
288
+ }
289
+ }
290
+ // Step 4: Recover entry point if needed
291
+ if (this.native.size() > 0 && !this.native.getEntryPointId()) {
292
+ // Find highest level node from native
293
+ const allIds = this.native.getAllIds();
294
+ let bestId = null;
295
+ let bestLevel = -1;
296
+ for (const nid of allIds) {
297
+ const nd = this.native.getNodeData(nid);
298
+ if (nd && nd.level > bestLevel) {
299
+ bestLevel = nd.level;
300
+ bestId = nid;
301
+ }
302
+ }
303
+ if (bestId) {
304
+ this.native.setSystemData(bestId, bestLevel);
305
+ prodLog.info(`NativeHNSW entry point recovered: ${bestId} at level ${bestLevel}`);
306
+ }
307
+ }
308
+ prodLog.info(`NativeHNSW index rebuilt: ${loadedCount.toLocaleString()} entities, ` +
309
+ `${this.native.getMaxLevel() + 1} levels, ` +
310
+ `entry point: ${this.native.getEntryPointId() || 'none'}`);
311
+ }
312
+ catch (error) {
313
+ prodLog.error('NativeHNSW rebuild failed:', error);
314
+ throw new Error(`Failed to rebuild NativeHNSW index: ${error}`);
315
+ }
316
+ }
317
+ // ---------------------------------------------------------------------------
318
+ // COW
319
+ // ---------------------------------------------------------------------------
320
+ enableCOW(parent) {
321
+ this.cowEnabled = true;
322
+ this.native = parent.native.fork();
323
+ this.config = parent.config;
324
+ this.distanceFunction = parent.distanceFunction;
325
+ this.useParallelization = parent.useParallelization;
326
+ this.unifiedCache = parent.unifiedCache;
327
+ prodLog.info(`NativeHNSW COW enabled: ${this.native.size()} nodes (Arc-based fork)`);
328
+ }
329
+ setUseParallelization(useParallelization) {
330
+ this.useParallelization = useParallelization;
331
+ }
332
+ getUseParallelization() {
333
+ return this.useParallelization;
334
+ }
335
+ // ---------------------------------------------------------------------------
336
+ // Info / Introspection
337
+ // ---------------------------------------------------------------------------
338
+ size() {
339
+ return this.native.size();
340
+ }
341
+ clear() {
342
+ this.native.clear();
343
+ this.dirtyNodes.clear();
344
+ this.dirtySystem = false;
345
+ }
346
+ getEntryPointId() {
347
+ return this.native.getEntryPointId() || null;
348
+ }
349
+ getMaxLevel() {
350
+ return this.native.getMaxLevel();
351
+ }
352
+ getDimension() {
353
+ return this.native.getDimension() ?? null;
354
+ }
355
+ getConfig() {
356
+ return { ...this.config };
357
+ }
358
+ getDistanceFunction() {
359
+ return this.distanceFunction;
360
+ }
361
+ /**
362
+ * Get all nouns — builds HNSWNoun objects from native data.
363
+ * @deprecated Use getNounsPaginated() instead
364
+ */
365
+ getNouns() {
366
+ const result = new Map();
367
+ const allIds = this.native.getAllIds();
368
+ for (const id of allIds) {
369
+ const nodeData = this.native.getNodeData(id);
370
+ const vector = this.native.getVector(id);
371
+ if (!nodeData || !vector)
372
+ continue;
373
+ const connections = new Map();
374
+ for (const [levelStr, ids] of Object.entries(nodeData.connections)) {
375
+ connections.set(parseInt(levelStr, 10), new Set(ids));
376
+ }
377
+ result.set(id, {
378
+ id,
379
+ vector,
380
+ connections,
381
+ level: nodeData.level,
382
+ });
383
+ }
384
+ return result;
385
+ }
386
+ getNounsPaginated(options = {}) {
387
+ const offset = options.offset || 0;
388
+ const limit = options.limit || 100;
389
+ const allIds = this.native.getAllIds();
390
+ const totalCount = allIds.length;
391
+ // Build noun objects for the requested page
392
+ const items = new Map();
393
+ let added = 0;
394
+ let skipped = 0;
395
+ for (const id of allIds) {
396
+ const nodeData = this.native.getNodeData(id);
397
+ const vector = this.native.getVector(id);
398
+ if (!nodeData || !vector)
399
+ continue;
400
+ const connections = new Map();
401
+ for (const [levelStr, ids] of Object.entries(nodeData.connections)) {
402
+ connections.set(parseInt(levelStr, 10), new Set(ids));
403
+ }
404
+ const noun = { id, vector, connections, level: nodeData.level };
405
+ if (options.filter && !options.filter(noun))
406
+ continue;
407
+ if (skipped < offset) {
408
+ skipped++;
409
+ continue;
410
+ }
411
+ items.set(id, noun);
412
+ added++;
413
+ if (added >= limit)
414
+ break;
415
+ }
416
+ return {
417
+ items,
418
+ totalCount,
419
+ hasMore: offset + limit < totalCount,
420
+ };
421
+ }
422
+ getNodesAtLevel(level) {
423
+ const ids = this.native.getNodesAtLevel(level);
424
+ const nodes = [];
425
+ for (const id of ids) {
426
+ const nodeData = this.native.getNodeData(id);
427
+ const vector = this.native.getVector(id);
428
+ if (!nodeData || !vector)
429
+ continue;
430
+ const connections = new Map();
431
+ for (const [levelStr, nids] of Object.entries(nodeData.connections)) {
432
+ connections.set(parseInt(levelStr, 10), new Set(nids));
433
+ }
434
+ nodes.push({ id, vector, connections, level: nodeData.level });
435
+ }
436
+ return nodes;
437
+ }
438
+ getLevelStats() {
439
+ const nativeStats = this.native.getLevelStats();
440
+ return nativeStats.map(s => ({
441
+ level: s.level,
442
+ nodeCount: s.nodeCount,
443
+ avgConnections: s.avgConnections,
444
+ }));
445
+ }
446
+ getIndexHealth() {
447
+ const health = this.native.getIndexHealth();
448
+ return {
449
+ averageConnections: health.averageConnections,
450
+ layerDistribution: health.layerDistribution,
451
+ maxLayer: health.maxLayer,
452
+ totalNodes: health.totalNodes,
453
+ };
454
+ }
455
+ getCacheStats() {
456
+ const cacheStats = this.unifiedCache.getStats();
457
+ const entityCount = this.native.size();
458
+ const vectorDimension = this.native.getDimension() || 384;
459
+ const bytesPerVector = vectorDimension * 4;
460
+ const estimatedVectorMemoryMB = (entityCount * bytesPerVector) / (1024 * 1024);
461
+ const availableCacheMB = (cacheStats.maxSize * 0.8) / (1024 * 1024);
462
+ const vectorsInCache = cacheStats.typeCounts.hnsw || 0;
463
+ const hnswMemoryBytes = cacheStats.typeSizes.hnsw || 0;
464
+ const hnswAccessCount = cacheStats.typeAccessCounts.hnsw || 0;
465
+ const totalAccessCount = cacheStats.totalAccessCount;
466
+ const hnswAccessPercent = totalAccessCount > 0 ? (hnswAccessCount / totalAccessCount) * 100 : 0;
467
+ const hnswCachePercent = cacheStats.maxSize > 0 ? (hnswMemoryBytes / cacheStats.maxSize) * 100 : 0;
468
+ const fairnessViolation = hnswCachePercent > 90 && hnswAccessPercent < 10;
469
+ const hitRatePercent = (cacheStats.hitRate * 100) || 0;
470
+ const cachingStrategy = estimatedVectorMemoryMB < availableCacheMB ? 'preloaded' : 'on-demand';
471
+ const recommendations = [];
472
+ if (cachingStrategy === 'on-demand' && hitRatePercent < 50) {
473
+ recommendations.push(`Low cache hit rate (${hitRatePercent.toFixed(1)}%). Consider increasing UnifiedCache size`);
474
+ }
475
+ if (fairnessViolation) {
476
+ recommendations.push(`Fairness violation: HNSW using ${hnswCachePercent.toFixed(1)}% cache with only ${hnswAccessPercent.toFixed(1)}% access`);
477
+ }
478
+ if (recommendations.length === 0) {
479
+ recommendations.push('All metrics healthy - no action needed');
480
+ }
481
+ return {
482
+ cachingStrategy,
483
+ autoDetection: {
484
+ entityCount,
485
+ estimatedVectorMemoryMB: parseFloat(estimatedVectorMemoryMB.toFixed(2)),
486
+ availableCacheMB: parseFloat(availableCacheMB.toFixed(2)),
487
+ threshold: 0.8,
488
+ rationale: cachingStrategy === 'preloaded'
489
+ ? `Vectors in native Rust memory (${estimatedVectorMemoryMB.toFixed(1)}MB)`
490
+ : `Adaptive on-demand loading (${estimatedVectorMemoryMB.toFixed(1)}MB > ${availableCacheMB.toFixed(1)}MB threshold)`
491
+ },
492
+ unifiedCache: {
493
+ totalSize: cacheStats.totalSize,
494
+ maxSize: cacheStats.maxSize,
495
+ utilizationPercent: parseFloat((cacheStats.utilization * 100).toFixed(2)),
496
+ itemCount: cacheStats.itemCount,
497
+ hitRatePercent: parseFloat(hitRatePercent.toFixed(2)),
498
+ totalAccessCount: cacheStats.totalAccessCount,
499
+ },
500
+ hnswCache: {
501
+ vectorsInCache,
502
+ cacheKeyPrefix: 'hnsw:vector:',
503
+ estimatedMemoryMB: parseFloat((hnswMemoryBytes / (1024 * 1024)).toFixed(2)),
504
+ },
505
+ fairness: {
506
+ hnswAccessCount,
507
+ hnswAccessPercent: parseFloat(hnswAccessPercent.toFixed(2)),
508
+ totalAccessCount,
509
+ fairnessViolation,
510
+ },
511
+ recommendations,
512
+ };
513
+ }
514
+ }
515
+ //# sourceMappingURL=NativeHNSWWrapper.js.map
package/dist/plugin.d.ts CHANGED
@@ -8,15 +8,17 @@
8
8
  * can always access their data. Compute acceleration requires a valid license.
9
9
  *
10
10
  * Provider registration order:
11
- * 1. storage:mmap-filesystem — ALWAYS registered (data access)
11
+ * 1. storage:filesystem — mmap-enhanced FileSystemStorage (ALWAYS registered)
12
12
  * 2. distance — SIMD-accelerated cosine (licensed)
13
- * 3. metadataIndex — Native Rust query/mutation engine (licensed)
14
- * 4. graphIndex — Native 4 LSM-trees with verb tracking (licensed)
15
- * 5. embeddings Candle ML native engine (CPU/CUDA/Metal) (licensed)
16
- * 6. embedBatch — Native batch embedding (single forward pass) (licensed)
17
- * 7. entityIdMapper Native UUID integer mapping (licensed)
18
- * 8. roaring CRoaring bitmap bindings (licensed)
19
- * 9. msgpack — Native encode/decode (licensed)
13
+ * 3. hnsw — Native Rust HNSW graph engine (licensed)
14
+ * 4. cache — Native Rust eviction engine with JS data storage (licensed)
15
+ * 5. metadataIndex Native Rust query/mutation engine (licensed)
16
+ * 6. graphIndex — Native 4 LSM-trees with verb tracking (licensed)
17
+ * 7. embeddings Candle ML native engine (CPU/CUDA/Metal) (licensed)
18
+ * 8. embedBatch Native batch embedding (single forward pass) (licensed)
19
+ * 9. entityIdMapper — Native UUID ↔ integer mapping (licensed)
20
+ * 10. roaring — CRoaring bitmap bindings (licensed)
21
+ * 11. msgpack — Native encode/decode (licensed)
20
22
  */
21
23
  import type { BrainyPlugin } from '@soulcraft/brainy/plugin';
22
24
  declare const cortexPlugin: BrainyPlugin;
package/dist/plugin.js CHANGED
@@ -8,27 +8,33 @@
8
8
  * can always access their data. Compute acceleration requires a valid license.
9
9
  *
10
10
  * Provider registration order:
11
- * 1. storage:mmap-filesystem — ALWAYS registered (data access)
11
+ * 1. storage:filesystem — mmap-enhanced FileSystemStorage (ALWAYS registered)
12
12
  * 2. distance — SIMD-accelerated cosine (licensed)
13
- * 3. metadataIndex — Native Rust query/mutation engine (licensed)
14
- * 4. graphIndex — Native 4 LSM-trees with verb tracking (licensed)
15
- * 5. embeddings Candle ML native engine (CPU/CUDA/Metal) (licensed)
16
- * 6. embedBatch — Native batch embedding (single forward pass) (licensed)
17
- * 7. entityIdMapper Native UUID integer mapping (licensed)
18
- * 8. roaring CRoaring bitmap bindings (licensed)
19
- * 9. msgpack — Native encode/decode (licensed)
13
+ * 3. hnsw — Native Rust HNSW graph engine (licensed)
14
+ * 4. cache — Native Rust eviction engine with JS data storage (licensed)
15
+ * 5. metadataIndex Native Rust query/mutation engine (licensed)
16
+ * 6. graphIndex — Native 4 LSM-trees with verb tracking (licensed)
17
+ * 7. embeddings Candle ML native engine (CPU/CUDA/Metal) (licensed)
18
+ * 8. embedBatch Native batch embedding (single forward pass) (licensed)
19
+ * 9. entityIdMapper — Native UUID ↔ integer mapping (licensed)
20
+ * 10. roaring — CRoaring bitmap bindings (licensed)
21
+ * 11. msgpack — Native encode/decode (licensed)
20
22
  */
21
23
  import { loadNativeModule, isNativeAvailable } from './native/index.js';
22
24
  import { validateLicense } from './license.js';
23
25
  const cortexPlugin = {
24
26
  name: '@soulcraft/cortex',
25
27
  async activate(context) {
26
- // Storage adapters are ALWAYS registered users must always be able
27
- // to access their data, even if the license expires. Brainy falls back
28
- // to JS compute but needs the storage adapter to read the files.
28
+ // Storage: Override built-in FileSystemStorage with mmap-enhanced version.
29
+ // MmapFileSystemStorage extends FileSystemStorage, adding binary blob methods
30
+ // (saveBinaryBlob/loadBinaryBlob/getBinaryBlobPath) that enable Rust native
31
+ // indexes to mmap SSTable files directly. Zero-config — users with
32
+ // type: 'filesystem' automatically get the enhanced version.
33
+ //
34
+ // ALWAYS registered (even without a license) so users can always access data.
29
35
  const { MmapFileSystemStorage } = await import('./storage/mmapFileSystemStorage.js');
30
- context.registerProvider('storage:mmap-filesystem', {
31
- name: 'mmap-filesystem',
36
+ context.registerProvider('storage:filesystem', {
37
+ name: 'filesystem',
32
38
  create: (config) => new MmapFileSystemStorage(config.rootDirectory, {
33
39
  compression: config.compression,
34
40
  compressionLevel: config.compressionLevel
@@ -72,6 +78,15 @@ const cortexPlugin = {
72
78
  });
73
79
  // Product Quantization: 16-32x compression for large datasets
74
80
  context.registerProvider('quantization:pq', native.NativePqCodebook);
81
+ // HNSW: Native Rust graph engine with SIMD distance and Arc-based COW
82
+ const { NativeHNSWWrapper } = await import('./hnsw/NativeHNSWWrapper.js');
83
+ context.registerProvider('hnsw', (config, distanceFn, options) => {
84
+ return new NativeHNSWWrapper(config, distanceFn, options);
85
+ });
86
+ // Cache: Native Rust eviction engine with JS data storage
87
+ // Registered before indexes so they use native cache for memory management
88
+ const { NativeUnifiedCacheWrapper } = await import('./utils/NativeUnifiedCache.js');
89
+ context.registerProvider('cache', new NativeUnifiedCacheWrapper());
75
90
  // Metadata index: Native Rust query/mutation engine
76
91
  const { MetadataIndexManager: NativeMetadataIndex } = await import('./utils/NativeMetadataIndex.js');
77
92
  context.registerProvider('metadataIndex', (storage) => new NativeMetadataIndex(storage));
@@ -879,7 +879,7 @@ export class MetadataIndexManager {
879
879
  catch { }
880
880
  // Adaptive rebuild strategy
881
881
  const storageType = this.storage.constructor.name;
882
- const isLocalStorage = storageType === 'FileSystemStorage' || storageType === 'MemoryStorage';
882
+ const isLocalStorage = storageType === 'FileSystemStorage' || storageType === 'MmapFileSystemStorage' || storageType === 'MemoryStorage';
883
883
  let totalNounsProcessed = 0;
884
884
  if (isLocalStorage) {
885
885
  const result = await this.storage.getNouns({
@@ -0,0 +1,81 @@
1
+ /**
2
+ * NativeUnifiedCacheWrapper — Native Rust eviction engine with JS data storage
3
+ *
4
+ * The Rust NativeUnifiedCache manages eviction metadata (key, type, size, cost,
5
+ * access counts) but does NOT store actual JS data. This wrapper:
6
+ * - Keeps a Map<string, any> for actual cached data
7
+ * - Delegates eviction decisions to the native engine
8
+ * - Implements request coalescing in JS (same as brainy's UnifiedCache)
9
+ * - Drop-in replacement for brainy's UnifiedCache
10
+ */
11
+ import type { UnifiedCacheConfig } from '@soulcraft/brainy/internals';
12
+ export declare class NativeUnifiedCacheWrapper {
13
+ private data;
14
+ private native;
15
+ private loadingPromises;
16
+ private readonly maxSize;
17
+ private readonly config;
18
+ private readonly memoryInfo;
19
+ private readonly allocationStrategy;
20
+ private memoryPressureCheckTimer;
21
+ private lastMemoryWarning;
22
+ constructor(config?: UnifiedCacheConfig);
23
+ get(key: string, loadFn?: () => Promise<any>): Promise<any>;
24
+ getSync(key: string): any | undefined;
25
+ set(key: string, data: any, type: 'hnsw' | 'metadata' | 'embedding' | 'other', size: number, rebuildCost?: number): void;
26
+ delete(key: string): boolean;
27
+ deleteByPrefix(prefix: string): number;
28
+ clear(type?: 'hnsw' | 'metadata' | 'embedding' | 'other'): void;
29
+ evictForSize(bytesNeeded: number): boolean;
30
+ private startFairnessMonitor;
31
+ private checkFairness;
32
+ private startMemoryPressureMonitor;
33
+ private checkMemoryPressure;
34
+ getStats(): {
35
+ totalSize: number;
36
+ maxSize: number;
37
+ utilization: number;
38
+ itemCount: number;
39
+ typeSizes: Record<string, number>;
40
+ typeCounts: Record<string, number>;
41
+ typeAccessCounts: Record<string, number>;
42
+ totalAccessCount: number;
43
+ hitRate: number;
44
+ memory: {
45
+ available: number;
46
+ source: "cgroup-v2" | "cgroup-v1" | "system" | "fallback";
47
+ isContainer: boolean;
48
+ systemTotal: number;
49
+ allocationRatio: number;
50
+ environment: "production" | "development" | "container" | "unknown";
51
+ };
52
+ };
53
+ getMemoryInfo(): {
54
+ memoryInfo: {
55
+ available: number;
56
+ source: "cgroup-v2" | "cgroup-v1" | "system" | "fallback";
57
+ isContainer: boolean;
58
+ systemTotal: number;
59
+ free: number;
60
+ warnings: string[];
61
+ };
62
+ allocationStrategy: {
63
+ cacheSize: number;
64
+ ratio: number;
65
+ minSize: number;
66
+ maxSize: number | null;
67
+ environment: "production" | "development" | "container" | "unknown";
68
+ modelMemory: number;
69
+ modelPrecision: "q8" | "fp32";
70
+ availableForCache: number;
71
+ reasoning: string;
72
+ };
73
+ currentPressure: {
74
+ pressure: "none" | "moderate" | "high" | "critical";
75
+ warnings: string[];
76
+ };
77
+ };
78
+ saveAccessPatterns(): Promise<any>;
79
+ loadAccessPatterns(patterns: any): Promise<void>;
80
+ }
81
+ //# sourceMappingURL=NativeUnifiedCache.d.ts.map
@@ -0,0 +1,268 @@
1
+ /**
2
+ * NativeUnifiedCacheWrapper — Native Rust eviction engine with JS data storage
3
+ *
4
+ * The Rust NativeUnifiedCache manages eviction metadata (key, type, size, cost,
5
+ * access counts) but does NOT store actual JS data. This wrapper:
6
+ * - Keeps a Map<string, any> for actual cached data
7
+ * - Delegates eviction decisions to the native engine
8
+ * - Implements request coalescing in JS (same as brainy's UnifiedCache)
9
+ * - Drop-in replacement for brainy's UnifiedCache
10
+ */
11
+ import { loadNativeModule } from '../native/index.js';
12
+ import { prodLog } from '@soulcraft/brainy/internals';
13
+ import { getRecommendedCacheConfig, formatBytes, checkMemoryPressure, } from '@soulcraft/brainy/internals';
14
+ // Cache type enum matching Rust (0-3)
15
+ const CACHE_TYPE_MAP = {
16
+ hnsw: 0,
17
+ metadata: 1,
18
+ embedding: 2,
19
+ other: 3,
20
+ };
21
+ export class NativeUnifiedCacheWrapper {
22
+ data = new Map();
23
+ native;
24
+ loadingPromises = new Map();
25
+ maxSize;
26
+ config;
27
+ memoryInfo;
28
+ allocationStrategy;
29
+ memoryPressureCheckTimer = null;
30
+ lastMemoryWarning = 0;
31
+ constructor(config = {}) {
32
+ const recommendation = getRecommendedCacheConfig({
33
+ manualSize: config.maxSize,
34
+ minSize: config.minSize,
35
+ developmentMode: config.developmentMode,
36
+ });
37
+ this.memoryInfo = recommendation.memoryInfo;
38
+ this.allocationStrategy = recommendation.allocation;
39
+ this.maxSize = recommendation.allocation.cacheSize;
40
+ prodLog.info(`NativeUnifiedCache initialized: ${formatBytes(this.maxSize)} ` +
41
+ `(${this.allocationStrategy.environment} mode, ` +
42
+ `${(this.allocationStrategy.ratio * 100).toFixed(0)}% of ${formatBytes(this.allocationStrategy.availableForCache)} ` +
43
+ `after ${formatBytes(this.allocationStrategy.modelMemory)} ${this.allocationStrategy.modelPrecision.toUpperCase()} model)`);
44
+ for (const warning of recommendation.warnings) {
45
+ prodLog.warn(`NativeUnifiedCache: ${warning}`);
46
+ }
47
+ this.config = {
48
+ enableRequestCoalescing: true,
49
+ enableFairnessCheck: true,
50
+ fairnessCheckInterval: 30000,
51
+ persistPatterns: true,
52
+ enableMemoryMonitoring: true,
53
+ memoryCheckInterval: 30000,
54
+ ...config,
55
+ };
56
+ const bindings = loadNativeModule();
57
+ this.native = new bindings.NativeUnifiedCache(this.maxSize);
58
+ if (this.config.enableFairnessCheck) {
59
+ this.startFairnessMonitor();
60
+ }
61
+ if (this.config.enableMemoryMonitoring) {
62
+ this.startMemoryPressureMonitor();
63
+ }
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Core API
67
+ // ---------------------------------------------------------------------------
68
+ async get(key, loadFn) {
69
+ // Check JS data map
70
+ const cached = this.data.get(key);
71
+ if (cached !== undefined) {
72
+ this.native.recordAccess(key);
73
+ return cached;
74
+ }
75
+ if (!loadFn)
76
+ return undefined;
77
+ // Request coalescing
78
+ if (this.config.enableRequestCoalescing && this.loadingPromises.has(key)) {
79
+ return this.loadingPromises.get(key);
80
+ }
81
+ const loadPromise = loadFn();
82
+ if (this.config.enableRequestCoalescing) {
83
+ this.loadingPromises.set(key, loadPromise);
84
+ }
85
+ try {
86
+ const result = await loadPromise;
87
+ return result;
88
+ }
89
+ finally {
90
+ if (this.config.enableRequestCoalescing) {
91
+ this.loadingPromises.delete(key);
92
+ }
93
+ }
94
+ }
95
+ getSync(key) {
96
+ const cached = this.data.get(key);
97
+ if (cached !== undefined) {
98
+ this.native.recordAccess(key);
99
+ return cached;
100
+ }
101
+ return undefined;
102
+ }
103
+ set(key, data, type, size, rebuildCost = 1) {
104
+ const cacheType = CACHE_TYPE_MAP[type] ?? 3;
105
+ // Remove existing entry if present
106
+ if (this.data.has(key)) {
107
+ this.native.remove(key);
108
+ }
109
+ // Insert into native (may trigger eviction)
110
+ const evictionResult = this.native.insert(key, cacheType, size, rebuildCost);
111
+ // Remove evicted keys from JS data
112
+ for (const evictedKey of evictionResult.evictedKeys) {
113
+ this.data.delete(evictedKey);
114
+ }
115
+ // Store actual data in JS
116
+ this.data.set(key, data);
117
+ }
118
+ delete(key) {
119
+ const existed = this.data.has(key);
120
+ if (existed) {
121
+ this.native.remove(key);
122
+ this.data.delete(key);
123
+ }
124
+ return existed;
125
+ }
126
+ deleteByPrefix(prefix) {
127
+ const removedKeys = this.native.removeByPrefix(prefix);
128
+ for (const key of removedKeys) {
129
+ this.data.delete(key);
130
+ }
131
+ return removedKeys.length;
132
+ }
133
+ clear(type) {
134
+ if (!type) {
135
+ const clearedKeys = this.native.clear();
136
+ for (const key of clearedKeys) {
137
+ this.data.delete(key);
138
+ }
139
+ // Also clear any remaining data entries
140
+ this.data.clear();
141
+ }
142
+ else {
143
+ const cacheType = CACHE_TYPE_MAP[type] ?? 3;
144
+ const clearedKeys = this.native.clear(cacheType);
145
+ for (const key of clearedKeys) {
146
+ this.data.delete(key);
147
+ }
148
+ }
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Eviction
152
+ // ---------------------------------------------------------------------------
153
+ evictForSize(bytesNeeded) {
154
+ const result = this.native.evictForSize(bytesNeeded);
155
+ for (const key of result.evictedKeys) {
156
+ this.data.delete(key);
157
+ }
158
+ return result.bytesFreed >= bytesNeeded;
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // Fairness
162
+ // ---------------------------------------------------------------------------
163
+ startFairnessMonitor() {
164
+ const timer = setInterval(() => {
165
+ this.checkFairness();
166
+ }, this.config.fairnessCheckInterval);
167
+ if (timer.unref)
168
+ timer.unref();
169
+ }
170
+ checkFairness() {
171
+ const result = this.native.checkFairness();
172
+ for (const key of result.evictedKeys) {
173
+ this.data.delete(key);
174
+ }
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Memory pressure
178
+ // ---------------------------------------------------------------------------
179
+ startMemoryPressureMonitor() {
180
+ const checkInterval = this.config.memoryCheckInterval || 30000;
181
+ this.memoryPressureCheckTimer = setInterval(() => {
182
+ this.checkMemoryPressure();
183
+ }, checkInterval);
184
+ if (this.memoryPressureCheckTimer.unref) {
185
+ this.memoryPressureCheckTimer.unref();
186
+ }
187
+ }
188
+ checkMemoryPressure() {
189
+ const stats = this.native.getStats();
190
+ const pressure = checkMemoryPressure(stats.totalSize, this.memoryInfo);
191
+ const now = Date.now();
192
+ const fiveMinutes = 5 * 60 * 1000;
193
+ if (pressure.warnings.length > 0 && now - this.lastMemoryWarning > fiveMinutes) {
194
+ for (const warning of pressure.warnings) {
195
+ prodLog.warn(`NativeUnifiedCache: ${warning}`);
196
+ }
197
+ this.lastMemoryWarning = now;
198
+ }
199
+ if (pressure.pressure === 'critical') {
200
+ const targetSize = Math.floor(this.maxSize * 0.7);
201
+ const bytesToFree = stats.totalSize - targetSize;
202
+ if (bytesToFree > 0) {
203
+ prodLog.warn(`NativeUnifiedCache: Critical memory pressure - forcing eviction of ${formatBytes(bytesToFree)}`);
204
+ this.evictForSize(bytesToFree);
205
+ }
206
+ }
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // Statistics
210
+ // ---------------------------------------------------------------------------
211
+ getStats() {
212
+ const nativeStats = this.native.getStats();
213
+ const typeNames = ['hnsw', 'metadata', 'embedding', 'other'];
214
+ const typeSizes = {};
215
+ const typeCounts = {};
216
+ const typeAccessCounts = {};
217
+ for (let i = 0; i < typeNames.length; i++) {
218
+ typeSizes[typeNames[i]] = nativeStats.typeSizes[i] || 0;
219
+ typeCounts[typeNames[i]] = nativeStats.typeCounts[i] || 0;
220
+ typeAccessCounts[typeNames[i]] = nativeStats.typeAccessCounts[i] || 0;
221
+ }
222
+ const hitRate = nativeStats.totalAccessCount > 0
223
+ ? nativeStats.itemCount / nativeStats.totalAccessCount
224
+ : 0;
225
+ return {
226
+ totalSize: nativeStats.totalSize,
227
+ maxSize: nativeStats.maxSize,
228
+ utilization: nativeStats.maxSize > 0 ? nativeStats.totalSize / nativeStats.maxSize : 0,
229
+ itemCount: nativeStats.itemCount,
230
+ typeSizes,
231
+ typeCounts,
232
+ typeAccessCounts,
233
+ totalAccessCount: nativeStats.totalAccessCount,
234
+ hitRate,
235
+ memory: {
236
+ available: this.memoryInfo.available,
237
+ source: this.memoryInfo.source,
238
+ isContainer: this.memoryInfo.isContainer,
239
+ systemTotal: this.memoryInfo.systemTotal,
240
+ allocationRatio: this.allocationStrategy.ratio,
241
+ environment: this.allocationStrategy.environment,
242
+ },
243
+ };
244
+ }
245
+ getMemoryInfo() {
246
+ const stats = this.native.getStats();
247
+ return {
248
+ memoryInfo: { ...this.memoryInfo },
249
+ allocationStrategy: { ...this.allocationStrategy },
250
+ currentPressure: checkMemoryPressure(stats.totalSize, this.memoryInfo),
251
+ };
252
+ }
253
+ // ---------------------------------------------------------------------------
254
+ // Access pattern persistence
255
+ // ---------------------------------------------------------------------------
256
+ async saveAccessPatterns() {
257
+ if (!this.config.persistPatterns)
258
+ return;
259
+ const json = this.native.saveAccessPatterns();
260
+ return JSON.parse(json);
261
+ }
262
+ async loadAccessPatterns(patterns) {
263
+ if (!patterns)
264
+ return;
265
+ this.native.loadAccessPatterns(JSON.stringify(patterns));
266
+ }
267
+ }
268
+ //# sourceMappingURL=NativeUnifiedCache.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/cortex",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Native Rust acceleration for Brainy — SIMD distance, vector quantization, zero-copy mmap, native embeddings. Commercial license required (14-day free trial).",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",