@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.
@@ -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
- const cacheKey = `hierarchy:${id}:${JSON.stringify(options)}`;
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
- const errorMessage = error instanceof Error ? error.message : String(error);
598
- throw new NeuralAPIError(`Failed to build hierarchy: ${errorMessage}`, 'HIERARCHY_ERROR', { id, options });
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
- // Filter items that have the specified field (check both root level and metadata)
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
- if (!item || !item.entity)
1914
- return false;
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.noun,
1944
- type: entity.noun,
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.noun,
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
- self: { id: item.id, vector: item.vector, metadata: item.metadata }
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
- const valuePattern = new RegExp(`${match.term}\\s*(?:is|=|:)?\\s*(\\S+)`, 'i');
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];
@@ -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
- const filePath = path.join(this.verbsDir, `${id}.json`);
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.verbMetadata.get(verbId);
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.verbMetadata.get(id);
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 // Alias for backward compatibility
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
- // Count tracking will be handled when verb metadata is deleted
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: result.totalCount || 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: result.totalCount || 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
- if (params.from === params.to) {
196
- throw new Error('cannot create self-referential relationship');
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.31.0",
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",