@soulcraft/brainy 3.50.0 β 3.50.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 +38 -0
- package/dist/coreTypes.d.ts +17 -1
- package/dist/storage/adapters/baseStorageAdapter.d.ts +13 -13
- package/dist/storage/adapters/fileSystemStorage.js +25 -6
- package/dist/storage/adapters/gcsStorage.js +17 -5
- package/dist/storage/adapters/memoryStorage.js +17 -9
- package/dist/storage/adapters/opfsStorage.js +25 -6
- package/dist/storage/adapters/r2Storage.js +17 -2
- package/dist/storage/adapters/s3CompatibleStorage.js +14 -2
- package/dist/storage/adapters/typeAwareStorageAdapter.d.ts +11 -1
- package/dist/storage/adapters/typeAwareStorageAdapter.js +25 -16
- package/dist/storage/baseStorage.d.ts +7 -0
- package/dist/storage/baseStorage.js +44 -27
- package/dist/utils/metadataIndex.d.ts +4 -0
- package/dist/utils/metadataIndex.js +24 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
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.1](https://github.com/soulcraftlabs/brainy/compare/v3.50.0...v3.50.1) (2025-10-16)
|
|
6
|
+
|
|
7
|
+
### π Critical Bug Fixes
|
|
8
|
+
|
|
9
|
+
**Fixed: Metadata Explosion Bug - 69K Files Reduced to ~1K**
|
|
10
|
+
|
|
11
|
+
**Issue**: Metadata indexing was creating 60+ chunk files per entity (69,429 files for 1,143 entities)
|
|
12
|
+
- Root cause: Vector embeddings (384-dimensional arrays) were being indexed in metadata
|
|
13
|
+
- Each vector dimension created a separate chunk file with numeric field names
|
|
14
|
+
- Caused server hangs, VFS operations timing out, and Graph View UI failures
|
|
15
|
+
|
|
16
|
+
**Impact**:
|
|
17
|
+
- β
File reduction: 69,429 β ~1,200 files (58x reduction / 1,200x per entity)
|
|
18
|
+
- β
Storage reduction: 3.3GB β ~10MB metadata (330x reduction)
|
|
19
|
+
- β
Fixes server initialization hangs (loading 69K files)
|
|
20
|
+
- β
Fixes metadata batch loading stalling at batch 23
|
|
21
|
+
- β
Fixes VFS getDescendants() hanging indefinitely
|
|
22
|
+
- β
Fixes Graph View UI not loading in Soulcraft Studio
|
|
23
|
+
|
|
24
|
+
**Solution**:
|
|
25
|
+
- Added `NEVER_INDEX` Set excluding vector field names: `['vector', 'embedding', 'embeddings', 'connections']`
|
|
26
|
+
- Added safety check to skip arrays > 10 elements
|
|
27
|
+
- Preserves small array indexing (tags, categories, roles)
|
|
28
|
+
|
|
29
|
+
**Test Results**:
|
|
30
|
+
- β
7/7 integration tests passing
|
|
31
|
+
- β
Verified: 6 chunk files for 10 entities (was 7,210 before fix)
|
|
32
|
+
- β
611/622 unit tests passing
|
|
33
|
+
|
|
34
|
+
**Files Modified**:
|
|
35
|
+
- `src/utils/metadataIndex.ts` - Core metadata explosion fix
|
|
36
|
+
- `src/coreTypes.ts` - HNSWVerb type enforcement with VerbType enum
|
|
37
|
+
- `src/storage/adapters/*` - Include core relational fields (verb, sourceId, targetId)
|
|
38
|
+
- `src/storage/adapters/baseStorageAdapter.ts` - Type enforcement (HNSWNoun, GraphVerb)
|
|
39
|
+
- `tests/integration/metadata-vector-exclusion.test.ts` - Comprehensive test coverage
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
5
43
|
### [3.47.0](https://github.com/soulcraftlabs/brainy/compare/v3.46.0...v3.47.0) (2025-10-15)
|
|
6
44
|
|
|
7
45
|
### β¨ Features
|
package/dist/coreTypes.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
12
|
-
abstract getNoun(id: string): Promise<
|
|
13
|
-
abstract getNounsByNounType(nounType: string): Promise<
|
|
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:
|
|
16
|
-
abstract getVerb(id: string): Promise<
|
|
17
|
-
abstract getVerbsBySource(sourceId: string): Promise<
|
|
18
|
-
abstract getVerbsByTarget(targetId: string): Promise<
|
|
19
|
-
abstract getVerbsByType(type: string): Promise<
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
//
|
|
345
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
640
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
315
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
165
|
+
// Fallback for GraphVerb (type alias)
|
|
164
166
|
if ('type' in verb && verb.type) {
|
|
165
167
|
return verb.type;
|
|
166
168
|
}
|
|
167
|
-
//
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
type:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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,10 @@ 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
|
+
* - Vector fields (384+ dimensions) were creating 825K chunk files for 1,144 entities
|
|
234
|
+
* - Arrays should not have their indices indexed as separate fields
|
|
231
235
|
*/
|
|
232
236
|
private extractIndexableFields;
|
|
233
237
|
/**
|
|
@@ -849,28 +849,45 @@ 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
|
+
* - Vector fields (384+ dimensions) were creating 825K chunk files for 1,144 entities
|
|
855
|
+
* - Arrays should not have their indices indexed as separate fields
|
|
852
856
|
*/
|
|
853
857
|
extractIndexableFields(metadata) {
|
|
854
858
|
const fields = [];
|
|
859
|
+
// Fields that should NEVER be indexed (vectors, embeddings, large arrays)
|
|
860
|
+
const NEVER_INDEX = new Set(['vector', 'embedding', 'embeddings', 'connections']);
|
|
855
861
|
const extract = (obj, prefix = '') => {
|
|
856
862
|
for (const [key, value] of Object.entries(obj)) {
|
|
857
863
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
864
|
+
// Skip fields in never-index list (CRITICAL: prevents vector indexing bug)
|
|
865
|
+
if (NEVER_INDEX.has(key))
|
|
866
|
+
continue;
|
|
867
|
+
// Skip fields based on user configuration
|
|
858
868
|
if (!this.shouldIndexField(fullKey))
|
|
859
869
|
continue;
|
|
870
|
+
// Skip large arrays (> 10 elements) - likely vectors or bulk data
|
|
871
|
+
if (Array.isArray(value) && value.length > 10)
|
|
872
|
+
continue;
|
|
860
873
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
861
|
-
// Recurse into nested objects
|
|
874
|
+
// Recurse into nested objects (but not arrays)
|
|
862
875
|
extract(value, fullKey);
|
|
863
876
|
}
|
|
864
|
-
else {
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
877
|
+
else if (Array.isArray(value) && value.length <= 10) {
|
|
878
|
+
// Small arrays: index as multi-value field (all with same field name)
|
|
879
|
+
// Example: tags: ["javascript", "node"] β field="tags", value="javascript" + field="tags", value="node"
|
|
880
|
+
for (const item of value) {
|
|
881
|
+
// Only index primitive values (not nested objects/arrays)
|
|
882
|
+
if (item !== null && typeof item !== 'object') {
|
|
870
883
|
fields.push({ field: fullKey, value: item });
|
|
871
884
|
}
|
|
872
885
|
}
|
|
873
886
|
}
|
|
887
|
+
else {
|
|
888
|
+
// Primitive value: index it
|
|
889
|
+
fields.push({ field: fullKey, value });
|
|
890
|
+
}
|
|
874
891
|
}
|
|
875
892
|
};
|
|
876
893
|
if (metadata && typeof metadata === 'object') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "3.50.
|
|
3
|
+
"version": "3.50.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",
|