@soulcraft/brainy 3.50.0 β†’ 3.50.2

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,85 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/soulcraftlabs/standard-version) for commit guidelines.
4
4
 
5
+ ### [3.50.2](https://github.com/soulcraftlabs/brainy/compare/v3.50.1...v3.50.2) (2025-10-16)
6
+
7
+ ### πŸ› Critical Bug Fix - Emergency Hotfix for v3.50.1
8
+
9
+ **Fixed: v3.50.1 Incomplete Fix - Numeric Field Names Still Being Indexed**
10
+
11
+ **Issue**: v3.50.1 prevented vector fields by name ('vector', 'embedding') but missed vectors stored as objects with numeric keys:
12
+ - Studio team diagnostic showed **212,531 chunk files** still being created
13
+ - Files had numeric field names: `"field": "54716"`, `"field": "100000"`, `"field": "100001"`
14
+ - Total file count: **424,837 files** (expected ~1,200)
15
+ - Root cause: Vectors stored as objects `{0: 0.1, 1: 0.2, ...}` bypassed v3.50.1's field name check
16
+
17
+ **Impact**:
18
+ - βœ… File reduction: 424,837 β†’ ~1,200 files (354x reduction)
19
+ - βœ… Prevents 212K+ chunk files from being created
20
+ - βœ… Fixes server hangs during initialization
21
+ - βœ… Completes the metadata explosion fix started in v3.50.1
22
+
23
+ **Solution**:
24
+ - Added regex check in `extractIndexableFields()`: `if (/^\d+$/.test(key)) continue`
25
+ - Skips ANY purely numeric field name (array indices as object keys)
26
+ - Catches: "0", "1", "2", "100", "54716", "100000", etc.
27
+ - Works in combination with v3.50.1's semantic field name checks
28
+
29
+ **Test Results**:
30
+ - βœ… Added new test: "should NOT index objects with numeric keys (v3.50.2 fix)"
31
+ - βœ… 8/8 integration tests passing
32
+ - βœ… Verifies NO chunk files have numeric field names
33
+
34
+ **Files Modified**:
35
+ - `src/utils/metadataIndex.ts` (line 1106) - Added numeric field name check
36
+ - `tests/integration/metadata-vector-exclusion.test.ts` - Added v3.50.2 test case
37
+
38
+ **For Studio Team**:
39
+ After upgrading to v3.50.2:
40
+ 1. Delete `_system/` directory to remove corrupted chunk files
41
+ 2. Restart server - metadata index will rebuild correctly
42
+ 3. File count should normalize to ~1,200 total (from 424,837)
43
+
44
+ ---
45
+
46
+ ### [3.50.1](https://github.com/soulcraftlabs/brainy/compare/v3.50.0...v3.50.1) (2025-10-16)
47
+
48
+ ### πŸ› Critical Bug Fixes
49
+
50
+ **Fixed: Metadata Explosion Bug - 69K Files Reduced to ~1K**
51
+
52
+ **Issue**: Metadata indexing was creating 60+ chunk files per entity (69,429 files for 1,143 entities)
53
+ - Root cause: Vector embeddings (384-dimensional arrays) were being indexed in metadata
54
+ - Each vector dimension created a separate chunk file with numeric field names
55
+ - Caused server hangs, VFS operations timing out, and Graph View UI failures
56
+
57
+ **Impact**:
58
+ - βœ… File reduction: 69,429 β†’ ~1,200 files (58x reduction / 1,200x per entity)
59
+ - βœ… Storage reduction: 3.3GB β†’ ~10MB metadata (330x reduction)
60
+ - βœ… Fixes server initialization hangs (loading 69K files)
61
+ - βœ… Fixes metadata batch loading stalling at batch 23
62
+ - βœ… Fixes VFS getDescendants() hanging indefinitely
63
+ - βœ… Fixes Graph View UI not loading in Soulcraft Studio
64
+
65
+ **Solution**:
66
+ - Added `NEVER_INDEX` Set excluding vector field names: `['vector', 'embedding', 'embeddings', 'connections']`
67
+ - Added safety check to skip arrays > 10 elements
68
+ - Preserves small array indexing (tags, categories, roles)
69
+
70
+ **Test Results**:
71
+ - βœ… 7/7 integration tests passing
72
+ - βœ… Verified: 6 chunk files for 10 entities (was 7,210 before fix)
73
+ - βœ… 611/622 unit tests passing
74
+
75
+ **Files Modified**:
76
+ - `src/utils/metadataIndex.ts` - Core metadata explosion fix
77
+ - `src/coreTypes.ts` - HNSWVerb type enforcement with VerbType enum
78
+ - `src/storage/adapters/*` - Include core relational fields (verb, sourceId, targetId)
79
+ - `src/storage/adapters/baseStorageAdapter.ts` - Type enforcement (HNSWNoun, GraphVerb)
80
+ - `tests/integration/metadata-vector-exclusion.test.ts` - Comprehensive test coverage
81
+
82
+ ---
83
+
5
84
  ### [3.47.0](https://github.com/soulcraftlabs/brainy/compare/v3.46.0...v3.47.0) (2025-10-15)
6
85
 
7
86
  ### ✨ Features
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Type definitions for the Soulcraft Brainy
3
3
  */
4
+ import type { VerbType } from './types/graphTypes.js';
4
5
  /**
5
6
  * Vector representation - an array of numbers
6
7
  */
@@ -76,12 +77,27 @@ export interface HNSWNoun {
76
77
  }
77
78
  /**
78
79
  * Lightweight verb for HNSW index storage
79
- * Contains only essential data needed for vector operations
80
+ * Contains essential data including core relational fields
81
+ *
82
+ * ARCHITECTURAL FIX (v3.50.1): verb/sourceId/targetId are now first-class fields
83
+ * These are NOT metadata - they're the essence of what a verb IS:
84
+ * - verb: The relationship type (creates, contains, etc.) - needed for routing & display
85
+ * - sourceId: What entity this verb connects FROM - needed for graph traversal
86
+ * - targetId: What entity this verb connects TO - needed for graph traversal
87
+ *
88
+ * Benefits:
89
+ * - ONE file read instead of two for 90% of operations
90
+ * - No type caching needed (type is always available)
91
+ * - Faster graph traversal (source/target immediately available)
92
+ * - Aligns with actual usage patterns
80
93
  */
81
94
  export interface HNSWVerb {
82
95
  id: string;
83
96
  vector: Vector;
84
97
  connections: Map<number, Set<string>>;
98
+ verb: VerbType;
99
+ sourceId: string;
100
+ targetId: string;
85
101
  metadata?: any;
86
102
  }
87
103
  /**
@@ -2,21 +2,21 @@
2
2
  * Base Storage Adapter
3
3
  * Provides common functionality for all storage adapters, including statistics tracking
4
4
  */
5
- import { StatisticsData, StorageAdapter } from '../../coreTypes.js';
5
+ import { StatisticsData, StorageAdapter, HNSWNoun, GraphVerb } from '../../coreTypes.js';
6
6
  /**
7
7
  * Base class for storage adapters that implements statistics tracking
8
8
  */
9
9
  export declare abstract class BaseStorageAdapter implements StorageAdapter {
10
10
  abstract init(): Promise<void>;
11
- abstract saveNoun(noun: any): Promise<void>;
12
- abstract getNoun(id: string): Promise<any | null>;
13
- abstract getNounsByNounType(nounType: string): Promise<any[]>;
11
+ abstract saveNoun(noun: HNSWNoun): Promise<void>;
12
+ abstract getNoun(id: string): Promise<HNSWNoun | null>;
13
+ abstract getNounsByNounType(nounType: string): Promise<HNSWNoun[]>;
14
14
  abstract deleteNoun(id: string): Promise<void>;
15
- abstract saveVerb(verb: any): Promise<void>;
16
- abstract getVerb(id: string): Promise<any | null>;
17
- abstract getVerbsBySource(sourceId: string): Promise<any[]>;
18
- abstract getVerbsByTarget(targetId: string): Promise<any[]>;
19
- abstract getVerbsByType(type: string): Promise<any[]>;
15
+ abstract saveVerb(verb: GraphVerb): Promise<void>;
16
+ abstract getVerb(id: string): Promise<GraphVerb | null>;
17
+ abstract getVerbsBySource(sourceId: string): Promise<GraphVerb[]>;
18
+ abstract getVerbsByTarget(targetId: string): Promise<GraphVerb[]>;
19
+ abstract getVerbsByType(type: string): Promise<GraphVerb[]>;
20
20
  abstract deleteVerb(id: string): Promise<void>;
21
21
  abstract saveMetadata(id: string, metadata: any): Promise<void>;
22
22
  abstract getMetadata(id: string): Promise<any | null>;
@@ -64,7 +64,7 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
64
64
  metadata?: Record<string, any>;
65
65
  };
66
66
  }): Promise<{
67
- items: any[];
67
+ items: HNSWNoun[];
68
68
  totalCount?: number;
69
69
  hasMore: boolean;
70
70
  nextCursor?: string;
@@ -88,7 +88,7 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
88
88
  metadata?: Record<string, any>;
89
89
  };
90
90
  }): Promise<{
91
- items: any[];
91
+ items: GraphVerb[];
92
92
  totalCount?: number;
93
93
  hasMore: boolean;
94
94
  nextCursor?: string;
@@ -108,7 +108,7 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
108
108
  metadata?: Record<string, any>;
109
109
  };
110
110
  }): Promise<{
111
- items: any[];
111
+ items: HNSWNoun[];
112
112
  totalCount?: number;
113
113
  hasMore: boolean;
114
114
  nextCursor?: string;
@@ -130,7 +130,7 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
130
130
  metadata?: Record<string, any>;
131
131
  };
132
132
  }): Promise<{
133
- items: any[];
133
+ items: GraphVerb[];
134
134
  totalCount?: number;
135
135
  hasMore: boolean;
136
136
  nextCursor?: string;
@@ -341,13 +341,18 @@ export class FileSystemStorage extends BaseStorage {
341
341
  // Check if this is a new edge to update counts
342
342
  const isNew = !(await this.fileExists(this.getVerbPath(edge.id)));
343
343
  // Convert connections Map to a serializable format
344
- // CRITICAL: Only save lightweight vector data (no metadata)
345
- // Metadata is saved separately via saveVerbMetadata() (2-file system)
344
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields in verb vector file
345
+ // These fields are essential for 90% of operations - no metadata lookup needed
346
346
  const serializableEdge = {
347
347
  id: edge.id,
348
348
  vector: edge.vector,
349
- connections: this.mapToObject(edge.connections, (set) => Array.from(set))
350
- // NO metadata field - saved separately for scalability
349
+ connections: this.mapToObject(edge.connections, (set) => Array.from(set)),
350
+ // CORE RELATIONAL DATA (v3.50.1+)
351
+ verb: edge.verb,
352
+ sourceId: edge.sourceId,
353
+ targetId: edge.targetId,
354
+ // User metadata (if any) - saved separately for scalability
355
+ // metadata field is saved separately via saveVerbMetadata()
351
356
  };
352
357
  const filePath = this.getVerbPath(edge.id);
353
358
  await this.ensureDirectoryExists(path.dirname(filePath));
@@ -375,10 +380,17 @@ export class FileSystemStorage extends BaseStorage {
375
380
  for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) {
376
381
  connections.set(Number(level), new Set(nodeIds));
377
382
  }
383
+ // ARCHITECTURAL FIX (v3.50.1): Return HNSWVerb with core relational fields
378
384
  return {
379
385
  id: parsedEdge.id,
380
386
  vector: parsedEdge.vector,
381
- connections
387
+ connections,
388
+ // CORE RELATIONAL DATA (read from vector file)
389
+ verb: parsedEdge.verb,
390
+ sourceId: parsedEdge.sourceId,
391
+ targetId: parsedEdge.targetId,
392
+ // User metadata (retrieved separately via getVerbMetadata())
393
+ metadata: parsedEdge.metadata
382
394
  };
383
395
  }
384
396
  catch (error) {
@@ -411,10 +423,17 @@ export class FileSystemStorage extends BaseStorage {
411
423
  for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) {
412
424
  connections.set(Number(level), new Set(nodeIds));
413
425
  }
426
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields
414
427
  allEdges.push({
415
428
  id: parsedEdge.id,
416
429
  vector: parsedEdge.vector,
417
- connections
430
+ connections,
431
+ // CORE RELATIONAL DATA
432
+ verb: parsedEdge.verb,
433
+ sourceId: parsedEdge.sourceId,
434
+ targetId: parsedEdge.targetId,
435
+ // User metadata
436
+ metadata: parsedEdge.metadata
418
437
  });
419
438
  }
420
439
  }
@@ -636,16 +636,21 @@ export class GcsStorage extends BaseStorage {
636
636
  try {
637
637
  this.logger.trace(`Saving edge ${edge.id}`);
638
638
  // Convert connections Map to serializable format
639
- // CRITICAL: Only save lightweight vector data (no metadata)
640
- // Metadata is saved separately via saveVerbMetadata() (2-file system)
639
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields in verb vector file
640
+ // These fields are essential for 90% of operations - no metadata lookup needed
641
641
  const serializableEdge = {
642
642
  id: edge.id,
643
643
  vector: edge.vector,
644
644
  connections: Object.fromEntries(Array.from(edge.connections.entries()).map(([level, verbIds]) => [
645
645
  level,
646
646
  Array.from(verbIds)
647
- ]))
648
- // NO metadata field - saved separately for scalability
647
+ ])),
648
+ // CORE RELATIONAL DATA (v3.50.1+)
649
+ verb: edge.verb,
650
+ sourceId: edge.sourceId,
651
+ targetId: edge.targetId,
652
+ // User metadata (if any) - saved separately for scalability
653
+ // metadata field is saved separately via saveVerbMetadata()
649
654
  };
650
655
  // Get the GCS key with UUID-based sharding
651
656
  const key = this.getVerbKey(edge.id);
@@ -719,10 +724,17 @@ export class GcsStorage extends BaseStorage {
719
724
  for (const [level, verbIds] of Object.entries(data.connections || {})) {
720
725
  connections.set(Number(level), new Set(verbIds));
721
726
  }
727
+ // ARCHITECTURAL FIX (v3.50.1): Return HNSWVerb with core relational fields
722
728
  const edge = {
723
729
  id: data.id,
724
730
  vector: data.vector,
725
- connections
731
+ connections,
732
+ // CORE RELATIONAL DATA (read from vector file)
733
+ verb: data.verb,
734
+ sourceId: data.sourceId,
735
+ targetId: data.targetId,
736
+ // User metadata (retrieved separately via getVerbMetadata())
737
+ metadata: data.metadata
726
738
  };
727
739
  // Update cache
728
740
  this.verbCacheManager.set(id, edge);
@@ -226,10 +226,17 @@ export class MemoryStorage extends BaseStorage {
226
226
  async saveVerb_internal(verb) {
227
227
  const isNew = !this.verbs.has(verb.id);
228
228
  // Create a deep copy to avoid reference issues
229
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields
229
230
  const verbCopy = {
230
231
  id: verb.id,
231
232
  vector: [...verb.vector],
232
- connections: new Map()
233
+ connections: new Map(),
234
+ // CORE RELATIONAL DATA
235
+ verb: verb.verb,
236
+ sourceId: verb.sourceId,
237
+ targetId: verb.targetId,
238
+ // User metadata (if any)
239
+ metadata: verb.metadata
233
240
  };
234
241
  // Copy connections
235
242
  for (const [level, connections] of verb.connections.entries()) {
@@ -252,22 +259,23 @@ export class MemoryStorage extends BaseStorage {
252
259
  return null;
253
260
  }
254
261
  // Return a deep copy of the HNSWVerb
262
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields
255
263
  const verbCopy = {
256
264
  id: verb.id,
257
265
  vector: [...verb.vector],
258
- connections: new Map()
266
+ connections: new Map(),
267
+ // CORE RELATIONAL DATA
268
+ verb: verb.verb,
269
+ sourceId: verb.sourceId,
270
+ targetId: verb.targetId,
271
+ // User metadata
272
+ metadata: verb.metadata
259
273
  };
260
274
  // Copy connections
261
275
  for (const [level, connections] of verb.connections.entries()) {
262
276
  verbCopy.connections.set(level, new Set(connections));
263
277
  }
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
- };
278
+ return verbCopy;
271
279
  }
272
280
  /**
273
281
  * Get verbs with pagination and filtering
@@ -311,13 +311,18 @@ export class OPFSStorage extends BaseStorage {
311
311
  async saveEdge(edge) {
312
312
  await this.ensureInitialized();
313
313
  try {
314
- // CRITICAL: Only save lightweight vector data (no metadata)
315
- // Metadata is saved separately via saveVerbMetadata() (2-file system)
314
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields in verb vector file
315
+ // These fields are essential for 90% of operations - no metadata lookup needed
316
316
  const serializableEdge = {
317
317
  id: edge.id,
318
318
  vector: edge.vector,
319
- connections: this.mapToObject(edge.connections, (set) => Array.from(set))
320
- // NO metadata field - saved separately for scalability
319
+ connections: this.mapToObject(edge.connections, (set) => Array.from(set)),
320
+ // CORE RELATIONAL DATA (v3.50.1+)
321
+ verb: edge.verb,
322
+ sourceId: edge.sourceId,
323
+ targetId: edge.targetId,
324
+ // User metadata (if any) - saved separately for scalability
325
+ // metadata field is saved separately via saveVerbMetadata()
321
326
  };
322
327
  // Use UUID-based sharding for verbs
323
328
  const shardId = getShardIdFromUuid(edge.id);
@@ -388,10 +393,17 @@ export class OPFSStorage extends BaseStorage {
388
393
  augmentation: 'unknown',
389
394
  version: '1.0'
390
395
  };
396
+ // ARCHITECTURAL FIX (v3.50.1): Return HNSWVerb with core relational fields
391
397
  return {
392
398
  id: data.id,
393
399
  vector: data.vector,
394
- connections
400
+ connections,
401
+ // CORE RELATIONAL DATA (read from vector file)
402
+ verb: data.verb,
403
+ sourceId: data.sourceId,
404
+ targetId: data.targetId,
405
+ // User metadata (retrieved separately via getVerbMetadata())
406
+ metadata: data.metadata
395
407
  };
396
408
  }
397
409
  catch (error) {
@@ -433,10 +445,17 @@ export class OPFSStorage extends BaseStorage {
433
445
  augmentation: 'unknown',
434
446
  version: '1.0'
435
447
  };
448
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields
436
449
  allEdges.push({
437
450
  id: data.id,
438
451
  vector: data.vector,
439
- connections
452
+ connections,
453
+ // CORE RELATIONAL DATA
454
+ verb: data.verb,
455
+ sourceId: data.sourceId,
456
+ targetId: data.targetId,
457
+ // User metadata
458
+ metadata: data.metadata
440
459
  });
441
460
  }
442
461
  catch (error) {
@@ -549,13 +549,21 @@ export class R2Storage extends BaseStorage {
549
549
  async saveEdgeDirect(edge) {
550
550
  const requestId = await this.applyBackpressure();
551
551
  try {
552
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields in verb vector file
553
+ // These fields are essential for 90% of operations - no metadata lookup needed
552
554
  const serializableEdge = {
553
555
  id: edge.id,
554
556
  vector: edge.vector,
555
557
  connections: Object.fromEntries(Array.from(edge.connections.entries()).map(([level, verbIds]) => [
556
558
  level,
557
559
  Array.from(verbIds)
558
- ]))
560
+ ])),
561
+ // CORE RELATIONAL DATA (v3.50.1+)
562
+ verb: edge.verb,
563
+ sourceId: edge.sourceId,
564
+ targetId: edge.targetId,
565
+ // User metadata (if any) - saved separately for scalability
566
+ // metadata field is saved separately via saveVerbMetadata()
559
567
  };
560
568
  const key = this.getVerbKey(edge.id);
561
569
  const { PutObjectCommand } = await import('@aws-sdk/client-s3');
@@ -612,10 +620,17 @@ export class R2Storage extends BaseStorage {
612
620
  for (const [level, verbIds] of Object.entries(data.connections || {})) {
613
621
  connections.set(Number(level), new Set(verbIds));
614
622
  }
623
+ // ARCHITECTURAL FIX (v3.50.1): Return HNSWVerb with core relational fields
615
624
  const edge = {
616
625
  id: data.id,
617
626
  vector: data.vector,
618
- connections
627
+ connections,
628
+ // CORE RELATIONAL DATA (read from vector file)
629
+ verb: data.verb,
630
+ sourceId: data.sourceId,
631
+ targetId: data.targetId,
632
+ // User metadata (retrieved separately via getVerbMetadata())
633
+ metadata: data.metadata
619
634
  };
620
635
  this.verbCacheManager.set(id, edge);
621
636
  this.releaseBackpressure(true, requestId);
@@ -1179,10 +1179,15 @@ export class S3CompatibleStorage extends BaseStorage {
1179
1179
  // Convert connections Map to a serializable format
1180
1180
  // CRITICAL: Only save lightweight vector data (no metadata)
1181
1181
  // Metadata is saved separately via saveVerbMetadata() (2-file system)
1182
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields in verb vector file
1182
1183
  const serializableEdge = {
1183
1184
  id: edge.id,
1184
1185
  vector: edge.vector,
1185
- connections: this.mapToObject(edge.connections, (set) => Array.from(set))
1186
+ connections: this.mapToObject(edge.connections, (set) => Array.from(set)),
1187
+ // CORE RELATIONAL DATA (v3.50.1+)
1188
+ verb: edge.verb,
1189
+ sourceId: edge.sourceId,
1190
+ targetId: edge.targetId,
1186
1191
  // NO metadata field - saved separately for scalability
1187
1192
  };
1188
1193
  // Import the PutObjectCommand only when needed
@@ -1279,10 +1284,17 @@ export class S3CompatibleStorage extends BaseStorage {
1279
1284
  for (const [level, nodeIds] of Object.entries(parsedEdge.connections)) {
1280
1285
  connections.set(Number(level), new Set(nodeIds));
1281
1286
  }
1287
+ // ARCHITECTURAL FIX (v3.50.1): Return HNSWVerb with core relational fields
1282
1288
  const edge = {
1283
1289
  id: parsedEdge.id,
1284
1290
  vector: parsedEdge.vector,
1285
- connections
1291
+ connections,
1292
+ // CORE RELATIONAL DATA (read from vector file)
1293
+ verb: parsedEdge.verb,
1294
+ sourceId: parsedEdge.sourceId,
1295
+ targetId: parsedEdge.targetId,
1296
+ // User metadata (retrieved separately via getVerbMetadata())
1297
+ metadata: parsedEdge.metadata
1286
1298
  };
1287
1299
  this.logger.trace(`Successfully retrieved edge ${id}`);
1288
1300
  return edge;
@@ -72,7 +72,9 @@ export declare class TypeAwareStorageAdapter extends BaseStorage {
72
72
  */
73
73
  private getNounType;
74
74
  /**
75
- * Get verb type from verb object or cache
75
+ * Get verb type from verb object
76
+ *
77
+ * ARCHITECTURAL FIX (v3.50.1): Simplified - verb field is now always present
76
78
  */
77
79
  private getVerbType;
78
80
  /**
@@ -93,10 +95,16 @@ export declare class TypeAwareStorageAdapter extends BaseStorage {
93
95
  protected deleteNoun_internal(id: string): Promise<void>;
94
96
  /**
95
97
  * Save verb (type-first path)
98
+ *
99
+ * ARCHITECTURAL FIX (v3.50.1): No more caching hack needed!
100
+ * HNSWVerb now includes verb field, so type is always available
96
101
  */
97
102
  protected saveVerb_internal(verb: HNSWVerb): Promise<void>;
98
103
  /**
99
104
  * Get verb (type-first path)
105
+ *
106
+ * ARCHITECTURAL FIX (v3.50.1): Cache still useful for performance
107
+ * Once we know where a verb is, we can retrieve it O(1) instead of searching all types
100
108
  */
101
109
  protected getVerb_internal(id: string): Promise<HNSWVerb | null>;
102
110
  /**
@@ -109,6 +117,8 @@ export declare class TypeAwareStorageAdapter extends BaseStorage {
109
117
  protected getVerbsByTarget_internal(targetId: string): Promise<GraphVerb[]>;
110
118
  /**
111
119
  * Get verbs by type (O(1) with type-first paths!)
120
+ *
121
+ * ARCHITECTURAL FIX (v3.50.1): Type is now in HNSWVerb, cached on read
112
122
  */
113
123
  protected getVerbsByType_internal(verbType: string): Promise<GraphVerb[]>;
114
124
  /**
@@ -153,24 +153,21 @@ export class TypeAwareStorageAdapter extends BaseStorage {
153
153
  return 'thing';
154
154
  }
155
155
  /**
156
- * Get verb type from verb object or cache
156
+ * Get verb type from verb object
157
+ *
158
+ * ARCHITECTURAL FIX (v3.50.1): Simplified - verb field is now always present
157
159
  */
158
160
  getVerbType(verb) {
159
- // Try verb property first
161
+ // v3.50.1+: verb is a required field in HNSWVerb
160
162
  if ('verb' in verb && verb.verb) {
161
163
  return verb.verb;
162
164
  }
163
- // Try type property
165
+ // Fallback for GraphVerb (type alias)
164
166
  if ('type' in verb && verb.type) {
165
167
  return verb.type;
166
168
  }
167
- // Try cache
168
- const cached = this.verbTypeCache.get(verb.id);
169
- if (cached) {
170
- return cached;
171
- }
172
- // Default to 'relatedTo' if unknown
173
- console.warn(`[TypeAwareStorage] Unknown verb type for ${verb.id}, defaulting to 'relatedTo'`);
169
+ // This should never happen with v3.50.1+ data
170
+ console.warn(`[TypeAwareStorage] Verb missing type field for ${verb.id}, defaulting to 'relatedTo'`);
174
171
  return 'relatedTo';
175
172
  }
176
173
  // ============================================================================
@@ -283,9 +280,13 @@ export class TypeAwareStorageAdapter extends BaseStorage {
283
280
  }
284
281
  /**
285
282
  * Save verb (type-first path)
283
+ *
284
+ * ARCHITECTURAL FIX (v3.50.1): No more caching hack needed!
285
+ * HNSWVerb now includes verb field, so type is always available
286
286
  */
287
287
  async saveVerb_internal(verb) {
288
- const type = this.getVerbType(verb);
288
+ // Type is now a first-class field in HNSWVerb - no caching needed!
289
+ const type = verb.verb;
289
290
  const path = getVerbVectorPath(type, verb.id);
290
291
  // Update type tracking
291
292
  const typeIndex = TypeUtils.getVerbIndex(type);
@@ -300,22 +301,27 @@ export class TypeAwareStorageAdapter extends BaseStorage {
300
301
  }
301
302
  /**
302
303
  * Get verb (type-first path)
304
+ *
305
+ * ARCHITECTURAL FIX (v3.50.1): Cache still useful for performance
306
+ * Once we know where a verb is, we can retrieve it O(1) instead of searching all types
303
307
  */
304
308
  async getVerb_internal(id) {
305
- // Try cache first
309
+ // Try cache first for O(1) retrieval
306
310
  const cachedType = this.verbTypeCache.get(id);
307
311
  if (cachedType) {
308
312
  const path = getVerbVectorPath(cachedType, id);
309
- return await this.u.readObjectFromPath(path);
313
+ const verb = await this.u.readObjectFromPath(path);
314
+ return verb;
310
315
  }
311
- // Search across all types
316
+ // Search across all types (only on first access)
312
317
  for (let i = 0; i < VERB_TYPE_COUNT; i++) {
313
318
  const type = TypeUtils.getVerbFromIndex(i);
314
319
  const path = getVerbVectorPath(type, id);
315
320
  try {
316
321
  const verb = await this.u.readObjectFromPath(path);
317
322
  if (verb) {
318
- this.verbTypeCache.set(id, type);
323
+ // Cache the type for next time (read from verb.verb field)
324
+ this.verbTypeCache.set(id, verb.verb);
319
325
  return verb;
320
326
  }
321
327
  }
@@ -389,6 +395,8 @@ export class TypeAwareStorageAdapter extends BaseStorage {
389
395
  }
390
396
  /**
391
397
  * Get verbs by type (O(1) with type-first paths!)
398
+ *
399
+ * ARCHITECTURAL FIX (v3.50.1): Type is now in HNSWVerb, cached on read
392
400
  */
393
401
  async getVerbsByType_internal(verbType) {
394
402
  const type = verbType;
@@ -399,11 +407,12 @@ export class TypeAwareStorageAdapter extends BaseStorage {
399
407
  try {
400
408
  const hnswVerb = await this.u.readObjectFromPath(path);
401
409
  if (hnswVerb) {
410
+ // Cache type from HNSWVerb for future O(1) retrievals
411
+ this.verbTypeCache.set(hnswVerb.id, hnswVerb.verb);
402
412
  // Convert to GraphVerb
403
413
  const graphVerb = await this.convertHNSWVerbToGraphVerb(hnswVerb);
404
414
  if (graphVerb) {
405
415
  verbs.push(graphVerb);
406
- this.verbTypeCache.set(hnswVerb.id, type);
407
416
  }
408
417
  }
409
418
  }
@@ -71,6 +71,10 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
71
71
  deleteNoun(id: string): Promise<void>;
72
72
  /**
73
73
  * Save a verb to storage
74
+ *
75
+ * ARCHITECTURAL FIX (v3.50.1): HNSWVerb now includes verb/sourceId/targetId
76
+ * These are core relational fields, not metadata. They're stored in the vector
77
+ * file for fast access and to align with actual usage patterns.
74
78
  */
75
79
  saveVerb(verb: GraphVerb): Promise<void>;
76
80
  /**
@@ -79,6 +83,9 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
79
83
  getVerb(id: string): Promise<GraphVerb | null>;
80
84
  /**
81
85
  * Convert HNSWVerb to GraphVerb by combining with metadata
86
+ *
87
+ * ARCHITECTURAL FIX (v3.50.1): Core fields (verb/sourceId/targetId) are now in HNSWVerb
88
+ * Only optional fields (weight, timestamps, etc.) come from metadata file
82
89
  */
83
90
  protected convertHNSWVerbToGraphVerb(hnswVerb: HNSWVerb): Promise<GraphVerb | null>;
84
91
  /**
@@ -205,6 +205,10 @@ export class BaseStorage extends BaseStorageAdapter {
205
205
  }
206
206
  /**
207
207
  * Save a verb to storage
208
+ *
209
+ * ARCHITECTURAL FIX (v3.50.1): HNSWVerb now includes verb/sourceId/targetId
210
+ * These are core relational fields, not metadata. They're stored in the vector
211
+ * file for fast access and to align with actual usage patterns.
208
212
  */
209
213
  async saveVerb(verb) {
210
214
  await this.ensureInitialized();
@@ -212,27 +216,29 @@ export class BaseStorage extends BaseStorageAdapter {
212
216
  if (verb.verb) {
213
217
  validateVerbType(verb.verb);
214
218
  }
215
- // Extract the lightweight HNSWVerb data
219
+ // Extract HNSWVerb with CORE relational fields included
216
220
  const hnswVerb = {
217
221
  id: verb.id,
218
222
  vector: verb.vector,
219
- connections: verb.connections || new Map()
223
+ connections: verb.connections || new Map(),
224
+ // CORE RELATIONAL DATA (v3.50.1+)
225
+ verb: (verb.verb || verb.type || 'relatedTo'),
226
+ sourceId: verb.sourceId || verb.source || '',
227
+ targetId: verb.targetId || verb.target || '',
228
+ // User metadata (if any)
229
+ metadata: verb.metadata
220
230
  };
221
- // Extract and save the metadata separately
231
+ // Extract lightweight metadata for separate file (optional fields only)
222
232
  const metadata = {
223
- sourceId: verb.sourceId || verb.source,
224
- targetId: verb.targetId || verb.target,
225
- source: verb.source || verb.sourceId,
226
- target: verb.target || verb.targetId,
227
- type: verb.type || verb.verb,
228
- verb: verb.verb || verb.type,
229
233
  weight: verb.weight,
230
- metadata: verb.metadata,
231
234
  data: verb.data,
232
235
  createdAt: verb.createdAt,
233
236
  updatedAt: verb.updatedAt,
234
237
  createdBy: verb.createdBy,
235
- embedding: verb.embedding
238
+ // Legacy aliases for backward compatibility
239
+ source: verb.source || verb.sourceId,
240
+ target: verb.target || verb.targetId,
241
+ type: verb.type || verb.verb
236
242
  };
237
243
  // Save both the HNSWVerb and metadata atomically
238
244
  try {
@@ -273,13 +279,14 @@ export class BaseStorage extends BaseStorageAdapter {
273
279
  }
274
280
  /**
275
281
  * Convert HNSWVerb to GraphVerb by combining with metadata
282
+ *
283
+ * ARCHITECTURAL FIX (v3.50.1): Core fields (verb/sourceId/targetId) are now in HNSWVerb
284
+ * Only optional fields (weight, timestamps, etc.) come from metadata file
276
285
  */
277
286
  async convertHNSWVerbToGraphVerb(hnswVerb) {
278
287
  try {
288
+ // Metadata file is now optional - contains only weight, timestamps, etc.
279
289
  const metadata = await this.getVerbMetadata(hnswVerb.id);
280
- if (!metadata) {
281
- return null;
282
- }
283
290
  // Create default timestamp if not present
284
291
  const defaultTimestamp = {
285
292
  seconds: Math.floor(Date.now() / 1000),
@@ -293,18 +300,21 @@ export class BaseStorage extends BaseStorageAdapter {
293
300
  return {
294
301
  id: hnswVerb.id,
295
302
  vector: hnswVerb.vector,
296
- sourceId: metadata.sourceId,
297
- targetId: metadata.targetId,
298
- source: metadata.source,
299
- target: metadata.target,
300
- verb: metadata.verb,
301
- type: metadata.type,
302
- weight: metadata.weight || 1.0,
303
- metadata: metadata.metadata || {},
304
- createdAt: metadata.createdAt || defaultTimestamp,
305
- updatedAt: metadata.updatedAt || defaultTimestamp,
306
- createdBy: metadata.createdBy || defaultCreatedBy,
307
- data: metadata.data,
303
+ // CORE FIELDS from HNSWVerb (v3.50.1+)
304
+ verb: hnswVerb.verb,
305
+ sourceId: hnswVerb.sourceId,
306
+ targetId: hnswVerb.targetId,
307
+ // Aliases for backward compatibility
308
+ type: hnswVerb.verb,
309
+ source: hnswVerb.sourceId,
310
+ target: hnswVerb.targetId,
311
+ // Optional fields from metadata file
312
+ weight: metadata?.weight || 1.0,
313
+ metadata: hnswVerb.metadata || {},
314
+ createdAt: metadata?.createdAt || defaultTimestamp,
315
+ updatedAt: metadata?.updatedAt || defaultTimestamp,
316
+ createdBy: metadata?.createdBy || defaultCreatedBy,
317
+ data: metadata?.data,
308
318
  embedding: hnswVerb.vector
309
319
  };
310
320
  }
@@ -324,12 +334,19 @@ export class BaseStorage extends BaseStorageAdapter {
324
334
  pagination: { limit: Number.MAX_SAFE_INTEGER }
325
335
  });
326
336
  // Convert GraphVerbs back to HNSWVerbs for internal use
337
+ // ARCHITECTURAL FIX (v3.50.1): Include core relational fields
327
338
  const hnswVerbs = [];
328
339
  for (const graphVerb of result.items) {
329
340
  const hnswVerb = {
330
341
  id: graphVerb.id,
331
342
  vector: graphVerb.vector,
332
- connections: new Map()
343
+ connections: new Map(),
344
+ // CORE RELATIONAL DATA
345
+ verb: (graphVerb.verb || graphVerb.type || 'relatedTo'),
346
+ sourceId: graphVerb.sourceId || graphVerb.source || '',
347
+ targetId: graphVerb.targetId || graphVerb.target || '',
348
+ // User metadata
349
+ metadata: graphVerb.metadata
333
350
  };
334
351
  hnswVerbs.push(hnswVerb);
335
352
  }
@@ -228,6 +228,11 @@ export declare class MetadataIndexManager {
228
228
  private shouldIndexField;
229
229
  /**
230
230
  * Extract indexable field-value pairs from metadata
231
+ *
232
+ * BUG FIX (v3.50.1): Exclude vector embeddings and large arrays from indexing
233
+ * BUG FIX (v3.50.2): Also exclude purely numeric field names (array indices)
234
+ * - Vector fields (384+ dimensions) were creating 825K chunk files for 1,144 entities
235
+ * - Arrays converted to objects with numeric keys were still being indexed
231
236
  */
232
237
  private extractIndexableFields;
233
238
  /**
@@ -849,28 +849,51 @@ export class MetadataIndexManager {
849
849
  }
850
850
  /**
851
851
  * Extract indexable field-value pairs from metadata
852
+ *
853
+ * BUG FIX (v3.50.1): Exclude vector embeddings and large arrays from indexing
854
+ * BUG FIX (v3.50.2): Also exclude purely numeric field names (array indices)
855
+ * - Vector fields (384+ dimensions) were creating 825K chunk files for 1,144 entities
856
+ * - Arrays converted to objects with numeric keys were still being indexed
852
857
  */
853
858
  extractIndexableFields(metadata) {
854
859
  const fields = [];
860
+ // Fields that should NEVER be indexed (vectors, embeddings, large arrays)
861
+ const NEVER_INDEX = new Set(['vector', 'embedding', 'embeddings', 'connections']);
855
862
  const extract = (obj, prefix = '') => {
856
863
  for (const [key, value] of Object.entries(obj)) {
857
864
  const fullKey = prefix ? `${prefix}.${key}` : key;
865
+ // Skip fields in never-index list (CRITICAL: prevents vector indexing bug)
866
+ if (NEVER_INDEX.has(key))
867
+ continue;
868
+ // Skip purely numeric field names (array indices converted to object keys)
869
+ // Legitimate field names should never be purely numeric
870
+ // This catches vectors stored as objects: {0: 0.1, 1: 0.2, ...}
871
+ if (/^\d+$/.test(key))
872
+ continue;
873
+ // Skip fields based on user configuration
858
874
  if (!this.shouldIndexField(fullKey))
859
875
  continue;
876
+ // Skip large arrays (> 10 elements) - likely vectors or bulk data
877
+ if (Array.isArray(value) && value.length > 10)
878
+ continue;
860
879
  if (value && typeof value === 'object' && !Array.isArray(value)) {
861
- // Recurse into nested objects
880
+ // Recurse into nested objects (but not arrays)
862
881
  extract(value, fullKey);
863
882
  }
864
- else {
865
- // Index this field
866
- fields.push({ field: fullKey, value });
867
- // If it's an array, also index each element
868
- if (Array.isArray(value)) {
869
- for (const item of value) {
883
+ else if (Array.isArray(value) && value.length <= 10) {
884
+ // Small arrays: index as multi-value field (all with same field name)
885
+ // Example: tags: ["javascript", "node"] β†’ field="tags", value="javascript" + field="tags", value="node"
886
+ for (const item of value) {
887
+ // Only index primitive values (not nested objects/arrays)
888
+ if (item !== null && typeof item !== 'object') {
870
889
  fields.push({ field: fullKey, value: item });
871
890
  }
872
891
  }
873
892
  }
893
+ else {
894
+ // Primitive value: index it
895
+ fields.push({ field: fullKey, value });
896
+ }
874
897
  }
875
898
  };
876
899
  if (metadata && typeof metadata === 'object') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "3.50.0",
3
+ "version": "3.50.2",
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",