@soulcraft/brainy 4.7.2 → 4.7.4
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 +49 -0
- package/dist/brainy.js +1 -1
- package/dist/neural/SmartExtractor.js +13 -3
- package/dist/neural/signals/PatternSignal.js +38 -18
- package/dist/storage/adapters/azureBlobStorage.js +31 -7
- package/dist/storage/adapters/fileSystemStorage.js +31 -10
- package/dist/storage/adapters/gcsStorage.js +34 -8
- package/dist/storage/adapters/memoryStorage.js +6 -7
- package/dist/storage/adapters/opfsStorage.js +29 -15
- package/dist/storage/adapters/r2Storage.js +25 -5
- package/dist/storage/adapters/s3CompatibleStorage.js +40 -12
- package/dist/storage/adapters/typeAwareStorageAdapter.js +34 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
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
|
+
### [4.7.4](https://github.com/soulcraftlabs/brainy/compare/v4.7.3...v4.7.4) (2025-10-27)
|
|
6
|
+
|
|
7
|
+
**CRITICAL SYSTEMIC VFS BUG FIX - Workshop Team Unblocked!**
|
|
8
|
+
|
|
9
|
+
This hotfix resolves a systemic bug affecting ALL storage adapters that caused VFS queries to return empty results even when data existed.
|
|
10
|
+
|
|
11
|
+
#### 🐛 Critical Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **storage**: Fix systemic metadata skip bug across ALL 7 storage adapters
|
|
14
|
+
- **Impact**: VFS queries returned empty arrays despite 577 "Contains" relationships existing
|
|
15
|
+
- **Root Cause**: All storage adapters skipped entities if metadata file read returned null
|
|
16
|
+
- **Bug Pattern**: `if (!metadata) continue` in getNouns()/getVerbs() methods
|
|
17
|
+
- **Fixed Locations**: 12 bug sites across 7 adapters (TypeAware, Memory, FileSystem, GCS, S3, R2, OPFS, Azure)
|
|
18
|
+
- **Solution**: Allow optional metadata with `metadata: (metadata || {}) as NounMetadata`
|
|
19
|
+
- **Result**: Workshop team UNBLOCKED - VFS entities now queryable
|
|
20
|
+
|
|
21
|
+
* **neural**: Fix SmartExtractor weighted score threshold bug (28 test failures → 4)
|
|
22
|
+
- **Root Cause**: Single signal with 0.8 confidence × 0.2 weight = 0.16 < 0.60 threshold
|
|
23
|
+
- **Solution**: Use original confidence when only one signal matches
|
|
24
|
+
- **Impact**: Entity type extraction now works correctly
|
|
25
|
+
|
|
26
|
+
* **neural**: Fix PatternSignal priority ordering
|
|
27
|
+
- Specific patterns (organization "Inc", location "City, ST") now ranked higher than generic patterns
|
|
28
|
+
- Prevents person full-name pattern from overriding organization/location indicators
|
|
29
|
+
|
|
30
|
+
* **api**: Fix Brainy.relate() weight parameter not returned in getRelations()
|
|
31
|
+
- **Root Cause**: Weight stored in metadata but read from wrong location
|
|
32
|
+
- **Solution**: Extract weight from metadata: `v.metadata?.weight ?? 1.0`
|
|
33
|
+
|
|
34
|
+
#### 📊 Test Results
|
|
35
|
+
|
|
36
|
+
- TypeAwareStorageAdapter: 17/17 tests passing (was 7 failures)
|
|
37
|
+
- SmartExtractor: 42/46 tests passing (was 28 failures)
|
|
38
|
+
- Neural domain clustering: 3/3 tests passing
|
|
39
|
+
- Brainy.relate() weight: 1/1 test passing
|
|
40
|
+
|
|
41
|
+
#### 🏗️ Architecture Notes
|
|
42
|
+
|
|
43
|
+
**Two-Phase Fix**:
|
|
44
|
+
1. Storage Layer (NOW FIXED): Returns ALL entities, even with empty metadata
|
|
45
|
+
2. VFS Layer (ALREADY SAFE): PathResolver uses optional chaining `entity.metadata?.vfsType`
|
|
46
|
+
|
|
47
|
+
**Result**: Valid VFS entities pass through, invalid entities safely filtered out.
|
|
48
|
+
|
|
49
|
+
### [4.7.3](https://github.com/soulcraftlabs/brainy/compare/v4.7.2...v4.7.3) (2025-10-27)
|
|
50
|
+
|
|
51
|
+
- fix(storage): CRITICAL - preserve vectors when updating HNSW connections (v4.7.3) (46e7482)
|
|
52
|
+
|
|
53
|
+
|
|
5
54
|
### [4.4.0](https://github.com/soulcraftlabs/brainy/compare/v4.3.2...v4.4.0) (2025-10-24)
|
|
6
55
|
|
|
7
56
|
- docs: update CHANGELOG for v4.4.0 release (a3c8a28)
|
package/dist/brainy.js
CHANGED
|
@@ -2561,7 +2561,7 @@ export class Brainy {
|
|
|
2561
2561
|
from: v.sourceId,
|
|
2562
2562
|
to: v.targetId,
|
|
2563
2563
|
type: (v.verb || v.type),
|
|
2564
|
-
weight: v.weight,
|
|
2564
|
+
weight: v.metadata?.weight ?? 1.0, // v4.7.4: weight is in metadata
|
|
2565
2565
|
metadata: v.metadata,
|
|
2566
2566
|
service: v.metadata?.service,
|
|
2567
2567
|
createdAt: typeof v.createdAt === 'number' ? v.createdAt : Date.now()
|
|
@@ -402,8 +402,16 @@ export class SmartExtractor {
|
|
|
402
402
|
bestSignals = data.signals;
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
+
// Determine final confidence score
|
|
406
|
+
// FIX: When only one signal matches, use its original confidence instead of weighted score
|
|
407
|
+
// The weighted score is too low when only one signal matches (e.g., 0.8 * 0.2 = 0.16 < 0.60 threshold)
|
|
408
|
+
let finalConfidence = bestScore;
|
|
409
|
+
if (bestSignals.length === 1) {
|
|
410
|
+
// Single signal: use its original confidence
|
|
411
|
+
finalConfidence = bestSignals[0].confidence;
|
|
412
|
+
}
|
|
405
413
|
// Check minimum confidence threshold
|
|
406
|
-
if (!bestType ||
|
|
414
|
+
if (!bestType || finalConfidence < this.options.minConfidence) {
|
|
407
415
|
return null;
|
|
408
416
|
}
|
|
409
417
|
// Track signal contributions
|
|
@@ -415,7 +423,7 @@ export class SmartExtractor {
|
|
|
415
423
|
const evidence = `Ensemble: ${signalNames} (${bestSignals.length} signal${bestSignals.length > 1 ? 's' : ''} agree)`;
|
|
416
424
|
return {
|
|
417
425
|
type: bestType,
|
|
418
|
-
confidence: Math.min(
|
|
426
|
+
confidence: Math.min(finalConfidence, 1.0), // Cap at 1.0
|
|
419
427
|
source: 'ensemble',
|
|
420
428
|
evidence,
|
|
421
429
|
metadata: {
|
|
@@ -444,7 +452,9 @@ export class SmartExtractor {
|
|
|
444
452
|
return null;
|
|
445
453
|
}
|
|
446
454
|
const best = validResults[0];
|
|
447
|
-
|
|
455
|
+
// FIX: Use original confidence, not weighted score for threshold check
|
|
456
|
+
// Weighted score is for ranking signals, not for absolute threshold
|
|
457
|
+
if (best.confidence < this.options.minConfidence) {
|
|
448
458
|
return null;
|
|
449
459
|
}
|
|
450
460
|
return {
|
|
@@ -63,30 +63,50 @@ export class PatternSignal {
|
|
|
63
63
|
* - Document: files, papers, reports
|
|
64
64
|
*/
|
|
65
65
|
initializePatterns() {
|
|
66
|
-
//
|
|
67
|
-
this.addPatterns(NounType.
|
|
68
|
-
/\b(?:
|
|
69
|
-
/\b[A-Z][a-z]+\s+
|
|
70
|
-
/\b(?:CEO|CTO|CFO|COO|VP|Director|Manager|Engineer|Developer|Designer)\b/i,
|
|
71
|
-
/\b(?:author|creator|founder|inventor|contributor|maintainer)\b/i,
|
|
72
|
-
/\b(?:user|member|participant|attendee|speaker|presenter)\b/i
|
|
66
|
+
// Organization patterns - HIGH PRIORITY (must check before person full name pattern)
|
|
67
|
+
this.addPatterns(NounType.Organization, 0.88, [
|
|
68
|
+
/\b(?:Inc|LLC|Corp|Ltd|GmbH|SA|AG)\b/, // Strong org indicators
|
|
69
|
+
/\b[A-Z][a-z]+\s+(?:Company|Corporation|Enterprises|Industries|Group)\b/
|
|
73
70
|
]);
|
|
74
|
-
// Location patterns
|
|
75
|
-
this.addPatterns(NounType.Location, 0.
|
|
76
|
-
/\b
|
|
77
|
-
/\b(?:street|avenue|road|boulevard|lane|drive)\b/i
|
|
78
|
-
/\b(?:building|tower|center|complex|headquarters)\b/i,
|
|
79
|
-
/\b(?:north|south|east|west|central)\s+[A-Z][a-z]+/i,
|
|
80
|
-
/\b[A-Z][a-z]+,\s*[A-Z]{2}\b/ // City, State format
|
|
71
|
+
// Location patterns - HIGH PRIORITY (city/country format, addresses)
|
|
72
|
+
this.addPatterns(NounType.Location, 0.86, [
|
|
73
|
+
/\b[A-Z][a-z]+,\s*[A-Z]{2}\b/, // City, State format (e.g., "Paris, FR")
|
|
74
|
+
/\b(?:street|avenue|road|boulevard|lane|drive)\b/i
|
|
81
75
|
]);
|
|
82
|
-
//
|
|
83
|
-
this.addPatterns(NounType.
|
|
84
|
-
/\b(?:
|
|
85
|
-
/\b
|
|
76
|
+
// Event patterns - HIGH PRIORITY (specific event keywords)
|
|
77
|
+
this.addPatterns(NounType.Event, 0.84, [
|
|
78
|
+
/\b(?:conference|summit|symposium|workshop|seminar|webinar)\b/i,
|
|
79
|
+
/\b(?:hackathon|bootcamp)\b/i
|
|
80
|
+
]);
|
|
81
|
+
// Person patterns - SPECIFIC INDICATORS (high confidence)
|
|
82
|
+
this.addPatterns(NounType.Person, 0.82, [
|
|
83
|
+
/\b(?:Dr|Prof|Mr|Mrs|Ms|Sir|Lady|Lord)\s+[A-Z][a-z]+/, // Titles
|
|
84
|
+
/\b(?:CEO|CTO|CFO|COO|VP|Director|Manager|Engineer|Developer|Designer)\b/i, // Roles
|
|
85
|
+
/\b(?:author|creator|founder|inventor|contributor|maintainer)\b/i
|
|
86
|
+
]);
|
|
87
|
+
// Organization patterns - MEDIUM PRIORITY
|
|
88
|
+
this.addPatterns(NounType.Organization, 0.76, [
|
|
86
89
|
/\b(?:university|college|institute|academy|school)\b/i,
|
|
87
90
|
/\b(?:department|division|team|committee|board)\b/i,
|
|
88
91
|
/\b(?:government|agency|bureau|ministry|administration)\b/i
|
|
89
92
|
]);
|
|
93
|
+
// Location patterns - MEDIUM PRIORITY
|
|
94
|
+
this.addPatterns(NounType.Location, 0.74, [
|
|
95
|
+
/\b(?:city|town|village|country|nation|state|province)\b/i,
|
|
96
|
+
/\b(?:building|tower|center|complex|headquarters)\b/i,
|
|
97
|
+
/\b(?:north|south|east|west|central)\s+[A-Z][a-z]+/i
|
|
98
|
+
]);
|
|
99
|
+
// Event patterns - MEDIUM PRIORITY
|
|
100
|
+
this.addPatterns(NounType.Event, 0.72, [
|
|
101
|
+
/\b(?:meeting|session|call|standup|retrospective|sprint)\b/i,
|
|
102
|
+
/\b(?:release|launch|deployment|rollout|update)\b/i,
|
|
103
|
+
/\b(?:training|course|tutorial)\b/i
|
|
104
|
+
]);
|
|
105
|
+
// Person patterns - GENERIC (low confidence, catches full names but easily overridden)
|
|
106
|
+
this.addPatterns(NounType.Person, 0.68, [
|
|
107
|
+
/\b[A-Z][a-z]+\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?\b/, // Full names (generic, low priority)
|
|
108
|
+
/\b(?:user|member|participant|attendee|speaker|presenter)\b/i
|
|
109
|
+
]);
|
|
90
110
|
// Technology patterns (Thing type)
|
|
91
111
|
this.addPatterns(NounType.Thing, 0.82, [
|
|
92
112
|
/\b(?:JavaScript|TypeScript|Python|Java|C\+\+|Go|Rust|Swift|Kotlin)\b/,
|
|
@@ -942,9 +942,8 @@ export class AzureBlobStorage extends BaseStorage {
|
|
|
942
942
|
const node = await this.getNode(id);
|
|
943
943
|
if (!node)
|
|
944
944
|
continue;
|
|
945
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
945
946
|
const metadata = await this.getNounMetadata(id);
|
|
946
|
-
if (!metadata)
|
|
947
|
-
continue;
|
|
948
947
|
// Apply filters if provided
|
|
949
948
|
if (options.filter) {
|
|
950
949
|
if (options.filter.nounType) {
|
|
@@ -960,7 +959,7 @@ export class AzureBlobStorage extends BaseStorage {
|
|
|
960
959
|
// Combine node with metadata
|
|
961
960
|
items.push({
|
|
962
961
|
...node,
|
|
963
|
-
metadata
|
|
962
|
+
metadata: (metadata || {}) // Empty if none
|
|
964
963
|
});
|
|
965
964
|
count++;
|
|
966
965
|
}
|
|
@@ -1275,13 +1274,38 @@ export class AzureBlobStorage extends BaseStorage {
|
|
|
1275
1274
|
async saveHNSWData(nounId, hnswData) {
|
|
1276
1275
|
await this.ensureInitialized();
|
|
1277
1276
|
try {
|
|
1277
|
+
// CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
|
|
1278
1278
|
const shard = getShardIdFromUuid(nounId);
|
|
1279
1279
|
const key = `entities/nouns/hnsw/${shard}/${nounId}.json`;
|
|
1280
1280
|
const blockBlobClient = this.containerClient.getBlockBlobClient(key);
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1281
|
+
try {
|
|
1282
|
+
// Read existing node data
|
|
1283
|
+
const downloadResponse = await blockBlobClient.download(0);
|
|
1284
|
+
const existingData = await this.streamToBuffer(downloadResponse.readableStreamBody);
|
|
1285
|
+
const existingNode = JSON.parse(existingData.toString());
|
|
1286
|
+
// Preserve id and vector, update only HNSW graph metadata
|
|
1287
|
+
const updatedNode = {
|
|
1288
|
+
...existingNode,
|
|
1289
|
+
level: hnswData.level,
|
|
1290
|
+
connections: hnswData.connections
|
|
1291
|
+
};
|
|
1292
|
+
const content = JSON.stringify(updatedNode, null, 2);
|
|
1293
|
+
await blockBlobClient.upload(content, content.length, {
|
|
1294
|
+
blobHTTPHeaders: { blobContentType: 'application/json' }
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
// If node doesn't exist yet, create it with just HNSW data
|
|
1299
|
+
if (error.statusCode === 404 || error.code === 'BlobNotFound') {
|
|
1300
|
+
const content = JSON.stringify(hnswData, null, 2);
|
|
1301
|
+
await blockBlobClient.upload(content, content.length, {
|
|
1302
|
+
blobHTTPHeaders: { blobContentType: 'application/json' }
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1285
1309
|
}
|
|
1286
1310
|
catch (error) {
|
|
1287
1311
|
this.logger.error(`Failed to save HNSW data for ${nounId}:`, error);
|
|
@@ -745,11 +745,10 @@ export class FileSystemStorage extends BaseStorage {
|
|
|
745
745
|
const data = await fs.promises.readFile(this.getNodePath(id), 'utf-8');
|
|
746
746
|
const parsedNoun = JSON.parse(data);
|
|
747
747
|
// v4.0.0: Load metadata from separate storage
|
|
748
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
748
749
|
const metadata = await this.getNounMetadata(id);
|
|
749
|
-
if (!metadata)
|
|
750
|
-
continue;
|
|
751
750
|
// Apply filter if provided
|
|
752
|
-
if (options.filter) {
|
|
751
|
+
if (options.filter && metadata) {
|
|
753
752
|
let matches = true;
|
|
754
753
|
for (const [key, value] of Object.entries(options.filter)) {
|
|
755
754
|
if (metadata[key] !== value) {
|
|
@@ -775,7 +774,7 @@ export class FileSystemStorage extends BaseStorage {
|
|
|
775
774
|
vector: parsedNoun.vector,
|
|
776
775
|
connections: connections,
|
|
777
776
|
level: parsedNoun.level || 0,
|
|
778
|
-
metadata: metadata
|
|
777
|
+
metadata: (metadata || {}) // Empty if none
|
|
779
778
|
};
|
|
780
779
|
items.push(nounWithMetadata);
|
|
781
780
|
successfullyLoaded++;
|
|
@@ -2156,12 +2155,34 @@ export class FileSystemStorage extends BaseStorage {
|
|
|
2156
2155
|
*/
|
|
2157
2156
|
async saveHNSWData(nounId, hnswData) {
|
|
2158
2157
|
await this.ensureInitialized();
|
|
2159
|
-
//
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2158
|
+
// CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
|
|
2159
|
+
// Previous implementation overwrote the entire file, destroying vector data
|
|
2160
|
+
// Now we READ the existing node, UPDATE only connections/level, then WRITE back the complete node
|
|
2161
|
+
const filePath = this.getNodePath(nounId);
|
|
2162
|
+
try {
|
|
2163
|
+
// Read existing node data
|
|
2164
|
+
const existingData = await fs.promises.readFile(filePath, 'utf-8');
|
|
2165
|
+
const existingNode = JSON.parse(existingData);
|
|
2166
|
+
// Preserve id and vector, update only HNSW graph metadata
|
|
2167
|
+
const updatedNode = {
|
|
2168
|
+
...existingNode, // Preserve all existing fields (id, vector, etc.)
|
|
2169
|
+
level: hnswData.level,
|
|
2170
|
+
connections: hnswData.connections
|
|
2171
|
+
};
|
|
2172
|
+
// Write back the COMPLETE node with updated HNSW data
|
|
2173
|
+
await fs.promises.writeFile(filePath, JSON.stringify(updatedNode, null, 2));
|
|
2174
|
+
}
|
|
2175
|
+
catch (error) {
|
|
2176
|
+
// If node doesn't exist yet, create it with just HNSW data
|
|
2177
|
+
// This should only happen during initial node creation
|
|
2178
|
+
if (error.code === 'ENOENT') {
|
|
2179
|
+
await this.ensureDirectoryExists(path.dirname(filePath));
|
|
2180
|
+
await fs.promises.writeFile(filePath, JSON.stringify(hnswData, null, 2));
|
|
2181
|
+
}
|
|
2182
|
+
else {
|
|
2183
|
+
throw error;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2165
2186
|
}
|
|
2166
2187
|
/**
|
|
2167
2188
|
* Get HNSW graph data for a noun
|
|
@@ -804,9 +804,8 @@ export class GcsStorage extends BaseStorage {
|
|
|
804
804
|
// v4.0.0: Combine nodes with metadata to create HNSWNounWithMetadata[]
|
|
805
805
|
const items = [];
|
|
806
806
|
for (const node of result.nodes) {
|
|
807
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
807
808
|
const metadata = await this.getNounMetadata(node.id);
|
|
808
|
-
if (!metadata)
|
|
809
|
-
continue;
|
|
810
809
|
// Apply filters if provided
|
|
811
810
|
if (options.filter) {
|
|
812
811
|
// Filter by noun type
|
|
@@ -839,7 +838,7 @@ export class GcsStorage extends BaseStorage {
|
|
|
839
838
|
vector: [...node.vector],
|
|
840
839
|
connections: new Map(node.connections),
|
|
841
840
|
level: node.level || 0,
|
|
842
|
-
metadata: metadata
|
|
841
|
+
metadata: (metadata || {}) // Empty if none
|
|
843
842
|
};
|
|
844
843
|
items.push(nounWithMetadata);
|
|
845
844
|
}
|
|
@@ -1469,14 +1468,41 @@ export class GcsStorage extends BaseStorage {
|
|
|
1469
1468
|
async saveHNSWData(nounId, hnswData) {
|
|
1470
1469
|
await this.ensureInitialized();
|
|
1471
1470
|
try {
|
|
1472
|
-
//
|
|
1471
|
+
// CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
|
|
1472
|
+
// Previous implementation overwrote the entire file, destroying vector data
|
|
1473
|
+
// Now we READ the existing node, UPDATE only connections/level, then WRITE back the complete node
|
|
1473
1474
|
const shard = getShardIdFromUuid(nounId);
|
|
1474
1475
|
const key = `entities/nouns/hnsw/${shard}/${nounId}.json`;
|
|
1475
1476
|
const file = this.bucket.file(key);
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
// Read existing node data
|
|
1479
|
+
const [existingData] = await file.download();
|
|
1480
|
+
const existingNode = JSON.parse(existingData.toString());
|
|
1481
|
+
// Preserve id and vector, update only HNSW graph metadata
|
|
1482
|
+
const updatedNode = {
|
|
1483
|
+
...existingNode, // Preserve all existing fields (id, vector, etc.)
|
|
1484
|
+
level: hnswData.level,
|
|
1485
|
+
connections: hnswData.connections
|
|
1486
|
+
};
|
|
1487
|
+
// Write back the COMPLETE node with updated HNSW data
|
|
1488
|
+
await file.save(JSON.stringify(updatedNode, null, 2), {
|
|
1489
|
+
contentType: 'application/json',
|
|
1490
|
+
resumable: false
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
catch (error) {
|
|
1494
|
+
// If node doesn't exist yet, create it with just HNSW data
|
|
1495
|
+
// This should only happen during initial node creation
|
|
1496
|
+
if (error.code === 404) {
|
|
1497
|
+
await file.save(JSON.stringify(hnswData, null, 2), {
|
|
1498
|
+
contentType: 'application/json',
|
|
1499
|
+
resumable: false
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
throw error;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1480
1506
|
}
|
|
1481
1507
|
catch (error) {
|
|
1482
1508
|
this.logger.error(`Failed to save HNSW data for ${nounId}:`, error);
|
|
@@ -152,16 +152,15 @@ export class MemoryStorage extends BaseStorage {
|
|
|
152
152
|
if (!noun)
|
|
153
153
|
continue;
|
|
154
154
|
// Get metadata from separate storage
|
|
155
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
155
156
|
const metadata = await this.getNounMetadata(id);
|
|
156
|
-
if (!metadata)
|
|
157
|
-
continue; // Skip if no metadata
|
|
158
157
|
// v4.0.0: Create HNSWNounWithMetadata with metadata field
|
|
159
158
|
const nounWithMetadata = {
|
|
160
159
|
id: noun.id,
|
|
161
160
|
vector: [...noun.vector],
|
|
162
161
|
connections: new Map(),
|
|
163
162
|
level: noun.level || 0,
|
|
164
|
-
metadata: metadata // Include metadata field
|
|
163
|
+
metadata: (metadata || {}) // Include metadata field (empty if none)
|
|
165
164
|
};
|
|
166
165
|
// Copy connections
|
|
167
166
|
for (const [level, connections] of noun.connections.entries()) {
|
|
@@ -357,9 +356,9 @@ export class MemoryStorage extends BaseStorage {
|
|
|
357
356
|
if (!hnswVerb)
|
|
358
357
|
continue;
|
|
359
358
|
// Get metadata from separate storage
|
|
359
|
+
// FIX v4.7.4: Don't skip verbs without metadata - metadata is optional in v4.0.0
|
|
360
|
+
// Core fields (verb, sourceId, targetId) are in HNSWVerb itself
|
|
360
361
|
const metadata = await this.getVerbMetadata(id);
|
|
361
|
-
if (!metadata)
|
|
362
|
-
continue; // Skip if no metadata
|
|
363
362
|
// v4.0.0: Create HNSWVerbWithMetadata with metadata field
|
|
364
363
|
const verbWithMetadata = {
|
|
365
364
|
id: hnswVerb.id,
|
|
@@ -369,8 +368,8 @@ export class MemoryStorage extends BaseStorage {
|
|
|
369
368
|
verb: hnswVerb.verb,
|
|
370
369
|
sourceId: hnswVerb.sourceId,
|
|
371
370
|
targetId: hnswVerb.targetId,
|
|
372
|
-
// Metadata field
|
|
373
|
-
metadata: metadata
|
|
371
|
+
// Metadata field (empty if none)
|
|
372
|
+
metadata: metadata || {}
|
|
374
373
|
};
|
|
375
374
|
// Copy connections
|
|
376
375
|
for (const [level, connections] of hnswVerb.connections.entries()) {
|
|
@@ -1411,11 +1411,10 @@ export class OPFSStorage extends BaseStorage {
|
|
|
1411
1411
|
const noun = await this.getNoun_internal(id);
|
|
1412
1412
|
if (noun) {
|
|
1413
1413
|
// Load metadata for filtering and combining
|
|
1414
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
1414
1415
|
const metadata = await this.getNounMetadata(id);
|
|
1415
|
-
if (!metadata)
|
|
1416
|
-
continue;
|
|
1417
1416
|
// Apply filters if provided
|
|
1418
|
-
if (options.filter) {
|
|
1417
|
+
if (options.filter && metadata) {
|
|
1419
1418
|
// Filter by noun type
|
|
1420
1419
|
if (options.filter.nounType) {
|
|
1421
1420
|
const nounTypes = Array.isArray(options.filter.nounType)
|
|
@@ -1453,7 +1452,7 @@ export class OPFSStorage extends BaseStorage {
|
|
|
1453
1452
|
vector: [...noun.vector],
|
|
1454
1453
|
connections: new Map(noun.connections),
|
|
1455
1454
|
level: noun.level || 0,
|
|
1456
|
-
metadata: metadata
|
|
1455
|
+
metadata: (metadata || {}) // Empty if none
|
|
1457
1456
|
};
|
|
1458
1457
|
items.push(nounWithMetadata);
|
|
1459
1458
|
}
|
|
@@ -1516,11 +1515,11 @@ export class OPFSStorage extends BaseStorage {
|
|
|
1516
1515
|
const hnswVerb = await this.getVerb_internal(id);
|
|
1517
1516
|
if (hnswVerb) {
|
|
1518
1517
|
// Load metadata for filtering and combining
|
|
1518
|
+
// FIX v4.7.4: Don't skip verbs without metadata - metadata is optional in v4.0.0
|
|
1519
|
+
// Core fields (verb, sourceId, targetId) are in HNSWVerb itself
|
|
1519
1520
|
const metadata = await this.getVerbMetadata(id);
|
|
1520
|
-
if (!metadata)
|
|
1521
|
-
continue;
|
|
1522
1521
|
// Apply filters if provided
|
|
1523
|
-
if (options.filter) {
|
|
1522
|
+
if (options.filter && metadata) {
|
|
1524
1523
|
// Filter by verb type
|
|
1525
1524
|
// v4.0.0: verb field is in HNSWVerb structure (NOT in metadata)
|
|
1526
1525
|
if (options.filter.verbType) {
|
|
@@ -1581,7 +1580,7 @@ export class OPFSStorage extends BaseStorage {
|
|
|
1581
1580
|
verb: hnswVerb.verb,
|
|
1582
1581
|
sourceId: hnswVerb.sourceId,
|
|
1583
1582
|
targetId: hnswVerb.targetId,
|
|
1584
|
-
metadata: metadata
|
|
1583
|
+
metadata: (metadata || {}) // Empty if none
|
|
1585
1584
|
};
|
|
1586
1585
|
items.push(verbWithMetadata);
|
|
1587
1586
|
}
|
|
@@ -1693,17 +1692,32 @@ export class OPFSStorage extends BaseStorage {
|
|
|
1693
1692
|
async saveHNSWData(nounId, hnswData) {
|
|
1694
1693
|
await this.ensureInitialized();
|
|
1695
1694
|
try {
|
|
1696
|
-
//
|
|
1695
|
+
// CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
|
|
1697
1696
|
const hnswDir = await this.nounsDir.getDirectoryHandle('hnsw', { create: true });
|
|
1698
|
-
// Use sharded path for HNSW data
|
|
1699
1697
|
const shard = getShardIdFromUuid(nounId);
|
|
1700
1698
|
const shardDir = await hnswDir.getDirectoryHandle(shard, { create: true });
|
|
1701
|
-
// Create or get the file in the shard directory
|
|
1702
1699
|
const fileHandle = await shardDir.getFileHandle(`${nounId}.json`, { create: true });
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1700
|
+
try {
|
|
1701
|
+
// Read existing node data
|
|
1702
|
+
const file = await fileHandle.getFile();
|
|
1703
|
+
const existingData = await file.text();
|
|
1704
|
+
const existingNode = JSON.parse(existingData);
|
|
1705
|
+
// Preserve id and vector, update only HNSW graph metadata
|
|
1706
|
+
const updatedNode = {
|
|
1707
|
+
...existingNode,
|
|
1708
|
+
level: hnswData.level,
|
|
1709
|
+
connections: hnswData.connections
|
|
1710
|
+
};
|
|
1711
|
+
const writable = await fileHandle.createWritable();
|
|
1712
|
+
await writable.write(JSON.stringify(updatedNode, null, 2));
|
|
1713
|
+
await writable.close();
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
// If node doesn't exist or read fails, create with just HNSW data
|
|
1717
|
+
const writable = await fileHandle.createWritable();
|
|
1718
|
+
await writable.write(JSON.stringify(hnswData, null, 2));
|
|
1719
|
+
await writable.close();
|
|
1720
|
+
}
|
|
1707
1721
|
}
|
|
1708
1722
|
catch (error) {
|
|
1709
1723
|
console.error(`Failed to save HNSW data for ${nounId}:`, error);
|
|
@@ -753,9 +753,30 @@ export class R2Storage extends BaseStorage {
|
|
|
753
753
|
}
|
|
754
754
|
async saveHNSWData(nounId, hnswData) {
|
|
755
755
|
await this.ensureInitialized();
|
|
756
|
+
// CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
|
|
756
757
|
const shard = getShardIdFromUuid(nounId);
|
|
757
758
|
const key = `entities/nouns/hnsw/${shard}/${nounId}.json`;
|
|
758
|
-
|
|
759
|
+
try {
|
|
760
|
+
// Read existing node data
|
|
761
|
+
const existingNode = await this.readObjectFromPath(key);
|
|
762
|
+
if (existingNode) {
|
|
763
|
+
// Preserve id and vector, update only HNSW graph metadata
|
|
764
|
+
const updatedNode = {
|
|
765
|
+
...existingNode,
|
|
766
|
+
level: hnswData.level,
|
|
767
|
+
connections: hnswData.connections
|
|
768
|
+
};
|
|
769
|
+
await this.writeObjectToPath(key, updatedNode);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
// Node doesn't exist yet, create with just HNSW data
|
|
773
|
+
await this.writeObjectToPath(key, hnswData);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
// If read fails, create with just HNSW data
|
|
778
|
+
await this.writeObjectToPath(key, hnswData);
|
|
779
|
+
}
|
|
759
780
|
}
|
|
760
781
|
async getHNSWData(nounId) {
|
|
761
782
|
await this.ensureInitialized();
|
|
@@ -862,11 +883,10 @@ export class R2Storage extends BaseStorage {
|
|
|
862
883
|
const noun = await this.getNoun_internal(id);
|
|
863
884
|
if (noun) {
|
|
864
885
|
// v4.0.0: Load metadata and combine with noun to create HNSWNounWithMetadata
|
|
886
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
865
887
|
const metadata = await this.getNounMetadata(id);
|
|
866
|
-
if (!metadata)
|
|
867
|
-
continue;
|
|
868
888
|
// Apply filters if provided
|
|
869
|
-
if (options.filter) {
|
|
889
|
+
if (options.filter && metadata) {
|
|
870
890
|
// Filter by noun type
|
|
871
891
|
if (options.filter.nounType) {
|
|
872
892
|
const nounTypes = Array.isArray(options.filter.nounType)
|
|
@@ -904,7 +924,7 @@ export class R2Storage extends BaseStorage {
|
|
|
904
924
|
vector: [...noun.vector],
|
|
905
925
|
connections: new Map(noun.connections),
|
|
906
926
|
level: noun.level || 0,
|
|
907
|
-
metadata: metadata
|
|
927
|
+
metadata: (metadata || {}) // Empty if none
|
|
908
928
|
};
|
|
909
929
|
items.push(nounWithMetadata);
|
|
910
930
|
}
|
|
@@ -2867,11 +2867,10 @@ export class S3CompatibleStorage extends BaseStorage {
|
|
|
2867
2867
|
// v4.0.0: Combine nodes with metadata to create HNSWNounWithMetadata[]
|
|
2868
2868
|
const nounsWithMetadata = [];
|
|
2869
2869
|
for (const node of result.nodes) {
|
|
2870
|
+
// FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
|
|
2870
2871
|
const metadata = await this.getNounMetadata(node.id);
|
|
2871
|
-
if (!metadata)
|
|
2872
|
-
continue;
|
|
2873
2872
|
// Apply filters if provided
|
|
2874
|
-
if (options.filter) {
|
|
2873
|
+
if (options.filter && metadata) {
|
|
2875
2874
|
// Filter by noun type
|
|
2876
2875
|
if (options.filter.nounType) {
|
|
2877
2876
|
const nounTypes = Array.isArray(options.filter.nounType)
|
|
@@ -2906,7 +2905,7 @@ export class S3CompatibleStorage extends BaseStorage {
|
|
|
2906
2905
|
vector: [...node.vector],
|
|
2907
2906
|
connections: new Map(node.connections),
|
|
2908
2907
|
level: node.level || 0,
|
|
2909
|
-
metadata: metadata
|
|
2908
|
+
metadata: (metadata || {}) // Empty if none
|
|
2910
2909
|
};
|
|
2911
2910
|
nounsWithMetadata.push(nounWithMetadata);
|
|
2912
2911
|
}
|
|
@@ -3055,16 +3054,45 @@ export class S3CompatibleStorage extends BaseStorage {
|
|
|
3055
3054
|
async saveHNSWData(nounId, hnswData) {
|
|
3056
3055
|
await this.ensureInitialized();
|
|
3057
3056
|
try {
|
|
3058
|
-
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
3059
|
-
//
|
|
3057
|
+
const { PutObjectCommand, GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
3058
|
+
// CRITICAL FIX (v4.7.3): Must preserve existing node data (id, vector) when updating HNSW metadata
|
|
3060
3059
|
const shard = getShardIdFromUuid(nounId);
|
|
3061
3060
|
const key = `entities/nouns/hnsw/${shard}/${nounId}.json`;
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3061
|
+
try {
|
|
3062
|
+
// Read existing node data
|
|
3063
|
+
const getResponse = await this.s3Client.send(new GetObjectCommand({
|
|
3064
|
+
Bucket: this.bucketName,
|
|
3065
|
+
Key: key
|
|
3066
|
+
}));
|
|
3067
|
+
const existingData = await getResponse.Body.transformToString();
|
|
3068
|
+
const existingNode = JSON.parse(existingData);
|
|
3069
|
+
// Preserve id and vector, update only HNSW graph metadata
|
|
3070
|
+
const updatedNode = {
|
|
3071
|
+
...existingNode,
|
|
3072
|
+
level: hnswData.level,
|
|
3073
|
+
connections: hnswData.connections
|
|
3074
|
+
};
|
|
3075
|
+
await this.s3Client.send(new PutObjectCommand({
|
|
3076
|
+
Bucket: this.bucketName,
|
|
3077
|
+
Key: key,
|
|
3078
|
+
Body: JSON.stringify(updatedNode, null, 2),
|
|
3079
|
+
ContentType: 'application/json'
|
|
3080
|
+
}));
|
|
3081
|
+
}
|
|
3082
|
+
catch (error) {
|
|
3083
|
+
// If node doesn't exist yet, create it with just HNSW data
|
|
3084
|
+
if (error.name === 'NoSuchKey' || error.Code === 'NoSuchKey') {
|
|
3085
|
+
await this.s3Client.send(new PutObjectCommand({
|
|
3086
|
+
Bucket: this.bucketName,
|
|
3087
|
+
Key: key,
|
|
3088
|
+
Body: JSON.stringify(hnswData, null, 2),
|
|
3089
|
+
ContentType: 'application/json'
|
|
3090
|
+
}));
|
|
3091
|
+
}
|
|
3092
|
+
else {
|
|
3093
|
+
throw error;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3068
3096
|
}
|
|
3069
3097
|
catch (error) {
|
|
3070
3098
|
this.logger.error(`Failed to save HNSW data for ${nounId}:`, error);
|
|
@@ -354,19 +354,26 @@ export class TypeAwareStorageAdapter extends BaseStorage {
|
|
|
354
354
|
// Check sourceId from HNSWVerb (v4.0.0: core fields are in HNSWVerb)
|
|
355
355
|
if (hnswVerb.sourceId !== sourceId)
|
|
356
356
|
continue;
|
|
357
|
-
// Load metadata separately
|
|
357
|
+
// Load metadata separately (optional in v4.0.0!)
|
|
358
|
+
// FIX: Don't skip verbs without metadata - metadata is optional!
|
|
359
|
+
// VFS relationships often have NO metadata (just verb/source/target)
|
|
358
360
|
const metadata = await this.getVerbMetadata(id);
|
|
359
|
-
if (!metadata)
|
|
360
|
-
continue;
|
|
361
361
|
// Create HNSWVerbWithMetadata (verbs don't have level field)
|
|
362
|
+
// Convert connections from plain object to Map<number, Set<string>>
|
|
363
|
+
const connectionsMap = new Map();
|
|
364
|
+
if (hnswVerb.connections && typeof hnswVerb.connections === 'object') {
|
|
365
|
+
for (const [level, ids] of Object.entries(hnswVerb.connections)) {
|
|
366
|
+
connectionsMap.set(Number(level), new Set(ids));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
362
369
|
const verbWithMetadata = {
|
|
363
370
|
id: hnswVerb.id,
|
|
364
371
|
vector: [...hnswVerb.vector],
|
|
365
|
-
connections:
|
|
372
|
+
connections: connectionsMap,
|
|
366
373
|
verb: hnswVerb.verb,
|
|
367
374
|
sourceId: hnswVerb.sourceId,
|
|
368
375
|
targetId: hnswVerb.targetId,
|
|
369
|
-
metadata: metadata
|
|
376
|
+
metadata: metadata || {} // Empty metadata if none exists
|
|
370
377
|
};
|
|
371
378
|
verbs.push(verbWithMetadata);
|
|
372
379
|
}
|
|
@@ -399,19 +406,25 @@ export class TypeAwareStorageAdapter extends BaseStorage {
|
|
|
399
406
|
// Check targetId from HNSWVerb (v4.0.0: core fields are in HNSWVerb)
|
|
400
407
|
if (hnswVerb.targetId !== targetId)
|
|
401
408
|
continue;
|
|
402
|
-
// Load metadata separately
|
|
409
|
+
// Load metadata separately (optional in v4.0.0!)
|
|
410
|
+
// FIX: Don't skip verbs without metadata - metadata is optional!
|
|
403
411
|
const metadata = await this.getVerbMetadata(id);
|
|
404
|
-
if (!metadata)
|
|
405
|
-
continue;
|
|
406
412
|
// Create HNSWVerbWithMetadata (verbs don't have level field)
|
|
413
|
+
// Convert connections from plain object to Map<number, Set<string>>
|
|
414
|
+
const connectionsMap = new Map();
|
|
415
|
+
if (hnswVerb.connections && typeof hnswVerb.connections === 'object') {
|
|
416
|
+
for (const [level, ids] of Object.entries(hnswVerb.connections)) {
|
|
417
|
+
connectionsMap.set(Number(level), new Set(ids));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
407
420
|
const verbWithMetadata = {
|
|
408
421
|
id: hnswVerb.id,
|
|
409
422
|
vector: [...hnswVerb.vector],
|
|
410
|
-
connections:
|
|
423
|
+
connections: connectionsMap,
|
|
411
424
|
verb: hnswVerb.verb,
|
|
412
425
|
sourceId: hnswVerb.sourceId,
|
|
413
426
|
targetId: hnswVerb.targetId,
|
|
414
|
-
metadata: metadata
|
|
427
|
+
metadata: metadata || {} // Empty metadata if none exists
|
|
415
428
|
};
|
|
416
429
|
verbs.push(verbWithMetadata);
|
|
417
430
|
}
|
|
@@ -439,19 +452,25 @@ export class TypeAwareStorageAdapter extends BaseStorage {
|
|
|
439
452
|
continue;
|
|
440
453
|
// Cache type from HNSWVerb for future O(1) retrievals
|
|
441
454
|
this.verbTypeCache.set(hnswVerb.id, hnswVerb.verb);
|
|
442
|
-
// Load metadata separately
|
|
455
|
+
// Load metadata separately (optional in v4.0.0!)
|
|
456
|
+
// FIX: Don't skip verbs without metadata - metadata is optional!
|
|
443
457
|
const metadata = await this.getVerbMetadata(hnswVerb.id);
|
|
444
|
-
if (!metadata)
|
|
445
|
-
continue;
|
|
446
458
|
// Create HNSWVerbWithMetadata (verbs don't have level field)
|
|
459
|
+
// Convert connections from plain object to Map<number, Set<string>>
|
|
460
|
+
const connectionsMap = new Map();
|
|
461
|
+
if (hnswVerb.connections && typeof hnswVerb.connections === 'object') {
|
|
462
|
+
for (const [level, ids] of Object.entries(hnswVerb.connections)) {
|
|
463
|
+
connectionsMap.set(Number(level), new Set(ids));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
447
466
|
const verbWithMetadata = {
|
|
448
467
|
id: hnswVerb.id,
|
|
449
468
|
vector: [...hnswVerb.vector],
|
|
450
|
-
connections:
|
|
469
|
+
connections: connectionsMap,
|
|
451
470
|
verb: hnswVerb.verb,
|
|
452
471
|
sourceId: hnswVerb.sourceId,
|
|
453
472
|
targetId: hnswVerb.targetId,
|
|
454
|
-
metadata: metadata
|
|
473
|
+
metadata: metadata || {} // Empty metadata if none exists
|
|
455
474
|
};
|
|
456
475
|
verbs.push(verbWithMetadata);
|
|
457
476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.4",
|
|
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",
|