@soulcraft/brainy 6.2.1 → 6.2.3

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 CHANGED
@@ -2,6 +2,12 @@
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
+ ### [6.2.2](https://github.com/soulcraftlabs/brainy/compare/v6.2.1...v6.2.2) (2025-11-25)
6
+
7
+ - refactor: remove 3,700+ LOC of unused HNSW implementations (e3146ce)
8
+ - fix(hnsw): entry point recovery prevents import failures and log spam (52eae67)
9
+
10
+
5
11
  ## [6.2.0](https://github.com/soulcraftlabs/brainy/compare/v6.1.0...v6.2.0) (2025-11-20)
6
12
 
7
13
  ### ⚡ Critical Performance Fix
package/dist/brainy.js CHANGED
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import { v4 as uuidv4 } from './universal/uuid.js';
8
8
  import { HNSWIndex } from './hnsw/hnswIndex.js';
9
- import { HNSWIndexOptimized } from './hnsw/hnswIndexOptimized.js';
10
9
  import { TypeAwareHNSWIndex } from './hnsw/typeAwareHNSWIndex.js';
11
10
  import { createStorage } from './storage/storageFactory.js';
12
11
  import { defaultEmbeddingFunction, cosineDistance } from './utils/index.js';
@@ -802,7 +801,7 @@ export class Brainy {
802
801
  if (this.index instanceof TypeAwareHNSWIndex && metadata.noun) {
803
802
  tx.addOperation(new RemoveFromTypeAwareHNSWOperation(this.index, id, noun.vector, metadata.noun));
804
803
  }
805
- else if (this.index instanceof HNSWIndex || this.index instanceof HNSWIndexOptimized) {
804
+ else if (this.index instanceof HNSWIndex) {
806
805
  tx.addOperation(new RemoveFromHNSWOperation(this.index, id, noun.vector));
807
806
  }
808
807
  }
@@ -1887,7 +1886,7 @@ export class Brainy {
1887
1886
  if (this.index instanceof TypeAwareHNSWIndex && metadata.noun) {
1888
1887
  tx.addOperation(new RemoveFromTypeAwareHNSWOperation(this.index, id, noun.vector, metadata.noun));
1889
1888
  }
1890
- else if (this.index instanceof HNSWIndex || this.index instanceof HNSWIndexOptimized) {
1889
+ else if (this.index instanceof HNSWIndex) {
1891
1890
  tx.addOperation(new RemoveFromHNSWOperation(this.index, id, noun.vector));
1892
1891
  }
1893
1892
  }
@@ -83,6 +83,12 @@ export declare class HNSWIndex {
83
83
  * Add a vector to the index
84
84
  */
85
85
  addItem(item: VectorDocument): Promise<string>;
86
+ /**
87
+ * O(1) entry point recovery using highLevelNodes index (v6.2.3).
88
+ * At any reasonable scale (1000+ nodes), level 2+ nodes are guaranteed to exist.
89
+ * For tiny indexes with only level 0-1 nodes, any node works as entry point.
90
+ */
91
+ private recoverEntryPointO1;
86
92
  /**
87
93
  * Search for nearest neighbors
88
94
  */
@@ -210,9 +210,8 @@ export class HNSWIndex {
210
210
  }
211
211
  // Find entry point
212
212
  if (!this.entryPointId) {
213
- console.error('Entry point ID is null');
214
- // If there's no entry point, this is the first noun, so we should have returned earlier
215
- // This is a safety check
213
+ // No entry point but nouns exist - corrupted state, recover by using this item
214
+ // This shouldn't normally happen as first item sets entry point above
216
215
  this.entryPointId = id;
217
216
  this.maxLevel = nounLevel;
218
217
  this.nouns.set(id, noun);
@@ -220,7 +219,7 @@ export class HNSWIndex {
220
219
  }
221
220
  const entryPoint = this.nouns.get(this.entryPointId);
222
221
  if (!entryPoint) {
223
- console.error(`Entry point with ID ${this.entryPointId} not found`);
222
+ // Entry point was deleted but ID not updated - recover by using new item
224
223
  // If the entry point doesn't exist, treat this as the first noun
225
224
  this.entryPointId = id;
226
225
  this.maxLevel = nounLevel;
@@ -383,6 +382,27 @@ export class HNSWIndex {
383
382
  }
384
383
  return id;
385
384
  }
385
+ /**
386
+ * O(1) entry point recovery using highLevelNodes index (v6.2.3).
387
+ * At any reasonable scale (1000+ nodes), level 2+ nodes are guaranteed to exist.
388
+ * For tiny indexes with only level 0-1 nodes, any node works as entry point.
389
+ */
390
+ recoverEntryPointO1() {
391
+ // O(1) recovery: check highLevelNodes from highest to lowest level
392
+ for (let level = this.MAX_TRACKED_LEVELS; level >= 2; level--) {
393
+ const nodesAtLevel = this.highLevelNodes.get(level);
394
+ if (nodesAtLevel && nodesAtLevel.size > 0) {
395
+ for (const nodeId of nodesAtLevel) {
396
+ if (this.nouns.has(nodeId)) {
397
+ return { id: nodeId, level };
398
+ }
399
+ }
400
+ }
401
+ }
402
+ // No high-level nodes - use any available node (works fine for HNSW)
403
+ const firstNode = this.nouns.keys().next().value;
404
+ return { id: firstNode ?? null, level: 0 };
405
+ }
386
406
  /**
387
407
  * Search for nearest neighbors
388
408
  */
@@ -398,14 +418,33 @@ export class HNSWIndex {
398
418
  throw new Error(`Query vector dimension mismatch: expected ${this.dimension}, got ${queryVector.length}`);
399
419
  }
400
420
  // Start from the entry point
421
+ // If entry point is null but nouns exist, attempt O(1) recovery (v6.2.3)
422
+ if (!this.entryPointId && this.nouns.size > 0) {
423
+ const { id: recoveredId, level: recoveredLevel } = this.recoverEntryPointO1();
424
+ if (recoveredId) {
425
+ this.entryPointId = recoveredId;
426
+ this.maxLevel = recoveredLevel;
427
+ }
428
+ }
401
429
  if (!this.entryPointId) {
402
- console.error('Entry point ID is null');
430
+ // Truly empty index - return empty results silently
403
431
  return [];
404
432
  }
405
- const entryPoint = this.nouns.get(this.entryPointId);
433
+ let entryPoint = this.nouns.get(this.entryPointId);
406
434
  if (!entryPoint) {
407
- console.error(`Entry point with ID ${this.entryPointId} not found`);
408
- return [];
435
+ // Entry point ID exists but noun was deleted - O(1) recovery (v6.2.3)
436
+ if (this.nouns.size > 0) {
437
+ const { id: recoveredId, level: recoveredLevel } = this.recoverEntryPointO1();
438
+ if (recoveredId) {
439
+ this.entryPointId = recoveredId;
440
+ this.maxLevel = recoveredLevel;
441
+ entryPoint = this.nouns.get(recoveredId);
442
+ }
443
+ }
444
+ // If still no entry point, return empty
445
+ if (!entryPoint) {
446
+ return [];
447
+ }
409
448
  }
410
449
  let currObj = entryPoint;
411
450
  // OPTIMIZATION: Preload entry point vector
@@ -915,6 +954,40 @@ export class HNSWIndex {
915
954
  offset += batchSize; // v5.7.11: Increment offset for next page
916
955
  }
917
956
  }
957
+ // Step 5: CRITICAL - Recover entry point if missing (v6.2.3 - O(1))
958
+ // This ensures consistency even if getHNSWSystem() returned null
959
+ if (this.nouns.size > 0 && this.entryPointId === null) {
960
+ prodLog.warn('HNSW rebuild: Entry point was null after loading nouns - recovering with O(1) lookup');
961
+ const { id: recoveredId, level: recoveredLevel } = this.recoverEntryPointO1();
962
+ this.entryPointId = recoveredId;
963
+ this.maxLevel = recoveredLevel;
964
+ prodLog.info(`HNSW entry point recovered: ${recoveredId} at level ${recoveredLevel}`);
965
+ // Persist recovered state to prevent future recovery
966
+ if (this.storage && recoveredId) {
967
+ await this.storage.saveHNSWSystem({
968
+ entryPointId: this.entryPointId,
969
+ maxLevel: this.maxLevel
970
+ }).catch((error) => {
971
+ prodLog.error('Failed to persist recovered HNSW system data:', error);
972
+ });
973
+ }
974
+ }
975
+ // Step 6: Validate entry point exists if set (handles stale/deleted entry point)
976
+ if (this.entryPointId && !this.nouns.has(this.entryPointId)) {
977
+ prodLog.warn(`HNSW: Entry point ${this.entryPointId} not found in loaded nouns - recovering with O(1) lookup`);
978
+ const { id: recoveredId, level: recoveredLevel } = this.recoverEntryPointO1();
979
+ this.entryPointId = recoveredId;
980
+ this.maxLevel = recoveredLevel;
981
+ // Persist corrected state
982
+ if (this.storage && recoveredId) {
983
+ await this.storage.saveHNSWSystem({
984
+ entryPointId: this.entryPointId,
985
+ maxLevel: this.maxLevel
986
+ }).catch((error) => {
987
+ prodLog.error('Failed to persist corrected HNSW system data:', error);
988
+ });
989
+ }
990
+ }
918
991
  const cacheInfo = shouldPreload
919
992
  ? ` (vectors preloaded)`
920
993
  : ` (adaptive caching - vectors loaded on-demand)`;
package/dist/index.d.ts CHANGED
@@ -51,9 +51,8 @@ export { StorageAugmentation, DynamicStorageAugmentation, MemoryStorageAugmentat
51
51
  export { WebSocketConduitAugmentation };
52
52
  import type { Vector, VectorDocument, SearchResult, DistanceFunction, EmbeddingFunction, EmbeddingModel, HNSWNoun, HNSWVerb, HNSWConfig, StorageAdapter } from './coreTypes.js';
53
53
  import { HNSWIndex } from './hnsw/hnswIndex.js';
54
- import { HNSWIndexOptimized, HNSWOptimizedConfig } from './hnsw/hnswIndexOptimized.js';
55
- export { HNSWIndex, HNSWIndexOptimized };
56
- export type { Vector, VectorDocument, SearchResult, DistanceFunction, EmbeddingFunction, EmbeddingModel, HNSWNoun, HNSWVerb, HNSWConfig, HNSWOptimizedConfig, StorageAdapter };
54
+ export { HNSWIndex };
55
+ export type { Vector, VectorDocument, SearchResult, DistanceFunction, EmbeddingFunction, EmbeddingModel, HNSWNoun, HNSWVerb, HNSWConfig, StorageAdapter };
57
56
  import type { AugmentationResponse, BrainyAugmentation, BaseAugmentation, AugmentationContext } from './types/augmentations.js';
58
57
  export { AugmentationManager, type AugmentationInfo } from './augmentationManager.js';
59
58
  export type { AugmentationResponse, BrainyAugmentation, BaseAugmentation, AugmentationContext };
package/dist/index.js CHANGED
@@ -111,10 +111,9 @@ MemoryStorageAugmentation, FileSystemStorageAugmentation, OPFSStorageAugmentatio
111
111
  createAutoStorageAugmentation, createStorageAugmentationFromConfig };
112
112
  // Other augmentation exports
113
113
  export { WebSocketConduitAugmentation };
114
- // Export HNSW index and optimized version
114
+ // Export HNSW index
115
115
  import { HNSWIndex } from './hnsw/hnswIndex.js';
116
- import { HNSWIndexOptimized } from './hnsw/hnswIndexOptimized.js';
117
- export { HNSWIndex, HNSWIndexOptimized };
116
+ export { HNSWIndex };
118
117
  // Export augmentation manager for type-safe augmentation management
119
118
  export { AugmentationManager } from './augmentationManager.js';
120
119
  import { NounType, VerbType } from './types/graphTypes.js';
@@ -1269,7 +1269,15 @@ export class AzureBlobStorage extends BaseStorage {
1269
1269
  return JSON.parse(downloaded.toString());
1270
1270
  }
1271
1271
  catch (error) {
1272
- if (error.statusCode === 404 || error.code === 'BlobNotFound') {
1272
+ // Azure may return not found errors in different formats
1273
+ const isNotFound = error.statusCode === 404 ||
1274
+ error.code === 'BlobNotFound' ||
1275
+ error.code === 404 ||
1276
+ error.details?.code === 'BlobNotFound' ||
1277
+ error.message?.includes('BlobNotFound') ||
1278
+ error.message?.includes('not found') ||
1279
+ error.message?.includes('404');
1280
+ if (isNotFound) {
1273
1281
  return null;
1274
1282
  }
1275
1283
  this.logger.error('Failed to get HNSW system data:', error);
@@ -1260,7 +1260,14 @@ export class GcsStorage extends BaseStorage {
1260
1260
  return JSON.parse(contents.toString());
1261
1261
  }
1262
1262
  catch (error) {
1263
- if (error.code === 404) {
1263
+ // GCS may return 404 in different formats depending on SDK version
1264
+ const is404 = error.code === 404 ||
1265
+ error.statusCode === 404 ||
1266
+ error.status === 404 ||
1267
+ error.message?.includes('No such object') ||
1268
+ error.message?.includes('not found') ||
1269
+ error.message?.includes('404');
1270
+ if (is404) {
1264
1271
  return null;
1265
1272
  }
1266
1273
  this.logger.error('Failed to get HNSW system data:', error);
@@ -2829,9 +2829,14 @@ export class S3CompatibleStorage extends BaseStorage {
2829
2829
  return JSON.parse(bodyContents);
2830
2830
  }
2831
2831
  catch (error) {
2832
- if (error.name === 'NoSuchKey' ||
2832
+ // S3 may return not found errors in different formats
2833
+ const isNotFound = error.name === 'NoSuchKey' ||
2834
+ error.code === 'NoSuchKey' ||
2835
+ error.$metadata?.httpStatusCode === 404 ||
2833
2836
  error.message?.includes('NoSuchKey') ||
2834
- error.message?.includes('not found')) {
2837
+ error.message?.includes('not found') ||
2838
+ error.message?.includes('404');
2839
+ if (isNotFound) {
2835
2840
  return null;
2836
2841
  }
2837
2842
  this.logger.error('Failed to get HNSW system data:', error);
@@ -618,6 +618,18 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
618
618
  * Periodically called when counts are updated
619
619
  */
620
620
  protected saveTypeStatistics(): Promise<void>;
621
+ /**
622
+ * Get noun counts by type (O(1) access to type statistics)
623
+ * v6.2.2: Exposed for MetadataIndexManager to use as single source of truth
624
+ * @returns Uint32Array indexed by NounType enum value (42 types)
625
+ */
626
+ getNounCountsByType(): Uint32Array;
627
+ /**
628
+ * Get verb counts by type (O(1) access to type statistics)
629
+ * v6.2.2: Exposed for MetadataIndexManager to use as single source of truth
630
+ * @returns Uint32Array indexed by VerbType enum value (127 types)
631
+ */
632
+ getVerbCountsByType(): Uint32Array;
621
633
  /**
622
634
  * Rebuild type counts from actual storage (v5.5.0)
623
635
  * Called when statistics are missing or inconsistent
@@ -1983,6 +1983,22 @@ export class BaseStorage extends BaseStorageAdapter {
1983
1983
  };
1984
1984
  await this.writeObjectToPath(`${SYSTEM_DIR}/type-statistics.json`, stats);
1985
1985
  }
1986
+ /**
1987
+ * Get noun counts by type (O(1) access to type statistics)
1988
+ * v6.2.2: Exposed for MetadataIndexManager to use as single source of truth
1989
+ * @returns Uint32Array indexed by NounType enum value (42 types)
1990
+ */
1991
+ getNounCountsByType() {
1992
+ return this.nounCountsByType;
1993
+ }
1994
+ /**
1995
+ * Get verb counts by type (O(1) access to type statistics)
1996
+ * v6.2.2: Exposed for MetadataIndexManager to use as single source of truth
1997
+ * @returns Uint32Array indexed by VerbType enum value (127 types)
1998
+ */
1999
+ getVerbCountsByType() {
2000
+ return this.verbCountsByType;
2001
+ }
1986
2002
  /**
1987
2003
  * Rebuild type counts from actual storage (v5.5.0)
1988
2004
  * Called when statistics are missing or inconsistent
@@ -13,7 +13,6 @@
13
13
  * - Fusion: O(k log k) where k = result count
14
14
  */
15
15
  import { HNSWIndex } from '../hnsw/hnswIndex.js';
16
- import { HNSWIndexOptimized } from '../hnsw/hnswIndexOptimized.js';
17
16
  import { TypeAwareHNSWIndex } from '../hnsw/typeAwareHNSWIndex.js';
18
17
  import { MetadataIndexManager } from '../utils/metadataIndex.js';
19
18
  import { Vector } from '../coreTypes.js';
@@ -68,7 +67,7 @@ export declare class TripleIntelligenceSystem {
68
67
  private planner;
69
68
  private embedder;
70
69
  private storage;
71
- constructor(metadataIndex: MetadataIndexManager, hnswIndex: HNSWIndex | HNSWIndexOptimized | TypeAwareHNSWIndex, graphIndex: GraphAdjacencyIndex, embedder: (text: string) => Promise<Vector>, storage: any);
70
+ constructor(metadataIndex: MetadataIndexManager, hnswIndex: HNSWIndex | TypeAwareHNSWIndex, graphIndex: GraphAdjacencyIndex, embedder: (text: string) => Promise<Vector>, storage: any);
72
71
  /**
73
72
  * Main find method - executes Triple Intelligence queries
74
73
  * Phase 3: Now with automatic type inference for 40% latency reduction
@@ -112,8 +112,9 @@ export declare class MetadataIndexManager {
112
112
  */
113
113
  private releaseLock;
114
114
  /**
115
- * Lazy load entity counts from storage statistics (O(1) operation)
116
- * This avoids rebuilding the entire index on startup
115
+ * Lazy load entity counts from the 'noun' field sparse index (O(n) where n = number of types)
116
+ * v6.2.2 FIX: Previously read from stats.nounCount which was SERVICE-keyed, not TYPE-keyed
117
+ * Now computes counts from the sparse index which has the correct type information
117
118
  */
118
119
  private lazyLoadCounts;
119
120
  /**
@@ -434,6 +435,9 @@ export declare class MetadataIndexManager {
434
435
  getTotalEntityCount(): number;
435
436
  /**
436
437
  * Get all entity types and their counts - O(1) operation
438
+ * v6.2.2: Fixed - totalEntitiesByType is correctly populated by updateTypeFieldAffinity
439
+ * during add operations. lazyLoadCounts was reading wrong data but that doesn't
440
+ * affect freshly-added entities within the same session.
437
441
  */
438
442
  getAllEntityCounts(): Map<string, number>;
439
443
  /**
@@ -84,8 +84,9 @@ export class MetadataIndexManager {
84
84
  this.chunkingStrategy = new AdaptiveChunkingStrategy();
85
85
  // Initialize Field Type Inference (v3.48.0)
86
86
  this.fieldTypeInference = new FieldTypeInference(storage);
87
- // Lazy load counts from storage statistics on first access
88
- this.lazyLoadCounts();
87
+ // v6.2.2: Removed lazyLoadCounts() call from constructor
88
+ // It was a race condition (not awaited) and read from wrong source.
89
+ // Now properly called in init() after warmCache() loads the sparse index.
89
90
  }
90
91
  /**
91
92
  * Initialize the metadata index manager
@@ -97,11 +98,15 @@ export class MetadataIndexManager {
97
98
  await this.loadFieldRegistry();
98
99
  // Initialize EntityIdMapper (loads UUID ↔ integer mappings from storage)
99
100
  await this.idMapper.init();
100
- // Phase 1b: Sync loaded counts to fixed-size arrays
101
- // This populates the Uint32Arrays from the Maps loaded by lazyLoadCounts()
102
- this.syncTypeCountsToFixed();
103
101
  // Warm the cache with common fields (v3.44.1 - lazy loading optimization)
102
+ // This loads the 'noun' sparse index which is needed for type counts
104
103
  await this.warmCache();
104
+ // v6.2.2: Load type counts AFTER warmCache (sparse index is now cached)
105
+ // Previously called in constructor without await and read from wrong source
106
+ await this.lazyLoadCounts();
107
+ // Phase 1b: Sync loaded counts to fixed-size arrays
108
+ // Now correctly happens AFTER lazyLoadCounts() finishes
109
+ this.syncTypeCountsToFixed();
105
110
  }
106
111
  /**
107
112
  * Warm the cache by preloading common field sparse indices (v3.44.1)
@@ -226,25 +231,34 @@ export class MetadataIndexManager {
226
231
  this.activeLocks.delete(lockKey);
227
232
  }
228
233
  /**
229
- * Lazy load entity counts from storage statistics (O(1) operation)
230
- * This avoids rebuilding the entire index on startup
234
+ * Lazy load entity counts from the 'noun' field sparse index (O(n) where n = number of types)
235
+ * v6.2.2 FIX: Previously read from stats.nounCount which was SERVICE-keyed, not TYPE-keyed
236
+ * Now computes counts from the sparse index which has the correct type information
231
237
  */
232
238
  async lazyLoadCounts() {
233
239
  try {
234
- // Get statistics from storage (should be O(1) with our FileSystemStorage improvements)
235
- const stats = await this.storage.getStatistics();
236
- if (stats && stats.nounCount) {
237
- // Populate entity counts from storage statistics
238
- for (const [type, count] of Object.entries(stats.nounCount)) {
239
- if (typeof count === 'number' && count > 0) {
240
- this.totalEntitiesByType.set(type, count);
240
+ // v6.2.2: Load counts from sparse index (correct source)
241
+ const nounSparseIndex = await this.loadSparseIndex('noun');
242
+ if (!nounSparseIndex) {
243
+ // No sparse index yet - counts will be populated as entities are added
244
+ return;
245
+ }
246
+ // Iterate through all chunks and sum up bitmap sizes by type
247
+ for (const chunkId of nounSparseIndex.getAllChunkIds()) {
248
+ const chunk = await this.chunkManager.loadChunk('noun', chunkId);
249
+ if (chunk) {
250
+ for (const [type, bitmap] of chunk.entries) {
251
+ const currentCount = this.totalEntitiesByType.get(type) || 0;
252
+ this.totalEntitiesByType.set(type, currentCount + bitmap.size);
241
253
  }
242
254
  }
243
255
  }
256
+ prodLog.debug(`✅ Loaded type counts from sparse index: ${this.totalEntitiesByType.size} types`);
244
257
  }
245
258
  catch (error) {
246
259
  // Silently fail - counts will be populated as entities are added
247
260
  // This maintains zero-configuration principle
261
+ prodLog.debug('Could not load type counts from sparse index:', error);
248
262
  }
249
263
  }
250
264
  /**
@@ -1875,6 +1889,9 @@ export class MetadataIndexManager {
1875
1889
  }
1876
1890
  /**
1877
1891
  * Get all entity types and their counts - O(1) operation
1892
+ * v6.2.2: Fixed - totalEntitiesByType is correctly populated by updateTypeFieldAffinity
1893
+ * during add operations. lazyLoadCounts was reading wrong data but that doesn't
1894
+ * affect freshly-added entities within the same session.
1878
1895
  */
1879
1896
  getAllEntityCounts() {
1880
1897
  return new Map(this.totalEntitiesByType);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "6.2.1",
3
+ "version": "6.2.3",
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",