@soulcraft/brainy 5.7.8 → 5.7.10
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 +5 -0
- package/dist/storage/baseStorage.d.ts +24 -0
- package/dist/storage/baseStorage.js +81 -33
- package/dist/utils/metadataIndex.js +72 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
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
|
+
### [5.7.9](https://github.com/soulcraftlabs/brainy/compare/v5.7.8...v5.7.9) (2025-11-13)
|
|
6
|
+
|
|
7
|
+
- fix: implement exists: false and missing operators in MetadataIndexManager (b0f72ef)
|
|
8
|
+
|
|
9
|
+
|
|
5
10
|
### [5.7.8](https://github.com/soulcraftlabs/brainy/compare/v5.7.7...v5.7.8) (2025-11-13)
|
|
6
11
|
|
|
7
12
|
- fix: reconstruct Map from JSON for HNSW connections (v5.7.8 hotfix) (f6f2717)
|
|
@@ -458,6 +458,30 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
|
|
|
458
458
|
* Verb type is a required field in HNSWVerb
|
|
459
459
|
*/
|
|
460
460
|
protected getVerbType(verb: HNSWVerb | GraphVerb): VerbType;
|
|
461
|
+
/**
|
|
462
|
+
* Deserialize HNSW connections from JSON storage format
|
|
463
|
+
*
|
|
464
|
+
* Converts plain object { "0": ["id1"], "1": ["id2"] }
|
|
465
|
+
* into Map<number, Set<string>>
|
|
466
|
+
*
|
|
467
|
+
* v5.7.10: Central helper to fix serialization bug across all code paths
|
|
468
|
+
* Root cause: JSON.stringify(Map) = {} (empty object), must reconstruct on read
|
|
469
|
+
*/
|
|
470
|
+
protected deserializeConnections(connections: any): Map<number, Set<string>>;
|
|
471
|
+
/**
|
|
472
|
+
* Deserialize HNSWNoun from JSON storage format
|
|
473
|
+
*
|
|
474
|
+
* v5.7.10: Ensures connections are properly reconstructed from Map → object → Map
|
|
475
|
+
* Fixes: "TypeError: noun.connections.entries is not a function"
|
|
476
|
+
*/
|
|
477
|
+
protected deserializeNoun(data: any): HNSWNoun;
|
|
478
|
+
/**
|
|
479
|
+
* Deserialize HNSWVerb from JSON storage format
|
|
480
|
+
*
|
|
481
|
+
* v5.7.10: Ensures connections are properly reconstructed from Map → object → Map
|
|
482
|
+
* Fixes same serialization bug for verbs
|
|
483
|
+
*/
|
|
484
|
+
protected deserializeVerb(data: any): HNSWVerb;
|
|
461
485
|
/**
|
|
462
486
|
* Save a noun to storage (type-first path)
|
|
463
487
|
*/
|
|
@@ -912,8 +912,11 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
912
912
|
if (!nounPath.endsWith('.json'))
|
|
913
913
|
continue;
|
|
914
914
|
try {
|
|
915
|
-
const
|
|
916
|
-
if (
|
|
915
|
+
const rawNoun = await this.readWithInheritance(nounPath);
|
|
916
|
+
if (rawNoun) {
|
|
917
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
918
|
+
// Replaces v5.7.8 manual deserialization (removed 13 lines at 1156-1168)
|
|
919
|
+
const noun = this.deserializeNoun(rawNoun);
|
|
917
920
|
// Load metadata
|
|
918
921
|
const metadataPath = getNounMetadataPath(type, noun.id);
|
|
919
922
|
const metadata = await this.readWithInheritance(metadataPath);
|
|
@@ -925,24 +928,10 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
925
928
|
continue;
|
|
926
929
|
}
|
|
927
930
|
}
|
|
928
|
-
// v5.7.8: Convert connections from plain object to Map (JSON deserialization fix)
|
|
929
|
-
// When loaded from JSON, Map becomes plain object - must reconstruct
|
|
930
|
-
const connections = new Map();
|
|
931
|
-
if (noun.connections && typeof noun.connections === 'object') {
|
|
932
|
-
for (const [levelStr, ids] of Object.entries(noun.connections)) {
|
|
933
|
-
if (Array.isArray(ids)) {
|
|
934
|
-
connections.set(parseInt(levelStr, 10), new Set(ids));
|
|
935
|
-
}
|
|
936
|
-
else if (ids && typeof ids === 'object') {
|
|
937
|
-
// Handle if it's already an array-like or Set-like object
|
|
938
|
-
connections.set(parseInt(levelStr, 10), new Set(Object.values(ids)));
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
931
|
// Combine noun + metadata (v5.4.0: Extract standard fields to top-level)
|
|
943
932
|
collectedNouns.push({
|
|
944
933
|
...noun,
|
|
945
|
-
|
|
934
|
+
// v5.7.10: connections already deserialized by deserializeNoun()
|
|
946
935
|
type: metadata.noun || type, // Required: Extract type from metadata
|
|
947
936
|
confidence: metadata.confidence,
|
|
948
937
|
weight: metadata.weight,
|
|
@@ -1731,6 +1720,64 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1731
1720
|
return 'relatedTo';
|
|
1732
1721
|
}
|
|
1733
1722
|
// ============================================================================
|
|
1723
|
+
// DESERIALIZATION HELPERS (v5.7.10)
|
|
1724
|
+
// Centralized Map/Set reconstruction from JSON storage format
|
|
1725
|
+
// ============================================================================
|
|
1726
|
+
/**
|
|
1727
|
+
* Deserialize HNSW connections from JSON storage format
|
|
1728
|
+
*
|
|
1729
|
+
* Converts plain object { "0": ["id1"], "1": ["id2"] }
|
|
1730
|
+
* into Map<number, Set<string>>
|
|
1731
|
+
*
|
|
1732
|
+
* v5.7.10: Central helper to fix serialization bug across all code paths
|
|
1733
|
+
* Root cause: JSON.stringify(Map) = {} (empty object), must reconstruct on read
|
|
1734
|
+
*/
|
|
1735
|
+
deserializeConnections(connections) {
|
|
1736
|
+
const result = new Map();
|
|
1737
|
+
if (!connections || typeof connections !== 'object') {
|
|
1738
|
+
return result;
|
|
1739
|
+
}
|
|
1740
|
+
// Already a Map (in-memory, not from JSON)
|
|
1741
|
+
if (connections instanceof Map) {
|
|
1742
|
+
return connections;
|
|
1743
|
+
}
|
|
1744
|
+
// Deserialize from plain object
|
|
1745
|
+
for (const [levelStr, ids] of Object.entries(connections)) {
|
|
1746
|
+
if (Array.isArray(ids)) {
|
|
1747
|
+
result.set(parseInt(levelStr, 10), new Set(ids));
|
|
1748
|
+
}
|
|
1749
|
+
else if (ids && typeof ids === 'object') {
|
|
1750
|
+
// Handle Set-like or array-like objects
|
|
1751
|
+
result.set(parseInt(levelStr, 10), new Set(Object.values(ids)));
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return result;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Deserialize HNSWNoun from JSON storage format
|
|
1758
|
+
*
|
|
1759
|
+
* v5.7.10: Ensures connections are properly reconstructed from Map → object → Map
|
|
1760
|
+
* Fixes: "TypeError: noun.connections.entries is not a function"
|
|
1761
|
+
*/
|
|
1762
|
+
deserializeNoun(data) {
|
|
1763
|
+
return {
|
|
1764
|
+
...data,
|
|
1765
|
+
connections: this.deserializeConnections(data.connections)
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Deserialize HNSWVerb from JSON storage format
|
|
1770
|
+
*
|
|
1771
|
+
* v5.7.10: Ensures connections are properly reconstructed from Map → object → Map
|
|
1772
|
+
* Fixes same serialization bug for verbs
|
|
1773
|
+
*/
|
|
1774
|
+
deserializeVerb(data) {
|
|
1775
|
+
return {
|
|
1776
|
+
...data,
|
|
1777
|
+
connections: this.deserializeConnections(data.connections)
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
// ============================================================================
|
|
1734
1781
|
// ABSTRACT METHOD IMPLEMENTATIONS (v5.4.0)
|
|
1735
1782
|
// Converted from abstract to concrete - all adapters now have built-in type-aware
|
|
1736
1783
|
// ============================================================================
|
|
@@ -1760,7 +1807,9 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1760
1807
|
if (cachedType) {
|
|
1761
1808
|
const path = getNounVectorPath(cachedType, id);
|
|
1762
1809
|
// COW-aware read (v5.0.1): Use COW helper for branch isolation
|
|
1763
|
-
|
|
1810
|
+
const data = await this.readWithInheritance(path);
|
|
1811
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
1812
|
+
return data ? this.deserializeNoun(data) : null;
|
|
1764
1813
|
}
|
|
1765
1814
|
// Need to search across all types (expensive, but cached after first access)
|
|
1766
1815
|
for (let i = 0; i < NOUN_TYPE_COUNT; i++) {
|
|
@@ -1772,7 +1821,8 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1772
1821
|
if (noun) {
|
|
1773
1822
|
// Cache the type for next time
|
|
1774
1823
|
this.nounTypeCache.set(id, type);
|
|
1775
|
-
|
|
1824
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
1825
|
+
return this.deserializeNoun(noun);
|
|
1776
1826
|
}
|
|
1777
1827
|
}
|
|
1778
1828
|
catch (error) {
|
|
@@ -1796,7 +1846,8 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1796
1846
|
// COW-aware read (v5.0.1): Use COW helper for branch isolation
|
|
1797
1847
|
const noun = await this.readWithInheritance(path);
|
|
1798
1848
|
if (noun) {
|
|
1799
|
-
|
|
1849
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
1850
|
+
nouns.push(this.deserializeNoun(noun));
|
|
1800
1851
|
// Cache the type
|
|
1801
1852
|
this.nounTypeCache.set(noun.id, type);
|
|
1802
1853
|
}
|
|
@@ -1891,7 +1942,8 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1891
1942
|
const path = getVerbVectorPath(cachedType, id);
|
|
1892
1943
|
// COW-aware read (v5.0.1): Use COW helper for branch isolation
|
|
1893
1944
|
const verb = await this.readWithInheritance(path);
|
|
1894
|
-
|
|
1945
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
1946
|
+
return verb ? this.deserializeVerb(verb) : null;
|
|
1895
1947
|
}
|
|
1896
1948
|
// Search across all types (only on first access)
|
|
1897
1949
|
for (let i = 0; i < VERB_TYPE_COUNT; i++) {
|
|
@@ -1903,7 +1955,8 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1903
1955
|
if (verb) {
|
|
1904
1956
|
// Cache the type for next time (read from verb.verb field)
|
|
1905
1957
|
this.verbTypeCache.set(id, verb.verb);
|
|
1906
|
-
|
|
1958
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
1959
|
+
return this.deserializeVerb(verb);
|
|
1907
1960
|
}
|
|
1908
1961
|
}
|
|
1909
1962
|
catch (error) {
|
|
@@ -2040,29 +2093,24 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
2040
2093
|
for (const path of paths) {
|
|
2041
2094
|
try {
|
|
2042
2095
|
// COW-aware read (v5.0.1): Use COW helper for branch isolation
|
|
2043
|
-
const
|
|
2044
|
-
if (!
|
|
2096
|
+
const rawVerb = await this.readWithInheritance(path);
|
|
2097
|
+
if (!rawVerb)
|
|
2045
2098
|
continue;
|
|
2099
|
+
// v5.7.10: Deserialize connections Map from JSON storage format
|
|
2100
|
+
// Replaces v5.7.8 manual deserialization (lines 2599-2605)
|
|
2101
|
+
const hnswVerb = this.deserializeVerb(rawVerb);
|
|
2046
2102
|
// Cache type from HNSWVerb for future O(1) retrievals
|
|
2047
2103
|
this.verbTypeCache.set(hnswVerb.id, hnswVerb.verb);
|
|
2048
2104
|
// Load metadata separately (optional in v4.0.0!)
|
|
2049
2105
|
// FIX: Don't skip verbs without metadata - metadata is optional!
|
|
2050
2106
|
const metadata = await this.getVerbMetadata(hnswVerb.id);
|
|
2051
|
-
// Create HNSWVerbWithMetadata (verbs don't have level field)
|
|
2052
|
-
// Convert connections from plain object to Map<number, Set<string>>
|
|
2053
|
-
const connectionsMap = new Map();
|
|
2054
|
-
if (hnswVerb.connections && typeof hnswVerb.connections === 'object') {
|
|
2055
|
-
for (const [level, ids] of Object.entries(hnswVerb.connections)) {
|
|
2056
|
-
connectionsMap.set(Number(level), new Set(ids));
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
2107
|
// v4.8.0: Extract standard fields from metadata to top-level
|
|
2060
2108
|
const metadataObj = (metadata || {});
|
|
2061
2109
|
const { createdAt, updatedAt, confidence, weight, service, data, createdBy, ...customMetadata } = metadataObj;
|
|
2062
2110
|
const verbWithMetadata = {
|
|
2063
2111
|
id: hnswVerb.id,
|
|
2064
2112
|
vector: [...hnswVerb.vector],
|
|
2065
|
-
connections:
|
|
2113
|
+
connections: hnswVerb.connections, // v5.7.10: Already deserialized
|
|
2066
2114
|
verb: hnswVerb.verb,
|
|
2067
2115
|
sourceId: hnswVerb.sourceId,
|
|
2068
2116
|
targetId: hnswVerb.targetId,
|
|
@@ -1322,7 +1322,8 @@ export class MetadataIndexManager {
|
|
|
1322
1322
|
// exists: boolean - check if field exists (any value)
|
|
1323
1323
|
case 'exists':
|
|
1324
1324
|
if (operand) {
|
|
1325
|
-
// Get all IDs that have this field (any value)
|
|
1325
|
+
// exists: true - Get all IDs that have this field (any value)
|
|
1326
|
+
// v3.43.0: From chunked sparse index with roaring bitmaps
|
|
1326
1327
|
// v3.44.1: Now fully lazy-loaded via UnifiedCache (no local sparseIndices Map)
|
|
1327
1328
|
const allIntIds = new Set();
|
|
1328
1329
|
// Load sparse index via UnifiedCache (lazy loading)
|
|
@@ -1344,6 +1345,76 @@ export class MetadataIndexManager {
|
|
|
1344
1345
|
// Convert integer IDs back to UUIDs
|
|
1345
1346
|
fieldResults = this.idMapper.intsIterableToUuids(allIntIds);
|
|
1346
1347
|
}
|
|
1348
|
+
else {
|
|
1349
|
+
// exists: false - Get all IDs that DON'T have this field
|
|
1350
|
+
// v5.7.9: Fixed excludeVFS bug (was returning empty array)
|
|
1351
|
+
const allItemIds = await this.getAllIds();
|
|
1352
|
+
const existsIntIds = new Set();
|
|
1353
|
+
// Get IDs that HAVE this field
|
|
1354
|
+
const sparseIndex = await this.loadSparseIndex(field);
|
|
1355
|
+
if (sparseIndex) {
|
|
1356
|
+
for (const chunkId of sparseIndex.getAllChunkIds()) {
|
|
1357
|
+
const chunk = await this.chunkManager.loadChunk(field, chunkId);
|
|
1358
|
+
if (chunk) {
|
|
1359
|
+
for (const bitmap of chunk.entries.values()) {
|
|
1360
|
+
for (const intId of bitmap) {
|
|
1361
|
+
existsIntIds.add(intId);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
// Convert to UUIDs and subtract from all IDs
|
|
1368
|
+
const existsUuids = this.idMapper.intsIterableToUuids(existsIntIds);
|
|
1369
|
+
const existsSet = new Set(existsUuids);
|
|
1370
|
+
fieldResults = allItemIds.filter(id => !existsSet.has(id));
|
|
1371
|
+
}
|
|
1372
|
+
break;
|
|
1373
|
+
// ===== MISSING OPERATOR =====
|
|
1374
|
+
// missing: boolean - equivalent to exists: !boolean (for compatibility with metadataFilter.ts)
|
|
1375
|
+
case 'missing':
|
|
1376
|
+
// missing: true is equivalent to exists: false
|
|
1377
|
+
// missing: false is equivalent to exists: true
|
|
1378
|
+
// v5.7.9: Added for API consistency with in-memory metadataFilter
|
|
1379
|
+
if (operand) {
|
|
1380
|
+
// missing: true - field does NOT exist (same as exists: false)
|
|
1381
|
+
const allItemIds = await this.getAllIds();
|
|
1382
|
+
const existsIntIds = new Set();
|
|
1383
|
+
const sparseIndex = await this.loadSparseIndex(field);
|
|
1384
|
+
if (sparseIndex) {
|
|
1385
|
+
for (const chunkId of sparseIndex.getAllChunkIds()) {
|
|
1386
|
+
const chunk = await this.chunkManager.loadChunk(field, chunkId);
|
|
1387
|
+
if (chunk) {
|
|
1388
|
+
for (const bitmap of chunk.entries.values()) {
|
|
1389
|
+
for (const intId of bitmap) {
|
|
1390
|
+
existsIntIds.add(intId);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const existsUuids = this.idMapper.intsIterableToUuids(existsIntIds);
|
|
1397
|
+
const existsSet = new Set(existsUuids);
|
|
1398
|
+
fieldResults = allItemIds.filter(id => !existsSet.has(id));
|
|
1399
|
+
}
|
|
1400
|
+
else {
|
|
1401
|
+
// missing: false - field DOES exist (same as exists: true)
|
|
1402
|
+
const allIntIds = new Set();
|
|
1403
|
+
const sparseIndex = await this.loadSparseIndex(field);
|
|
1404
|
+
if (sparseIndex) {
|
|
1405
|
+
for (const chunkId of sparseIndex.getAllChunkIds()) {
|
|
1406
|
+
const chunk = await this.chunkManager.loadChunk(field, chunkId);
|
|
1407
|
+
if (chunk) {
|
|
1408
|
+
for (const bitmap of chunk.entries.values()) {
|
|
1409
|
+
for (const intId of bitmap) {
|
|
1410
|
+
allIntIds.add(intId);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
fieldResults = this.idMapper.intsIterableToUuids(allIntIds);
|
|
1417
|
+
}
|
|
1347
1418
|
break;
|
|
1348
1419
|
}
|
|
1349
1420
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "5.7.
|
|
3
|
+
"version": "5.7.10",
|
|
4
4
|
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|