@soulcraft/brainy 3.36.1 → 3.37.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [3.37.1](https://github.com/soulcraftlabs/brainy/compare/v3.37.0...v3.37.1) (2025-10-10)
6
+
7
+
8
+ ### 🐛 Bug Fixes
9
+
10
+ * combine vector and metadata in getNoun/getVerb internal methods ([cb1e37c](https://github.com/soulcraftlabs/brainy/commit/cb1e37c0e8132f53be0f359feaef5dcf342462d2))
11
+
12
+ ### [3.37.0](https://github.com/soulcraftlabs/brainy/compare/v3.36.1...v3.37.0) (2025-10-10)
13
+
14
+ - fix: implement 2-file storage architecture for GCS scalability (59da5f6)
15
+
16
+
5
17
  ### [3.36.1](https://github.com/soulcraftlabs/brainy/compare/v3.36.0...v3.36.1) (2025-10-10)
6
18
 
7
19
  - fix: resolve critical GCS storage bugs preventing production use (3cd0b9a)
@@ -82,6 +82,7 @@ export interface HNSWVerb {
82
82
  id: string;
83
83
  vector: Vector;
84
84
  connections: Map<number, Set<string>>;
85
+ metadata?: any;
85
86
  }
86
87
  /**
87
88
  * Verb representing a relationship between nouns
@@ -161,7 +161,8 @@ export declare class FileSystemStorage extends BaseStorage {
161
161
  */
162
162
  protected saveNoun_internal(noun: HNSWNoun): Promise<void>;
163
163
  /**
164
- * Get a noun from storage
164
+ * Get a noun from storage (internal implementation)
165
+ * Combines vector data from getNode() with metadata from getNounMetadata()
165
166
  */
166
167
  protected getNoun_internal(id: string): Promise<HNSWNoun | null>;
167
168
  /**
@@ -177,7 +178,8 @@ export declare class FileSystemStorage extends BaseStorage {
177
178
  */
178
179
  protected saveVerb_internal(verb: HNSWVerb): Promise<void>;
179
180
  /**
180
- * Get a verb from storage
181
+ * Get a verb from storage (internal implementation)
182
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
181
183
  */
182
184
  protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
183
185
  /**
@@ -168,9 +168,14 @@ export class FileSystemStorage extends BaseStorage {
168
168
  // Check if this is a new node to update counts
169
169
  const isNew = !(await this.fileExists(this.getNodePath(node.id)));
170
170
  // Convert connections Map to a serializable format
171
+ // CRITICAL: Only save lightweight vector data (no metadata)
172
+ // Metadata is saved separately via saveNounMetadata() (2-file system)
171
173
  const serializableNode = {
172
- ...node,
173
- connections: this.mapToObject(node.connections, (set) => Array.from(set))
174
+ id: node.id,
175
+ vector: node.vector,
176
+ connections: this.mapToObject(node.connections, (set) => Array.from(set)),
177
+ level: node.level || 0
178
+ // NO metadata field - saved separately for scalability
174
179
  };
175
180
  const filePath = this.getNodePath(node.id);
176
181
  await this.ensureDirectoryExists(path.dirname(filePath));
@@ -200,12 +205,14 @@ export class FileSystemStorage extends BaseStorage {
200
205
  for (const [level, nodeIds] of Object.entries(parsedNode.connections)) {
201
206
  connections.set(Number(level), new Set(nodeIds));
202
207
  }
208
+ // CRITICAL: Only return lightweight vector data (no metadata)
209
+ // Metadata is retrieved separately via getNounMetadata() (2-file system)
203
210
  return {
204
211
  id: parsedNode.id,
205
212
  vector: parsedNode.vector,
206
213
  connections,
207
- level: parsedNode.level || 0,
208
- metadata: parsedNode.metadata
214
+ level: parsedNode.level || 0
215
+ // NO metadata field - retrieved separately for scalability
209
216
  };
210
217
  }
211
218
  catch (error) {
@@ -329,9 +336,13 @@ export class FileSystemStorage extends BaseStorage {
329
336
  // Check if this is a new edge to update counts
330
337
  const isNew = !(await this.fileExists(this.getVerbPath(edge.id)));
331
338
  // Convert connections Map to a serializable format
339
+ // CRITICAL: Only save lightweight vector data (no metadata)
340
+ // Metadata is saved separately via saveVerbMetadata() (2-file system)
332
341
  const serializableEdge = {
333
- ...edge,
342
+ id: edge.id,
343
+ vector: edge.vector,
334
344
  connections: this.mapToObject(edge.connections, (set) => Array.from(set))
345
+ // NO metadata field - saved separately for scalability
335
346
  };
336
347
  const filePath = this.getVerbPath(edge.id);
337
348
  await this.ensureDirectoryExists(path.dirname(filePath));
@@ -558,7 +569,9 @@ export class FileSystemStorage extends BaseStorage {
558
569
  const batch = ids.slice(i, i + batchSize);
559
570
  const batchPromises = batch.map(async (id) => {
560
571
  try {
561
- const metadata = await this.getMetadata(id);
572
+ // CRITICAL: Use getNounMetadata() instead of deprecated getMetadata()
573
+ // This ensures we fetch from the correct noun metadata store (2-file system)
574
+ const metadata = await this.getNounMetadata(id);
562
575
  return { id, metadata };
563
576
  }
564
577
  catch (error) {
@@ -850,10 +863,22 @@ export class FileSystemStorage extends BaseStorage {
850
863
  return this.saveNode(noun);
851
864
  }
852
865
  /**
853
- * Get a noun from storage
866
+ * Get a noun from storage (internal implementation)
867
+ * Combines vector data from getNode() with metadata from getNounMetadata()
854
868
  */
855
869
  async getNoun_internal(id) {
856
- return this.getNode(id);
870
+ // Get vector data (lightweight)
871
+ const node = await this.getNode(id);
872
+ if (!node) {
873
+ return null;
874
+ }
875
+ // Get metadata (entity data in 2-file system)
876
+ const metadata = await this.getNounMetadata(id);
877
+ // Combine into complete noun object
878
+ return {
879
+ ...node,
880
+ metadata: metadata || {}
881
+ };
857
882
  }
858
883
  /**
859
884
  * Get nouns by noun type
@@ -874,10 +899,22 @@ export class FileSystemStorage extends BaseStorage {
874
899
  return this.saveEdge(verb);
875
900
  }
876
901
  /**
877
- * Get a verb from storage
902
+ * Get a verb from storage (internal implementation)
903
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
878
904
  */
879
905
  async getVerb_internal(id) {
880
- return this.getEdge(id);
906
+ // Get vector data (lightweight)
907
+ const edge = await this.getEdge(id);
908
+ if (!edge) {
909
+ return null;
910
+ }
911
+ // Get metadata (relationship data in 2-file system)
912
+ const metadata = await this.getVerbMetadata(id);
913
+ // Combine into complete verb object
914
+ return {
915
+ ...edge,
916
+ metadata: metadata || {}
917
+ };
881
918
  }
882
919
  /**
883
920
  * Get verbs by source
@@ -148,6 +148,7 @@ export declare class GcsStorage extends BaseStorage {
148
148
  private saveNodeDirect;
149
149
  /**
150
150
  * Get a noun from storage (internal implementation)
151
+ * Combines vector data from getNode() with metadata from getNounMetadata()
151
152
  */
152
153
  protected getNoun_internal(id: string): Promise<HNSWNoun | null>;
153
154
  /**
@@ -196,6 +197,7 @@ export declare class GcsStorage extends BaseStorage {
196
197
  private saveEdgeDirect;
197
198
  /**
198
199
  * Get a verb from storage (internal implementation)
200
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
199
201
  */
200
202
  protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
201
203
  /**
@@ -306,6 +308,13 @@ export declare class GcsStorage extends BaseStorage {
306
308
  hasMore: boolean;
307
309
  nextCursor?: string;
308
310
  }>;
311
+ /**
312
+ * Batch fetch metadata for multiple noun IDs (efficient for large queries)
313
+ * Uses smaller batches to prevent GCS socket exhaustion
314
+ * @param ids Array of noun IDs to fetch metadata for
315
+ * @returns Map of ID to metadata
316
+ */
317
+ getMetadataBatch(ids: string[]): Promise<Map<string, any>>;
309
318
  /**
310
319
  * Clear all data from storage
311
320
  */
@@ -316,12 +316,17 @@ export class GcsStorage extends BaseStorage {
316
316
  try {
317
317
  this.logger.trace(`Saving node ${node.id}`);
318
318
  // Convert connections Map to a serializable format
319
+ // CRITICAL: Only save lightweight vector data (no metadata)
320
+ // Metadata is saved separately via saveNounMetadata() (2-file system)
319
321
  const serializableNode = {
320
- ...node,
322
+ id: node.id,
323
+ vector: node.vector,
321
324
  connections: Object.fromEntries(Array.from(node.connections.entries()).map(([level, nounIds]) => [
322
325
  level,
323
326
  Array.from(nounIds)
324
- ]))
327
+ ])),
328
+ level: node.level || 0
329
+ // NO metadata field - saved separately for scalability
325
330
  };
326
331
  // Get the GCS key with UUID-based sharding
327
332
  const key = this.getNounKey(node.id);
@@ -354,9 +359,21 @@ export class GcsStorage extends BaseStorage {
354
359
  }
355
360
  /**
356
361
  * Get a noun from storage (internal implementation)
362
+ * Combines vector data from getNode() with metadata from getNounMetadata()
357
363
  */
358
364
  async getNoun_internal(id) {
359
- return this.getNode(id);
365
+ // Get vector data (lightweight)
366
+ const node = await this.getNode(id);
367
+ if (!node) {
368
+ return null;
369
+ }
370
+ // Get metadata (entity data in 2-file system)
371
+ const metadata = await this.getNounMetadata(id);
372
+ // Combine into complete noun object
373
+ return {
374
+ ...node,
375
+ metadata: metadata || {}
376
+ };
360
377
  }
361
378
  /**
362
379
  * Get a node from storage
@@ -385,12 +402,14 @@ export class GcsStorage extends BaseStorage {
385
402
  for (const [level, nounIds] of Object.entries(data.connections || {})) {
386
403
  connections.set(Number(level), new Set(nounIds));
387
404
  }
405
+ // CRITICAL: Only return lightweight vector data (no metadata)
406
+ // Metadata is retrieved separately via getNounMetadata() (2-file system)
388
407
  const node = {
389
408
  id: data.id,
390
409
  vector: data.vector,
391
410
  connections,
392
- level: data.level || 0,
393
- metadata: data.metadata // CRITICAL: Include metadata for entity reconstruction
411
+ level: data.level || 0
412
+ // NO metadata field - retrieved separately for scalability
394
413
  };
395
414
  // Update cache
396
415
  this.nounCacheManager.set(id, node);
@@ -571,12 +590,16 @@ export class GcsStorage extends BaseStorage {
571
590
  try {
572
591
  this.logger.trace(`Saving edge ${edge.id}`);
573
592
  // Convert connections Map to serializable format
593
+ // CRITICAL: Only save lightweight vector data (no metadata)
594
+ // Metadata is saved separately via saveVerbMetadata() (2-file system)
574
595
  const serializableEdge = {
575
- ...edge,
596
+ id: edge.id,
597
+ vector: edge.vector,
576
598
  connections: Object.fromEntries(Array.from(edge.connections.entries()).map(([level, verbIds]) => [
577
599
  level,
578
600
  Array.from(verbIds)
579
601
  ]))
602
+ // NO metadata field - saved separately for scalability
580
603
  };
581
604
  // Get the GCS key with UUID-based sharding
582
605
  const key = this.getVerbKey(edge.id);
@@ -608,9 +631,21 @@ export class GcsStorage extends BaseStorage {
608
631
  }
609
632
  /**
610
633
  * Get a verb from storage (internal implementation)
634
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
611
635
  */
612
636
  async getVerb_internal(id) {
613
- return this.getEdge(id);
637
+ // Get vector data (lightweight)
638
+ const edge = await this.getEdge(id);
639
+ if (!edge) {
640
+ return null;
641
+ }
642
+ // Get metadata (relationship data in 2-file system)
643
+ const metadata = await this.getVerbMetadata(id);
644
+ // Combine into complete verb object
645
+ return {
646
+ ...edge,
647
+ metadata: metadata || {}
648
+ };
614
649
  }
615
650
  /**
616
651
  * Get an edge from storage
@@ -1001,6 +1036,46 @@ export class GcsStorage extends BaseStorage {
1001
1036
  filter: options?.filter
1002
1037
  });
1003
1038
  }
1039
+ /**
1040
+ * Batch fetch metadata for multiple noun IDs (efficient for large queries)
1041
+ * Uses smaller batches to prevent GCS socket exhaustion
1042
+ * @param ids Array of noun IDs to fetch metadata for
1043
+ * @returns Map of ID to metadata
1044
+ */
1045
+ async getMetadataBatch(ids) {
1046
+ await this.ensureInitialized();
1047
+ const results = new Map();
1048
+ const batchSize = 10; // Smaller batches for metadata to prevent socket exhaustion
1049
+ // Process in smaller batches
1050
+ for (let i = 0; i < ids.length; i += batchSize) {
1051
+ const batch = ids.slice(i, i + batchSize);
1052
+ const batchPromises = batch.map(async (id) => {
1053
+ try {
1054
+ // CRITICAL: Use getNounMetadata() instead of deprecated getMetadata()
1055
+ // This ensures we fetch from the correct noun metadata store (2-file system)
1056
+ const metadata = await this.getNounMetadata(id);
1057
+ return { id, metadata };
1058
+ }
1059
+ catch (error) {
1060
+ // Handle GCS-specific errors
1061
+ if (this.isThrottlingError(error)) {
1062
+ await this.handleThrottling(error);
1063
+ }
1064
+ this.logger.debug(`Failed to read metadata for ${id}:`, error);
1065
+ return { id, metadata: null };
1066
+ }
1067
+ });
1068
+ const batchResults = await Promise.all(batchPromises);
1069
+ for (const { id, metadata } of batchResults) {
1070
+ if (metadata !== null) {
1071
+ results.set(id, metadata);
1072
+ }
1073
+ }
1074
+ // Small yield between batches to prevent overwhelming GCS
1075
+ await new Promise(resolve => setImmediate(resolve));
1076
+ }
1077
+ return results;
1078
+ }
1004
1079
  /**
1005
1080
  * Clear all data from storage
1006
1081
  */
@@ -28,7 +28,8 @@ export declare class MemoryStorage extends BaseStorage {
28
28
  */
29
29
  protected saveNoun_internal(noun: HNSWNoun): Promise<void>;
30
30
  /**
31
- * Get a noun from storage
31
+ * Get a noun from storage (internal implementation)
32
+ * Combines vector data from nouns map with metadata from getNounMetadata()
32
33
  */
33
34
  protected getNoun_internal(id: string): Promise<HNSWNoun | null>;
34
35
  /**
@@ -77,7 +78,8 @@ export declare class MemoryStorage extends BaseStorage {
77
78
  */
78
79
  protected saveVerb_internal(verb: HNSWVerb): Promise<void>;
79
80
  /**
80
- * Get a verb from storage
81
+ * Get a verb from storage (internal implementation)
82
+ * Combines vector data from verbs map with metadata from getVerbMetadata()
81
83
  */
82
84
  protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
83
85
  /**
@@ -41,12 +41,14 @@ export class MemoryStorage extends BaseStorage {
41
41
  async saveNoun_internal(noun) {
42
42
  const isNew = !this.nouns.has(noun.id);
43
43
  // Create a deep copy to avoid reference issues
44
+ // CRITICAL: Only save lightweight vector data (no metadata)
45
+ // Metadata is saved separately via saveNounMetadata() (2-file system)
44
46
  const nounCopy = {
45
47
  id: noun.id,
46
48
  vector: [...noun.vector],
47
49
  connections: new Map(),
48
- level: noun.level || 0,
49
- metadata: noun.metadata
50
+ level: noun.level || 0
51
+ // NO metadata field - saved separately for scalability
50
52
  };
51
53
  // Copy connections
52
54
  for (const [level, connections] of noun.connections.entries()) {
@@ -61,7 +63,8 @@ export class MemoryStorage extends BaseStorage {
61
63
  }
62
64
  }
63
65
  /**
64
- * Get a noun from storage
66
+ * Get a noun from storage (internal implementation)
67
+ * Combines vector data from nouns map with metadata from getNounMetadata()
65
68
  */
66
69
  async getNoun_internal(id) {
67
70
  // Get the noun directly from the nouns map
@@ -75,14 +78,19 @@ export class MemoryStorage extends BaseStorage {
75
78
  id: noun.id,
76
79
  vector: [...noun.vector],
77
80
  connections: new Map(),
78
- level: noun.level || 0,
79
- metadata: noun.metadata
81
+ level: noun.level || 0
80
82
  };
81
83
  // Copy connections
82
84
  for (const [level, connections] of noun.connections.entries()) {
83
85
  nounCopy.connections.set(level, new Set(connections));
84
86
  }
85
- return nounCopy;
87
+ // Get metadata (entity data in 2-file system)
88
+ const metadata = await this.getNounMetadata(id);
89
+ // Combine into complete noun object
90
+ return {
91
+ ...nounCopy,
92
+ metadata: metadata || {}
93
+ };
86
94
  }
87
95
  /**
88
96
  * Get nouns with pagination and filtering
@@ -233,7 +241,8 @@ export class MemoryStorage extends BaseStorage {
233
241
  // since HNSWVerb doesn't contain type information
234
242
  }
235
243
  /**
236
- * Get a verb from storage
244
+ * Get a verb from storage (internal implementation)
245
+ * Combines vector data from verbs map with metadata from getVerbMetadata()
237
246
  */
238
247
  async getVerb_internal(id) {
239
248
  // Get the verb directly from the verbs map
@@ -242,16 +251,6 @@ export class MemoryStorage extends BaseStorage {
242
251
  if (!verb) {
243
252
  return null;
244
253
  }
245
- // Create default timestamp if not present
246
- const defaultTimestamp = {
247
- seconds: Math.floor(Date.now() / 1000),
248
- nanoseconds: (Date.now() % 1000) * 1000000
249
- };
250
- // Create default createdBy if not present
251
- const defaultCreatedBy = {
252
- augmentation: 'unknown',
253
- version: '1.0'
254
- };
255
254
  // Return a deep copy of the HNSWVerb
256
255
  const verbCopy = {
257
256
  id: verb.id,
@@ -262,7 +261,13 @@ export class MemoryStorage extends BaseStorage {
262
261
  for (const [level, connections] of verb.connections.entries()) {
263
262
  verbCopy.connections.set(level, new Set(connections));
264
263
  }
265
- return verbCopy;
264
+ // Get metadata (relationship data in 2-file system)
265
+ const metadata = await this.getVerbMetadata(id);
266
+ // Combine into complete verb object
267
+ return {
268
+ ...verbCopy,
269
+ metadata: metadata || {}
270
+ };
266
271
  }
267
272
  /**
268
273
  * Get verbs with pagination and filtering
@@ -475,7 +480,9 @@ export class MemoryStorage extends BaseStorage {
475
480
  const results = new Map();
476
481
  // Memory storage can handle all IDs at once since it's in-memory
477
482
  for (const id of ids) {
478
- const metadata = await this.getMetadata(id);
483
+ // CRITICAL: Use getNounMetadata() instead of deprecated getMetadata()
484
+ // This ensures we fetch from the correct noun metadata store (2-file system)
485
+ const metadata = await this.getNounMetadata(id);
479
486
  if (metadata) {
480
487
  results.set(id, metadata);
481
488
  }
@@ -53,7 +53,8 @@ export declare class OPFSStorage extends BaseStorage {
53
53
  */
54
54
  protected saveNoun_internal(noun: HNSWNoun_internal): Promise<void>;
55
55
  /**
56
- * Get a noun from storage
56
+ * Get a noun from storage (internal implementation)
57
+ * Combines vector data from file with metadata from getNounMetadata()
57
58
  */
58
59
  protected getNoun_internal(id: string): Promise<HNSWNoun_internal | null>;
59
60
  /**
@@ -86,6 +87,7 @@ export declare class OPFSStorage extends BaseStorage {
86
87
  protected saveEdge(edge: Edge): Promise<void>;
87
88
  /**
88
89
  * Get a verb from storage (internal implementation)
90
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
89
91
  */
90
92
  protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
91
93
  /**
@@ -143,10 +143,14 @@ export class OPFSStorage extends BaseStorage {
143
143
  async saveNoun_internal(noun) {
144
144
  await this.ensureInitialized();
145
145
  try {
146
- // Convert connections Map to a serializable format
146
+ // CRITICAL: Only save lightweight vector data (no metadata)
147
+ // Metadata is saved separately via saveNounMetadata() (2-file system)
147
148
  const serializableNoun = {
148
- ...noun,
149
- connections: this.mapToObject(noun.connections, (set) => Array.from(set))
149
+ id: noun.id,
150
+ vector: noun.vector,
151
+ connections: this.mapToObject(noun.connections, (set) => Array.from(set)),
152
+ level: noun.level || 0
153
+ // NO metadata field - saved separately for scalability
150
154
  };
151
155
  // Use UUID-based sharding for nouns
152
156
  const shardId = getShardIdFromUuid(noun.id);
@@ -169,7 +173,8 @@ export class OPFSStorage extends BaseStorage {
169
173
  }
170
174
  }
171
175
  /**
172
- * Get a noun from storage
176
+ * Get a noun from storage (internal implementation)
177
+ * Combines vector data from file with metadata from getNounMetadata()
173
178
  */
174
179
  async getNoun_internal(id) {
175
180
  await this.ensureInitialized();
@@ -189,12 +194,19 @@ export class OPFSStorage extends BaseStorage {
189
194
  for (const [level, nounIds] of Object.entries(data.connections)) {
190
195
  connections.set(Number(level), new Set(nounIds));
191
196
  }
192
- return {
197
+ const node = {
193
198
  id: data.id,
194
199
  vector: data.vector,
195
200
  connections,
196
201
  level: data.level || 0
197
202
  };
203
+ // Get metadata (entity data in 2-file system)
204
+ const metadata = await this.getNounMetadata(id);
205
+ // Combine into complete noun object
206
+ return {
207
+ ...node,
208
+ metadata: metadata || {}
209
+ };
198
210
  }
199
211
  catch (error) {
200
212
  // Noun not found or other error
@@ -299,10 +311,13 @@ export class OPFSStorage extends BaseStorage {
299
311
  async saveEdge(edge) {
300
312
  await this.ensureInitialized();
301
313
  try {
302
- // Convert connections Map to a serializable format
314
+ // CRITICAL: Only save lightweight vector data (no metadata)
315
+ // Metadata is saved separately via saveVerbMetadata() (2-file system)
303
316
  const serializableEdge = {
304
- ...edge,
317
+ id: edge.id,
318
+ vector: edge.vector,
305
319
  connections: this.mapToObject(edge.connections, (set) => Array.from(set))
320
+ // NO metadata field - saved separately for scalability
306
321
  };
307
322
  // Use UUID-based sharding for verbs
308
323
  const shardId = getShardIdFromUuid(edge.id);
@@ -326,9 +341,21 @@ export class OPFSStorage extends BaseStorage {
326
341
  }
327
342
  /**
328
343
  * Get a verb from storage (internal implementation)
344
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
329
345
  */
330
346
  async getVerb_internal(id) {
331
- return this.getEdge(id);
347
+ // Get vector data (lightweight)
348
+ const edge = await this.getEdge(id);
349
+ if (!edge) {
350
+ return null;
351
+ }
352
+ // Get metadata (relationship data in 2-file system)
353
+ const metadata = await this.getVerbMetadata(id);
354
+ // Combine into complete verb object
355
+ return {
356
+ ...edge,
357
+ metadata: metadata || {}
358
+ };
332
359
  }
333
360
  /**
334
361
  * Get an edge from storage
@@ -222,6 +222,7 @@ export declare class S3CompatibleStorage extends BaseStorage {
222
222
  protected saveNode(node: HNSWNode): Promise<void>;
223
223
  /**
224
224
  * Get a noun from storage (internal implementation)
225
+ * Combines vector data from getNode() with metadata from getNounMetadata()
225
226
  */
226
227
  protected getNoun_internal(id: string): Promise<HNSWNoun | null>;
227
228
  /**
@@ -297,6 +298,7 @@ export declare class S3CompatibleStorage extends BaseStorage {
297
298
  protected saveEdge(edge: Edge): Promise<void>;
298
299
  /**
299
300
  * Get a verb from storage (internal implementation)
301
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
300
302
  */
301
303
  protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
302
304
  /**
@@ -717,9 +717,14 @@ export class S3CompatibleStorage extends BaseStorage {
717
717
  try {
718
718
  this.logger.trace(`Saving node ${node.id}`);
719
719
  // Convert connections Map to a serializable format
720
+ // CRITICAL: Only save lightweight vector data (no metadata)
721
+ // Metadata is saved separately via saveNounMetadata() (2-file system)
720
722
  const serializableNode = {
721
- ...node,
722
- connections: this.mapToObject(node.connections, (set) => Array.from(set))
723
+ id: node.id,
724
+ vector: node.vector,
725
+ connections: this.mapToObject(node.connections, (set) => Array.from(set)),
726
+ level: node.level || 0
727
+ // NO metadata field - saved separately for scalability
723
728
  };
724
729
  // Import the PutObjectCommand only when needed
725
730
  const { PutObjectCommand } = await import('@aws-sdk/client-s3');
@@ -763,6 +768,13 @@ export class S3CompatibleStorage extends BaseStorage {
763
768
  catch (verifyError) {
764
769
  this.logger.warn(`Failed to verify node ${node.id} was saved correctly:`, verifyError);
765
770
  }
771
+ // Increment noun count - always increment total, and increment by type if metadata exists
772
+ this.totalNounCount++;
773
+ const metadata = await this.getNounMetadata(node.id);
774
+ if (metadata && metadata.type) {
775
+ const currentCount = this.entityCounts.get(metadata.type) || 0;
776
+ this.entityCounts.set(metadata.type, currentCount + 1);
777
+ }
766
778
  // Release backpressure on success
767
779
  this.releaseBackpressure(true, requestId);
768
780
  }
@@ -775,9 +787,21 @@ export class S3CompatibleStorage extends BaseStorage {
775
787
  }
776
788
  /**
777
789
  * Get a noun from storage (internal implementation)
790
+ * Combines vector data from getNode() with metadata from getNounMetadata()
778
791
  */
779
792
  async getNoun_internal(id) {
780
- return this.getNode(id);
793
+ // Get vector data (lightweight)
794
+ const node = await this.getNode(id);
795
+ if (!node) {
796
+ return null;
797
+ }
798
+ // Get metadata (entity data in 2-file system)
799
+ const metadata = await this.getNounMetadata(id);
800
+ // Combine into complete noun object
801
+ return {
802
+ ...node,
803
+ metadata: metadata || {}
804
+ };
781
805
  }
782
806
  /**
783
807
  * Get a node from storage
@@ -1112,9 +1136,13 @@ export class S3CompatibleStorage extends BaseStorage {
1112
1136
  const requestId = await this.applyBackpressure();
1113
1137
  try {
1114
1138
  // Convert connections Map to a serializable format
1139
+ // CRITICAL: Only save lightweight vector data (no metadata)
1140
+ // Metadata is saved separately via saveVerbMetadata() (2-file system)
1115
1141
  const serializableEdge = {
1116
- ...edge,
1142
+ id: edge.id,
1143
+ vector: edge.vector,
1117
1144
  connections: this.mapToObject(edge.connections, (set) => Array.from(set))
1145
+ // NO metadata field - saved separately for scalability
1118
1146
  };
1119
1147
  // Import the PutObjectCommand only when needed
1120
1148
  const { PutObjectCommand } = await import('@aws-sdk/client-s3');
@@ -1135,6 +1163,13 @@ export class S3CompatibleStorage extends BaseStorage {
1135
1163
  vector: edge.vector
1136
1164
  }
1137
1165
  });
1166
+ // Increment verb count - always increment total, and increment by type if metadata exists
1167
+ this.totalVerbCount++;
1168
+ const metadata = await this.getVerbMetadata(edge.id);
1169
+ if (metadata && metadata.type) {
1170
+ const currentCount = this.verbCounts.get(metadata.type) || 0;
1171
+ this.verbCounts.set(metadata.type, currentCount + 1);
1172
+ }
1138
1173
  // Release backpressure on success
1139
1174
  this.releaseBackpressure(true, requestId);
1140
1175
  }
@@ -1147,9 +1182,21 @@ export class S3CompatibleStorage extends BaseStorage {
1147
1182
  }
1148
1183
  /**
1149
1184
  * Get a verb from storage (internal implementation)
1185
+ * Combines vector data from getEdge() with metadata from getVerbMetadata()
1150
1186
  */
1151
1187
  async getVerb_internal(id) {
1152
- return this.getEdge(id);
1188
+ // Get vector data (lightweight)
1189
+ const edge = await this.getEdge(id);
1190
+ if (!edge) {
1191
+ return null;
1192
+ }
1193
+ // Get metadata (relationship data in 2-file system)
1194
+ const metadata = await this.getVerbMetadata(id);
1195
+ // Combine into complete verb object
1196
+ return {
1197
+ ...edge,
1198
+ metadata: metadata || {}
1199
+ };
1153
1200
  }
1154
1201
  /**
1155
1202
  * Get an edge from storage
@@ -1643,8 +1690,10 @@ export class S3CompatibleStorage extends BaseStorage {
1643
1690
  const batchPromises = batch.map(async (id) => {
1644
1691
  try {
1645
1692
  // Add timeout wrapper for individual metadata reads
1693
+ // CRITICAL: Use getNounMetadata() instead of deprecated getMetadata()
1694
+ // This ensures we fetch from the correct noun metadata store (2-file system)
1646
1695
  const metadata = await Promise.race([
1647
- this.getMetadata(id),
1696
+ this.getNounMetadata(id),
1648
1697
  new Promise((_, reject) => setTimeout(() => reject(new Error('Metadata read timeout')), 5000) // 5 second timeout
1649
1698
  )
1650
1699
  ]);
@@ -229,6 +229,11 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
229
229
  * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
230
230
  */
231
231
  getNounMetadata(id: string): Promise<any | null>;
232
+ /**
233
+ * Delete noun metadata from storage
234
+ * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
235
+ */
236
+ deleteNounMetadata(id: string): Promise<void>;
232
237
  /**
233
238
  * Save verb metadata to storage
234
239
  * Routes to correct sharded location based on UUID
@@ -141,11 +141,33 @@ export class BaseStorage extends BaseStorageAdapter {
141
141
  async saveNoun(noun) {
142
142
  await this.ensureInitialized();
143
143
  // Validate noun type before saving - storage boundary protection
144
- const metadata = await this.getNounMetadata(noun.id);
145
- if (metadata?.noun) {
146
- validateNounType(metadata.noun);
144
+ if (noun.metadata?.noun) {
145
+ validateNounType(noun.metadata.noun);
146
+ }
147
+ // Save both the HNSWNoun vector data and metadata separately (2-file system)
148
+ try {
149
+ // Save the lightweight HNSWNoun vector file first
150
+ await this.saveNoun_internal(noun);
151
+ // Then save the metadata to separate file (if present)
152
+ if (noun.metadata) {
153
+ await this.saveNounMetadata(noun.id, noun.metadata);
154
+ }
155
+ }
156
+ catch (error) {
157
+ console.error(`[ERROR] Failed to save noun ${noun.id}:`, error);
158
+ // Attempt cleanup - remove noun file if metadata failed
159
+ try {
160
+ const nounExists = await this.getNoun_internal(noun.id);
161
+ if (nounExists) {
162
+ console.log(`[CLEANUP] Attempting to remove orphaned noun file ${noun.id}`);
163
+ await this.deleteNoun_internal(noun.id);
164
+ }
165
+ }
166
+ catch (cleanupError) {
167
+ console.error(`[ERROR] Failed to cleanup orphaned noun ${noun.id}:`, cleanupError);
168
+ }
169
+ throw new Error(`Failed to save noun ${noun.id}: ${error instanceof Error ? error.message : String(error)}`);
147
170
  }
148
- return this.saveNoun_internal(noun);
149
171
  }
150
172
  /**
151
173
  * Get a noun from storage
@@ -168,7 +190,16 @@ export class BaseStorage extends BaseStorageAdapter {
168
190
  */
169
191
  async deleteNoun(id) {
170
192
  await this.ensureInitialized();
171
- return this.deleteNoun_internal(id);
193
+ // Delete both the vector file and metadata file (2-file system)
194
+ await this.deleteNoun_internal(id);
195
+ // Delete metadata file (if it exists)
196
+ try {
197
+ await this.deleteNounMetadata(id);
198
+ }
199
+ catch (error) {
200
+ // Ignore if metadata file doesn't exist
201
+ console.debug(`No metadata file to delete for noun ${id}`);
202
+ }
172
203
  }
173
204
  /**
174
205
  * Save a verb to storage
@@ -618,7 +649,16 @@ export class BaseStorage extends BaseStorageAdapter {
618
649
  */
619
650
  async deleteVerb(id) {
620
651
  await this.ensureInitialized();
621
- return this.deleteVerb_internal(id);
652
+ // Delete both the vector file and metadata file (2-file system)
653
+ await this.deleteVerb_internal(id);
654
+ // Delete metadata file (if it exists)
655
+ try {
656
+ await this.deleteVerbMetadata(id);
657
+ }
658
+ catch (error) {
659
+ // Ignore if metadata file doesn't exist
660
+ console.debug(`No metadata file to delete for verb ${id}`);
661
+ }
622
662
  }
623
663
  /**
624
664
  * Get graph index (lazy initialization)
@@ -684,6 +724,15 @@ export class BaseStorage extends BaseStorageAdapter {
684
724
  const keyInfo = this.analyzeKey(id, 'noun-metadata');
685
725
  return this.readObjectFromPath(keyInfo.fullPath);
686
726
  }
727
+ /**
728
+ * Delete noun metadata from storage
729
+ * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
730
+ */
731
+ async deleteNounMetadata(id) {
732
+ await this.ensureInitialized();
733
+ const keyInfo = this.analyzeKey(id, 'noun-metadata');
734
+ return this.deleteObjectFromPath(keyInfo.fullPath);
735
+ }
687
736
  /**
688
737
  * Save verb metadata to storage
689
738
  * Routes to correct sharded location based on UUID
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.36.1",
3
+ "version": "3.37.1",
4
4
  "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",