@soulcraft/brainy 3.31.0 → 3.32.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/dist/neural/improvedNeuralAPI.js +38 -40
- package/dist/neural/naturalLanguageProcessor.js +3 -1
- package/dist/neural/types.d.ts +7 -1
- package/dist/storage/adapters/fileSystemStorage.js +16 -1
- package/dist/storage/adapters/gcsStorage.js +4 -0
- package/dist/storage/adapters/memoryStorage.js +13 -5
- package/dist/storage/adapters/opfsStorage.js +2 -0
- package/dist/storage/adapters/s3CompatibleStorage.js +4 -13
- package/dist/storage/baseStorage.d.ts +5 -0
- package/dist/storage/baseStorage.js +29 -2
- package/dist/utils/paramValidation.js +2 -3
- package/package.json +1 -1
|
@@ -577,26 +577,35 @@ export class ImprovedNeuralAPI {
|
|
|
577
577
|
*/
|
|
578
578
|
async hierarchy(id, options = {}) {
|
|
579
579
|
const startTime = performance.now();
|
|
580
|
+
const cacheKey = `hierarchy:${id}:${JSON.stringify(options)}`;
|
|
581
|
+
if (this.hierarchyCache.has(cacheKey)) {
|
|
582
|
+
return this.hierarchyCache.get(cacheKey);
|
|
583
|
+
}
|
|
584
|
+
// Get item data - handle non-existent and invalid IDs gracefully
|
|
585
|
+
let item;
|
|
580
586
|
try {
|
|
581
|
-
|
|
582
|
-
if (this.hierarchyCache.has(cacheKey)) {
|
|
583
|
-
return this.hierarchyCache.get(cacheKey);
|
|
584
|
-
}
|
|
585
|
-
// Get item data
|
|
586
|
-
const item = await this.brain.get(id);
|
|
587
|
-
if (!item) {
|
|
588
|
-
throw new Error(`Item with ID ${id} not found`);
|
|
589
|
-
}
|
|
590
|
-
// Build hierarchy based on strategy
|
|
591
|
-
const hierarchy = await this._buildSemanticHierarchy(item, options);
|
|
592
|
-
this._cacheResult(cacheKey, hierarchy, this.hierarchyCache);
|
|
593
|
-
this._trackPerformance('hierarchy', startTime, 1, 'hierarchy');
|
|
594
|
-
return hierarchy;
|
|
587
|
+
item = await this.brain.get(id);
|
|
595
588
|
}
|
|
596
589
|
catch (error) {
|
|
597
|
-
|
|
598
|
-
|
|
590
|
+
// Handle validation errors, non-existent IDs, etc. gracefully
|
|
591
|
+
// Return empty hierarchy instead of throwing
|
|
592
|
+
return {
|
|
593
|
+
root: null,
|
|
594
|
+
levels: []
|
|
595
|
+
};
|
|
599
596
|
}
|
|
597
|
+
if (!item) {
|
|
598
|
+
// Return empty hierarchy for non-existent IDs
|
|
599
|
+
return {
|
|
600
|
+
root: null,
|
|
601
|
+
levels: []
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// Build hierarchy based on strategy
|
|
605
|
+
const hierarchy = await this._buildSemanticHierarchy(item, options);
|
|
606
|
+
this._cacheResult(cacheKey, hierarchy, this.hierarchyCache);
|
|
607
|
+
this._trackPerformance('hierarchy', startTime, 1, 'hierarchy');
|
|
608
|
+
return hierarchy;
|
|
600
609
|
}
|
|
601
610
|
// ===== PUBLIC API: ANALYSIS =====
|
|
602
611
|
/**
|
|
@@ -1908,27 +1917,10 @@ export class ImprovedNeuralAPI {
|
|
|
1908
1917
|
if (!result || !Array.isArray(result)) {
|
|
1909
1918
|
return [];
|
|
1910
1919
|
}
|
|
1911
|
-
//
|
|
1920
|
+
// Include ALL items for domain clustering - those without the field will be assigned to 'unknown' domain
|
|
1912
1921
|
const itemsWithField = result.filter((item) => {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
const entity = item.entity;
|
|
1916
|
-
// Check root level fields first (e.g., 'noun' for type)
|
|
1917
|
-
if (field === 'type' || field === 'nounType') {
|
|
1918
|
-
return entity.noun != null;
|
|
1919
|
-
}
|
|
1920
|
-
// Check if field exists at root level
|
|
1921
|
-
if (entity[field] != null) {
|
|
1922
|
-
return true;
|
|
1923
|
-
}
|
|
1924
|
-
// Check if field exists in metadata/data
|
|
1925
|
-
if (entity.metadata?.[field] != null) {
|
|
1926
|
-
return true;
|
|
1927
|
-
}
|
|
1928
|
-
if (entity.data?.[field] != null) {
|
|
1929
|
-
return true;
|
|
1930
|
-
}
|
|
1931
|
-
return false;
|
|
1922
|
+
// Just ensure item has entity
|
|
1923
|
+
return item && item.entity;
|
|
1932
1924
|
});
|
|
1933
1925
|
// Map to format expected by clustering methods
|
|
1934
1926
|
return itemsWithField.map((item) => {
|
|
@@ -1940,13 +1932,13 @@ export class ImprovedNeuralAPI {
|
|
|
1940
1932
|
...(entity.metadata || {}),
|
|
1941
1933
|
...(entity.data || {}),
|
|
1942
1934
|
// Include root-level fields in metadata for easy access
|
|
1943
|
-
noun: entity.
|
|
1944
|
-
type: entity.
|
|
1935
|
+
noun: entity.type,
|
|
1936
|
+
type: entity.type,
|
|
1945
1937
|
createdAt: entity.createdAt,
|
|
1946
1938
|
updatedAt: entity.updatedAt,
|
|
1947
1939
|
label: entity.label
|
|
1948
1940
|
},
|
|
1949
|
-
nounType: entity.
|
|
1941
|
+
nounType: entity.type,
|
|
1950
1942
|
label: entity.label || entity.data || '',
|
|
1951
1943
|
data: entity.data
|
|
1952
1944
|
};
|
|
@@ -2642,8 +2634,14 @@ export class ImprovedNeuralAPI {
|
|
|
2642
2634
|
}
|
|
2643
2635
|
async _buildSemanticHierarchy(item, options) {
|
|
2644
2636
|
// Build semantic hierarchy around an item
|
|
2637
|
+
// Return structure expected by tests: { root, levels }
|
|
2645
2638
|
return {
|
|
2646
|
-
|
|
2639
|
+
root: {
|
|
2640
|
+
id: item.id,
|
|
2641
|
+
vector: item.vector,
|
|
2642
|
+
metadata: item.metadata
|
|
2643
|
+
},
|
|
2644
|
+
levels: []
|
|
2647
2645
|
};
|
|
2648
2646
|
}
|
|
2649
2647
|
async _detectOutliersClusterBased(threshold, options) {
|
|
@@ -440,7 +440,9 @@ export class NaturalLanguageProcessor {
|
|
|
440
440
|
tripleQuery.where = {};
|
|
441
441
|
for (const match of fieldMatches) {
|
|
442
442
|
// Extract value for this field from query
|
|
443
|
-
|
|
443
|
+
// Escape special regex characters in the term
|
|
444
|
+
const escapedTerm = match.term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
445
|
+
const valuePattern = new RegExp(`${escapedTerm}\\s*(?:is|=|:)?\\s*(\\S+)`, 'i');
|
|
444
446
|
const valueMatch = query.match(valuePattern);
|
|
445
447
|
if (valueMatch) {
|
|
446
448
|
tripleQuery.where[match.field] = valueMatch[1];
|
package/dist/neural/types.d.ts
CHANGED
|
@@ -130,11 +130,17 @@ export interface NeighborsResult {
|
|
|
130
130
|
averageSimilarity: number;
|
|
131
131
|
}
|
|
132
132
|
export interface SemanticHierarchy {
|
|
133
|
-
self
|
|
133
|
+
self?: {
|
|
134
134
|
id: string;
|
|
135
135
|
vector?: Vector;
|
|
136
136
|
metadata?: any;
|
|
137
137
|
};
|
|
138
|
+
root?: {
|
|
139
|
+
id: string;
|
|
140
|
+
vector?: Vector;
|
|
141
|
+
metadata?: any;
|
|
142
|
+
} | null;
|
|
143
|
+
levels?: any[];
|
|
138
144
|
parent?: {
|
|
139
145
|
id: string;
|
|
140
146
|
similarity: number;
|
|
@@ -437,7 +437,8 @@ export class FileSystemStorage extends BaseStorage {
|
|
|
437
437
|
*/
|
|
438
438
|
async deleteEdge(id) {
|
|
439
439
|
await this.ensureInitialized();
|
|
440
|
-
|
|
440
|
+
// Delete the HNSWVerb file using sharded path
|
|
441
|
+
const filePath = this.getVerbPath(id);
|
|
441
442
|
try {
|
|
442
443
|
await fs.promises.unlink(filePath);
|
|
443
444
|
}
|
|
@@ -447,6 +448,20 @@ export class FileSystemStorage extends BaseStorage {
|
|
|
447
448
|
throw error;
|
|
448
449
|
}
|
|
449
450
|
}
|
|
451
|
+
// CRITICAL: Also delete verb metadata - this is what getVerbs() uses to find verbs
|
|
452
|
+
// Without this, getVerbsBySource() will still find "deleted" verbs via their metadata
|
|
453
|
+
try {
|
|
454
|
+
const metadata = await this.getVerbMetadata(id);
|
|
455
|
+
if (metadata) {
|
|
456
|
+
const verbType = metadata.verb || metadata.type || 'default';
|
|
457
|
+
this.decrementVerbCount(verbType);
|
|
458
|
+
await this.deleteVerbMetadata(id);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
// Ignore metadata deletion errors - verb file is already deleted
|
|
463
|
+
console.warn(`Failed to delete verb metadata for ${id}:`, error);
|
|
464
|
+
}
|
|
450
465
|
}
|
|
451
466
|
/**
|
|
452
467
|
* Primitive operation: Write object to path
|
|
@@ -789,6 +789,7 @@ export class GcsStorage extends BaseStorage {
|
|
|
789
789
|
: undefined;
|
|
790
790
|
return {
|
|
791
791
|
nodes,
|
|
792
|
+
totalCount: this.totalNounCount,
|
|
792
793
|
hasMore: !!nextCursor,
|
|
793
794
|
nextCursor
|
|
794
795
|
};
|
|
@@ -797,6 +798,7 @@ export class GcsStorage extends BaseStorage {
|
|
|
797
798
|
if (response?.nextPageToken) {
|
|
798
799
|
return {
|
|
799
800
|
nodes,
|
|
801
|
+
totalCount: this.totalNounCount,
|
|
800
802
|
hasMore: true,
|
|
801
803
|
nextCursor: `${shardIndex}:${response.nextPageToken}`
|
|
802
804
|
};
|
|
@@ -806,6 +808,7 @@ export class GcsStorage extends BaseStorage {
|
|
|
806
808
|
// No more shards or nodes
|
|
807
809
|
return {
|
|
808
810
|
nodes,
|
|
811
|
+
totalCount: this.totalNounCount,
|
|
809
812
|
hasMore: false,
|
|
810
813
|
nextCursor: undefined
|
|
811
814
|
};
|
|
@@ -943,6 +946,7 @@ export class GcsStorage extends BaseStorage {
|
|
|
943
946
|
}
|
|
944
947
|
return {
|
|
945
948
|
items: filteredVerbs,
|
|
949
|
+
totalCount: this.totalVerbCount,
|
|
946
950
|
hasMore: !!response?.nextPageToken,
|
|
947
951
|
nextCursor: response?.nextPageToken
|
|
948
952
|
};
|
|
@@ -293,7 +293,7 @@ export class MemoryStorage extends BaseStorage {
|
|
|
293
293
|
// Iterate through all verbs to find matches
|
|
294
294
|
for (const [verbId, hnswVerb] of this.verbs.entries()) {
|
|
295
295
|
// Get the metadata for this verb to do filtering
|
|
296
|
-
const metadata = this.
|
|
296
|
+
const metadata = await this.getVerbMetadata(verbId);
|
|
297
297
|
// Filter by verb type if specified
|
|
298
298
|
if (verbTypes && metadata && !verbTypes.includes(metadata.type || metadata.verb || '')) {
|
|
299
299
|
continue;
|
|
@@ -336,7 +336,7 @@ export class MemoryStorage extends BaseStorage {
|
|
|
336
336
|
const items = [];
|
|
337
337
|
for (const id of paginatedIds) {
|
|
338
338
|
const hnswVerb = this.verbs.get(id);
|
|
339
|
-
const metadata = this.
|
|
339
|
+
const metadata = await this.getVerbMetadata(id);
|
|
340
340
|
if (!hnswVerb)
|
|
341
341
|
continue;
|
|
342
342
|
if (!metadata) {
|
|
@@ -365,7 +365,7 @@ export class MemoryStorage extends BaseStorage {
|
|
|
365
365
|
updatedAt: metadata.updatedAt,
|
|
366
366
|
createdBy: metadata.createdBy,
|
|
367
367
|
data: metadata.data,
|
|
368
|
-
metadata: metadata.data //
|
|
368
|
+
metadata: metadata.metadata || metadata.data // Use metadata.metadata (user's custom metadata)
|
|
369
369
|
};
|
|
370
370
|
items.push(graphVerb);
|
|
371
371
|
}
|
|
@@ -416,9 +416,17 @@ export class MemoryStorage extends BaseStorage {
|
|
|
416
416
|
* Delete a verb from storage
|
|
417
417
|
*/
|
|
418
418
|
async deleteVerb_internal(id) {
|
|
419
|
-
//
|
|
420
|
-
// since HNSWVerb doesn't contain type information
|
|
419
|
+
// Delete the HNSWVerb from the verbs map
|
|
421
420
|
this.verbs.delete(id);
|
|
421
|
+
// CRITICAL: Also delete verb metadata - this is what getVerbs() uses to find verbs
|
|
422
|
+
// Without this, getVerbsBySource() will still find "deleted" verbs via their metadata
|
|
423
|
+
const metadata = await this.getVerbMetadata(id);
|
|
424
|
+
if (metadata) {
|
|
425
|
+
const verbType = metadata.verb || metadata.type || 'default';
|
|
426
|
+
this.decrementVerbCount(verbType);
|
|
427
|
+
// Delete the metadata using the base storage method
|
|
428
|
+
await this.deleteVerbMetadata(id);
|
|
429
|
+
}
|
|
422
430
|
}
|
|
423
431
|
/**
|
|
424
432
|
* Primitive operation: Write object to path
|
|
@@ -81,6 +81,8 @@ export class OPFSStorage extends BaseStorage {
|
|
|
81
81
|
this.indexDir = await this.rootDir.getDirectoryHandle(INDEX_DIR, {
|
|
82
82
|
create: true
|
|
83
83
|
});
|
|
84
|
+
// Initialize counts from storage
|
|
85
|
+
await this.initializeCounts();
|
|
84
86
|
this.isInitialized = true;
|
|
85
87
|
}
|
|
86
88
|
catch (error) {
|
|
@@ -235,6 +235,8 @@ export class S3CompatibleStorage extends BaseStorage {
|
|
|
235
235
|
this.initializeCoalescer();
|
|
236
236
|
// Auto-cleanup legacy /index folder on initialization
|
|
237
237
|
await this.cleanupLegacyIndexFolder();
|
|
238
|
+
// Initialize counts from storage
|
|
239
|
+
await this.initializeCounts();
|
|
238
240
|
this.isInitialized = true;
|
|
239
241
|
this.logger.info(`Initialized ${this.serviceType} storage with bucket ${this.bucketName}`);
|
|
240
242
|
}
|
|
@@ -1425,6 +1427,7 @@ export class S3CompatibleStorage extends BaseStorage {
|
|
|
1425
1427
|
}
|
|
1426
1428
|
return {
|
|
1427
1429
|
items: filteredGraphVerbs,
|
|
1430
|
+
totalCount: this.totalVerbCount, // Use pre-calculated count from init()
|
|
1428
1431
|
hasMore: result.hasMore,
|
|
1429
1432
|
nextCursor: result.nextCursor
|
|
1430
1433
|
};
|
|
@@ -2633,21 +2636,9 @@ export class S3CompatibleStorage extends BaseStorage {
|
|
|
2633
2636
|
filteredNodes = filteredByMetadata;
|
|
2634
2637
|
}
|
|
2635
2638
|
}
|
|
2636
|
-
// Calculate total count efficiently
|
|
2637
|
-
// For the first page (no cursor), we can estimate total count
|
|
2638
|
-
let totalCount;
|
|
2639
|
-
if (!cursor) {
|
|
2640
|
-
try {
|
|
2641
|
-
totalCount = await this.estimateTotalNounCount();
|
|
2642
|
-
}
|
|
2643
|
-
catch (error) {
|
|
2644
|
-
this.logger.warn('Failed to estimate total noun count:', error);
|
|
2645
|
-
// totalCount remains undefined
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
2639
|
return {
|
|
2649
2640
|
items: filteredNodes,
|
|
2650
|
-
totalCount,
|
|
2641
|
+
totalCount: this.totalNounCount, // Use pre-calculated count from init()
|
|
2651
2642
|
hasMore: result.hasMore,
|
|
2652
2643
|
nextCursor: result.nextCursor
|
|
2653
2644
|
};
|
|
@@ -245,6 +245,11 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
|
|
|
245
245
|
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
|
|
246
246
|
*/
|
|
247
247
|
getVerbMetadata(id: string): Promise<any | null>;
|
|
248
|
+
/**
|
|
249
|
+
* Delete verb metadata from storage
|
|
250
|
+
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
|
|
251
|
+
*/
|
|
252
|
+
deleteVerbMetadata(id: string): Promise<void>;
|
|
248
253
|
/**
|
|
249
254
|
* Save a noun to storage
|
|
250
255
|
* This method should be implemented by each specific adapter
|
|
@@ -422,9 +422,18 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
422
422
|
// If we have no items but hasMore is true, force hasMore to false
|
|
423
423
|
// This prevents pagination bugs from causing infinite loops
|
|
424
424
|
const safeHasMore = items.length > 0 ? result.hasMore : false;
|
|
425
|
+
// VALIDATION: Ensure adapter returns totalCount (prevents restart bugs)
|
|
426
|
+
// If adapter forgets to return totalCount, log warning and use pre-calculated count
|
|
427
|
+
let finalTotalCount = result.totalCount || totalCount;
|
|
428
|
+
if (result.totalCount === undefined && this.totalNounCount > 0) {
|
|
429
|
+
console.warn(`⚠️ Storage adapter missing totalCount in getNounsWithPagination result! ` +
|
|
430
|
+
`Using pre-calculated count (${this.totalNounCount}) as fallback. ` +
|
|
431
|
+
`Please ensure your storage adapter returns totalCount: this.totalNounCount`);
|
|
432
|
+
finalTotalCount = this.totalNounCount;
|
|
433
|
+
}
|
|
425
434
|
return {
|
|
426
435
|
items,
|
|
427
|
-
totalCount:
|
|
436
|
+
totalCount: finalTotalCount,
|
|
428
437
|
hasMore: safeHasMore,
|
|
429
438
|
nextCursor: result.nextCursor
|
|
430
439
|
};
|
|
@@ -571,9 +580,18 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
571
580
|
// If we have no items but hasMore is true, force hasMore to false
|
|
572
581
|
// This prevents pagination bugs from causing infinite loops
|
|
573
582
|
const safeHasMore = items.length > 0 ? result.hasMore : false;
|
|
583
|
+
// VALIDATION: Ensure adapter returns totalCount (prevents restart bugs)
|
|
584
|
+
// If adapter forgets to return totalCount, log warning and use pre-calculated count
|
|
585
|
+
let finalTotalCount = result.totalCount || totalCount;
|
|
586
|
+
if (result.totalCount === undefined && this.totalVerbCount > 0) {
|
|
587
|
+
console.warn(`⚠️ Storage adapter missing totalCount in getVerbsWithPagination result! ` +
|
|
588
|
+
`Using pre-calculated count (${this.totalVerbCount}) as fallback. ` +
|
|
589
|
+
`Please ensure your storage adapter returns totalCount: this.totalVerbCount`);
|
|
590
|
+
finalTotalCount = this.totalVerbCount;
|
|
591
|
+
}
|
|
574
592
|
return {
|
|
575
593
|
items,
|
|
576
|
-
totalCount:
|
|
594
|
+
totalCount: finalTotalCount,
|
|
577
595
|
hasMore: safeHasMore,
|
|
578
596
|
nextCursor: result.nextCursor
|
|
579
597
|
};
|
|
@@ -696,6 +714,15 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
696
714
|
const keyInfo = this.analyzeKey(id, 'verb-metadata');
|
|
697
715
|
return this.readObjectFromPath(keyInfo.fullPath);
|
|
698
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Delete verb metadata from storage
|
|
719
|
+
* Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
|
|
720
|
+
*/
|
|
721
|
+
async deleteVerbMetadata(id) {
|
|
722
|
+
await this.ensureInitialized();
|
|
723
|
+
const keyInfo = this.analyzeKey(id, 'verb-metadata');
|
|
724
|
+
return this.deleteObjectFromPath(keyInfo.fullPath);
|
|
725
|
+
}
|
|
699
726
|
/**
|
|
700
727
|
* Helper method to convert a Map to a plain object for serialization
|
|
701
728
|
*/
|
|
@@ -192,9 +192,8 @@ export function validateRelateParams(params) {
|
|
|
192
192
|
if (!params.to) {
|
|
193
193
|
throw new Error('to entity ID is required');
|
|
194
194
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
195
|
+
// Allow self-referential relationships - they're valid in graph systems
|
|
196
|
+
// (e.g., a person can be related to themselves, a file can reference itself, etc.)
|
|
198
197
|
// Validate verb type - default to RelatedTo if not specified
|
|
199
198
|
if (params.type === undefined) {
|
|
200
199
|
params.type = VerbType.RelatedTo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.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",
|