@soulcraft/brainy 5.6.2 → 5.7.0

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.
@@ -10,6 +10,7 @@ import { getShardIdFromUuid } from './sharding.js';
10
10
  import { RefManager } from './cow/RefManager.js';
11
11
  import { BlobStorage } from './cow/BlobStorage.js';
12
12
  import { CommitLog } from './cow/CommitLog.js';
13
+ import { prodLog } from '../utils/logger.js';
13
14
  // Clean directory structure (v4.7.2+)
14
15
  // All storage adapters use this consistent structure
15
16
  export const NOUNS_METADATA_DIR = 'entities/nouns/metadata';
@@ -118,7 +119,7 @@ export class BaseStorage extends BaseStorageAdapter {
118
119
  // UUID validation for entity keys
119
120
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
120
121
  if (!uuidRegex.test(id)) {
121
- console.warn(`[Storage] Unknown key format: ${id} - treating as system resource`);
122
+ prodLog.warn(`[Storage] Unknown key format: ${id} - treating as system resource`);
122
123
  return {
123
124
  original: id,
124
125
  isEntity: false,
@@ -472,7 +473,7 @@ export class BaseStorage extends BaseStorageAdapter {
472
473
  // Load metadata
473
474
  const metadata = await this.getNounMetadata(id);
474
475
  if (!metadata) {
475
- console.warn(`[Storage] Noun ${id} has vector but no metadata - this should not happen in v4.0.0`);
476
+ prodLog.warn(`[Storage] Noun ${id} has vector but no metadata - this should not happen in v4.0.0`);
476
477
  return null;
477
478
  }
478
479
  // Combine into HNSWNounWithMetadata - v4.8.0: Extract standard fields to top-level
@@ -541,7 +542,7 @@ export class BaseStorage extends BaseStorageAdapter {
541
542
  }
542
543
  catch (error) {
543
544
  // Ignore if metadata file doesn't exist
544
- console.debug(`No metadata file to delete for noun ${id}`);
545
+ prodLog.debug(`No metadata file to delete for noun ${id}`);
545
546
  }
546
547
  }
547
548
  /**
@@ -572,7 +573,7 @@ export class BaseStorage extends BaseStorageAdapter {
572
573
  // Load metadata
573
574
  const metadata = await this.getVerbMetadata(id);
574
575
  if (!metadata) {
575
- console.warn(`[Storage] Verb ${id} has vector but no metadata - this should not happen in v4.0.0`);
576
+ prodLog.warn(`[Storage] Verb ${id} has vector but no metadata - this should not happen in v4.0.0`);
576
577
  return null;
577
578
  }
578
579
  // Combine into HNSWVerbWithMetadata - v4.8.0: Extract standard fields to top-level
@@ -650,7 +651,7 @@ export class BaseStorage extends BaseStorageAdapter {
650
651
  };
651
652
  }
652
653
  catch (error) {
653
- console.error(`Failed to convert HNSWVerb to GraphVerb for ${hnswVerb.id}:`, error);
654
+ prodLog.error(`Failed to convert HNSWVerb to GraphVerb for ${hnswVerb.id}:`, error);
654
655
  return null;
655
656
  }
656
657
  }
@@ -778,7 +779,7 @@ export class BaseStorage extends BaseStorageAdapter {
778
779
  }
779
780
  catch (countError) {
780
781
  // Ignore errors from count method, it's optional
781
- console.warn('Error getting noun count:', countError);
782
+ prodLog.warn('Error getting noun count:', countError);
782
783
  }
783
784
  // Check if the adapter has a paginated method for getting nouns
784
785
  if (typeof this.getNounsWithPagination === 'function') {
@@ -799,7 +800,7 @@ export class BaseStorage extends BaseStorageAdapter {
799
800
  // If adapter forgets to return totalCount, log warning and use pre-calculated count
800
801
  let finalTotalCount = result.totalCount || totalCount;
801
802
  if (result.totalCount === undefined && this.totalNounCount > 0) {
802
- console.warn(`⚠️ Storage adapter missing totalCount in getNounsWithPagination result! ` +
803
+ prodLog.warn(`⚠️ Storage adapter missing totalCount in getNounsWithPagination result! ` +
803
804
  `Using pre-calculated count (${this.totalNounCount}) as fallback. ` +
804
805
  `Please ensure your storage adapter returns totalCount: this.totalNounCount`);
805
806
  finalTotalCount = this.totalNounCount;
@@ -812,7 +813,7 @@ export class BaseStorage extends BaseStorageAdapter {
812
813
  };
813
814
  }
814
815
  // Storage adapter does not support pagination
815
- console.error('Storage adapter does not support pagination. The deprecated getAllNouns_internal() method has been removed. Please implement getNounsWithPagination() in your storage adapter.');
816
+ prodLog.error('Storage adapter does not support pagination. The deprecated getAllNouns_internal() method has been removed. Please implement getNounsWithPagination() in your storage adapter.');
816
817
  return {
817
818
  items: [],
818
819
  totalCount: 0,
@@ -820,7 +821,7 @@ export class BaseStorage extends BaseStorageAdapter {
820
821
  };
821
822
  }
822
823
  catch (error) {
823
- console.error('Error getting nouns with pagination:', error);
824
+ prodLog.error('Error getting nouns with pagination:', error);
824
825
  return {
825
826
  items: [],
826
827
  totalCount: 0,
@@ -1158,7 +1159,7 @@ export class BaseStorage extends BaseStorageAdapter {
1158
1159
  }
1159
1160
  catch (countError) {
1160
1161
  // Ignore errors from count method, it's optional
1161
- console.warn('Error getting verb count:', countError);
1162
+ prodLog.warn('Error getting verb count:', countError);
1162
1163
  }
1163
1164
  // Check if the adapter has a paginated method for getting verbs
1164
1165
  if (typeof this.getVerbsWithPagination === 'function') {
@@ -1180,7 +1181,7 @@ export class BaseStorage extends BaseStorageAdapter {
1180
1181
  // If adapter forgets to return totalCount, log warning and use pre-calculated count
1181
1182
  let finalTotalCount = result.totalCount || totalCount;
1182
1183
  if (result.totalCount === undefined && this.totalVerbCount > 0) {
1183
- console.warn(`⚠️ Storage adapter missing totalCount in getVerbsWithPagination result! ` +
1184
+ prodLog.warn(`⚠️ Storage adapter missing totalCount in getVerbsWithPagination result! ` +
1184
1185
  `Using pre-calculated count (${this.totalVerbCount}) as fallback. ` +
1185
1186
  `Please ensure your storage adapter returns totalCount: this.totalVerbCount`);
1186
1187
  finalTotalCount = this.totalVerbCount;
@@ -1194,7 +1195,7 @@ export class BaseStorage extends BaseStorageAdapter {
1194
1195
  }
1195
1196
  // UNIVERSAL FALLBACK: Iterate through verb types with early termination (billion-scale safe)
1196
1197
  // This approach works for ALL storage adapters without requiring adapter-specific pagination
1197
- console.warn('Using universal type-iteration strategy for getVerbs(). ' +
1198
+ prodLog.warn('Using universal type-iteration strategy for getVerbs(). ' +
1198
1199
  'This works for all adapters but may be slower than native pagination. ' +
1199
1200
  'For optimal performance at scale, storage adapters can implement getVerbsWithPagination().');
1200
1201
  const collectedVerbs = [];
@@ -1273,7 +1274,7 @@ export class BaseStorage extends BaseStorageAdapter {
1273
1274
  };
1274
1275
  }
1275
1276
  catch (error) {
1276
- console.error('Error getting verbs with pagination:', error);
1277
+ prodLog.error('Error getting verbs with pagination:', error);
1277
1278
  return {
1278
1279
  items: [],
1279
1280
  totalCount: 0,
@@ -1294,22 +1295,45 @@ export class BaseStorage extends BaseStorageAdapter {
1294
1295
  }
1295
1296
  catch (error) {
1296
1297
  // Ignore if metadata file doesn't exist
1297
- console.debug(`No metadata file to delete for verb ${id}`);
1298
+ prodLog.debug(`No metadata file to delete for verb ${id}`);
1298
1299
  }
1299
1300
  }
1300
1301
  /**
1301
- * Get graph index (lazy initialization)
1302
+ * Get graph index (lazy initialization with concurrent access protection)
1303
+ * v5.7.1: Fixed race condition where concurrent calls could trigger multiple rebuilds
1302
1304
  */
1303
1305
  async getGraphIndex() {
1304
- if (!this.graphIndex) {
1305
- console.log('Initializing GraphAdjacencyIndex...');
1306
- this.graphIndex = new GraphAdjacencyIndex(this);
1307
- // Check if we need to rebuild from existing data
1308
- const sampleVerbs = await this.getVerbs({ pagination: { limit: 1 } });
1309
- if (sampleVerbs.items.length > 0) {
1310
- console.log('Found existing verbs, rebuilding graph index...');
1311
- await this.graphIndex.rebuild();
1312
- }
1306
+ // If already initialized, return immediately
1307
+ if (this.graphIndex) {
1308
+ return this.graphIndex;
1309
+ }
1310
+ // If initialization in progress, wait for it
1311
+ if (this.graphIndexPromise) {
1312
+ return this.graphIndexPromise;
1313
+ }
1314
+ // Start initialization (only first caller reaches here)
1315
+ this.graphIndexPromise = this._initializeGraphIndex();
1316
+ try {
1317
+ const index = await this.graphIndexPromise;
1318
+ return index;
1319
+ }
1320
+ finally {
1321
+ // Clear promise after completion (success or failure)
1322
+ this.graphIndexPromise = undefined;
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Internal method to initialize graph index (called once by getGraphIndex)
1327
+ * @private
1328
+ */
1329
+ async _initializeGraphIndex() {
1330
+ prodLog.info('Initializing GraphAdjacencyIndex...');
1331
+ this.graphIndex = new GraphAdjacencyIndex(this);
1332
+ // Check if we need to rebuild from existing data
1333
+ const sampleVerbs = await this.getVerbs({ pagination: { limit: 1 } });
1334
+ if (sampleVerbs.items.length > 0) {
1335
+ prodLog.info('Found existing verbs, rebuilding graph index...');
1336
+ await this.graphIndex.rebuild();
1313
1337
  }
1314
1338
  return this.graphIndex;
1315
1339
  }
@@ -1592,7 +1616,7 @@ export class BaseStorage extends BaseStorageAdapter {
1592
1616
  * Ensures verbCountsByType is always accurate for reliable pagination
1593
1617
  */
1594
1618
  async rebuildTypeCounts() {
1595
- console.log('[BaseStorage] Rebuilding type counts from storage...');
1619
+ prodLog.info('[BaseStorage] Rebuilding type counts from storage...');
1596
1620
  // Rebuild verb counts by checking each type directory
1597
1621
  for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1598
1622
  const type = TypeUtils.getVerbFromIndex(i);
@@ -1623,7 +1647,7 @@ export class BaseStorage extends BaseStorageAdapter {
1623
1647
  await this.saveTypeStatistics();
1624
1648
  const totalVerbs = this.verbCountsByType.reduce((sum, count) => sum + count, 0);
1625
1649
  const totalNouns = this.nounCountsByType.reduce((sum, count) => sum + count, 0);
1626
- console.log(`[BaseStorage] Rebuilt counts: ${totalNouns} nouns, ${totalVerbs} verbs`);
1650
+ prodLog.info(`[BaseStorage] Rebuilt counts: ${totalNouns} nouns, ${totalVerbs} verbs`);
1627
1651
  }
1628
1652
  /**
1629
1653
  * Get noun type from cache or metadata
@@ -1637,7 +1661,7 @@ export class BaseStorage extends BaseStorageAdapter {
1637
1661
  }
1638
1662
  // Default to 'thing' if unknown
1639
1663
  // This should only happen if saveNoun_internal is called before saveNounMetadata
1640
- console.warn(`[BaseStorage] Unknown noun type for ${noun.id}, defaulting to 'thing'`);
1664
+ prodLog.warn(`[BaseStorage] Unknown noun type for ${noun.id}, defaulting to 'thing'`);
1641
1665
  return 'thing';
1642
1666
  }
1643
1667
  /**
@@ -1654,7 +1678,7 @@ export class BaseStorage extends BaseStorageAdapter {
1654
1678
  return verb.type;
1655
1679
  }
1656
1680
  // This should never happen with current data
1657
- console.warn(`[BaseStorage] Verb missing type field for ${verb.id}, defaulting to 'relatedTo'`);
1681
+ prodLog.warn(`[BaseStorage] Verb missing type field for ${verb.id}, defaulting to 'relatedTo'`);
1658
1682
  return 'relatedTo';
1659
1683
  }
1660
1684
  // ============================================================================
@@ -1729,7 +1753,7 @@ export class BaseStorage extends BaseStorageAdapter {
1729
1753
  }
1730
1754
  }
1731
1755
  catch (error) {
1732
- console.warn(`[BaseStorage] Failed to load noun from ${path}:`, error);
1756
+ prodLog.warn(`[BaseStorage] Failed to load noun from ${path}:`, error);
1733
1757
  }
1734
1758
  }
1735
1759
  return nouns;
@@ -1784,6 +1808,25 @@ export class BaseStorage extends BaseStorageAdapter {
1784
1808
  this.verbTypeCache.set(verb.id, type);
1785
1809
  // COW-aware write (v5.0.1): Use COW helper for branch isolation
1786
1810
  await this.writeObjectToBranch(path, verb);
1811
+ // v5.7.0: Update GraphAdjacencyIndex incrementally for billion-scale optimization
1812
+ // CRITICAL: Only update if index already initialized to avoid circular dependency
1813
+ // Index is lazy-loaded on first query, then maintained incrementally
1814
+ if (this.graphIndex && this.graphIndex.isInitialized) {
1815
+ // Fast incremental update - no rebuild needed
1816
+ await this.graphIndex.addVerb({
1817
+ id: verb.id,
1818
+ sourceId: verb.sourceId,
1819
+ targetId: verb.targetId,
1820
+ vector: verb.vector,
1821
+ source: verb.sourceId,
1822
+ target: verb.targetId,
1823
+ verb: verb.verb,
1824
+ type: verb.verb,
1825
+ createdAt: { seconds: Math.floor(Date.now() / 1000), nanoseconds: 0 },
1826
+ updatedAt: { seconds: Math.floor(Date.now() / 1000), nanoseconds: 0 },
1827
+ createdBy: { augmentation: 'storage', version: '5.7.0' }
1828
+ });
1829
+ }
1787
1830
  // Periodically save statistics
1788
1831
  if (this.verbCountsByType[typeIndex] % 100 === 0) {
1789
1832
  await this.saveTypeStatistics();
@@ -1825,109 +1868,71 @@ export class BaseStorage extends BaseStorageAdapter {
1825
1868
  * v5.4.0: Fixed to directly list verb files instead of directories
1826
1869
  */
1827
1870
  async getVerbsBySource_internal(sourceId) {
1828
- // v5.4.0: Type-first implementation - scan across all verb types
1829
- // COW-aware: uses readWithInheritance for each verb
1871
+ // v5.7.0: BILLION-SCALE OPTIMIZATION - Use GraphAdjacencyIndex for O(log n) lookup
1872
+ // Previous: O(total_verbs) - scanned all 127 verb types
1873
+ // Now: O(log n) LSM-tree lookup + O(verbs_for_source) load
1830
1874
  await this.ensureInitialized();
1875
+ const startTime = performance.now();
1876
+ // Get GraphAdjacencyIndex (lazy-initialized)
1877
+ const graphIndex = await this.getGraphIndex();
1878
+ // O(log n) lookup with bloom filter optimization
1879
+ const verbIds = await graphIndex.getVerbIdsBySource(sourceId);
1880
+ // Load each verb by ID (uses existing optimized getVerb())
1831
1881
  const results = [];
1832
- // Iterate through all verb types
1833
- for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1834
- const type = TypeUtils.getVerbFromIndex(i);
1835
- const typeDir = `entities/verbs/${type}/vectors`;
1882
+ for (const verbId of verbIds) {
1836
1883
  try {
1837
- // v5.4.0 FIX: List all verb files directly (not shard directories)
1838
- // listObjectsInBranch returns full paths to .json files, not directories
1839
- const verbFiles = await this.listObjectsInBranch(typeDir);
1840
- for (const verbPath of verbFiles) {
1841
- // Skip if not a .json file
1842
- if (!verbPath.endsWith('.json'))
1843
- continue;
1844
- try {
1845
- const verb = await this.readWithInheritance(verbPath);
1846
- if (verb && verb.sourceId === sourceId) {
1847
- // v5.4.0: Use proper path helper instead of string replacement
1848
- const metadataPath = getVerbMetadataPath(type, verb.id);
1849
- const metadata = await this.readWithInheritance(metadataPath);
1850
- // v5.4.0: Extract standard fields from metadata to top-level (like nouns)
1851
- results.push({
1852
- ...verb,
1853
- weight: metadata?.weight,
1854
- confidence: metadata?.confidence,
1855
- createdAt: metadata?.createdAt
1856
- ? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
1857
- : Date.now(),
1858
- updatedAt: metadata?.updatedAt
1859
- ? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
1860
- : Date.now(),
1861
- service: metadata?.service,
1862
- createdBy: metadata?.createdBy,
1863
- metadata: metadata || {}
1864
- });
1865
- }
1866
- }
1867
- catch (error) {
1868
- // Skip verbs that fail to load
1869
- }
1884
+ const verb = await this.getVerb(verbId);
1885
+ if (verb) {
1886
+ results.push(verb);
1870
1887
  }
1871
1888
  }
1872
1889
  catch (error) {
1873
- // Skip types that have no data
1890
+ // Skip verbs that fail to load (handles deleted/corrupted verbs gracefully)
1874
1891
  }
1875
1892
  }
1893
+ const elapsed = performance.now() - startTime;
1894
+ // Performance monitoring - should be 100-10,000x faster than old O(n) scan
1895
+ if (elapsed > 50.0) {
1896
+ prodLog.warn(`getVerbsBySource_internal: Slow query for ${sourceId} ` +
1897
+ `(${verbIds.length} verbs, ${elapsed.toFixed(2)}ms). ` +
1898
+ `Expected <50ms with index optimization.`);
1899
+ }
1876
1900
  return results;
1877
1901
  }
1878
1902
  /**
1879
1903
  * Get verbs by target (COW-aware implementation)
1880
- * v5.4.0: Fixed to directly list verb files instead of directories
1904
+ * v5.7.0: BILLION-SCALE OPTIMIZATION - Use GraphAdjacencyIndex for O(log n) lookup
1881
1905
  */
1882
1906
  async getVerbsByTarget_internal(targetId) {
1883
- // v5.4.0: Type-first implementation - scan across all verb types
1884
- // COW-aware: uses readWithInheritance for each verb
1907
+ // v5.7.0: BILLION-SCALE OPTIMIZATION - Use GraphAdjacencyIndex for O(log n) lookup
1908
+ // Previous: O(total_verbs) - scanned all 127 verb types
1909
+ // Now: O(log n) LSM-tree lookup + O(verbs_for_target) load
1885
1910
  await this.ensureInitialized();
1911
+ const startTime = performance.now();
1912
+ // Get GraphAdjacencyIndex (lazy-initialized)
1913
+ const graphIndex = await this.getGraphIndex();
1914
+ // O(log n) lookup with bloom filter optimization
1915
+ const verbIds = await graphIndex.getVerbIdsByTarget(targetId);
1916
+ // Load each verb by ID (uses existing optimized getVerb())
1886
1917
  const results = [];
1887
- // Iterate through all verb types
1888
- for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1889
- const type = TypeUtils.getVerbFromIndex(i);
1890
- const typeDir = `entities/verbs/${type}/vectors`;
1918
+ for (const verbId of verbIds) {
1891
1919
  try {
1892
- // v5.4.0 FIX: List all verb files directly (not shard directories)
1893
- // listObjectsInBranch returns full paths to .json files, not directories
1894
- const verbFiles = await this.listObjectsInBranch(typeDir);
1895
- for (const verbPath of verbFiles) {
1896
- // Skip if not a .json file
1897
- if (!verbPath.endsWith('.json'))
1898
- continue;
1899
- try {
1900
- const verb = await this.readWithInheritance(verbPath);
1901
- if (verb && verb.targetId === targetId) {
1902
- // v5.4.0: Use proper path helper instead of string replacement
1903
- const metadataPath = getVerbMetadataPath(type, verb.id);
1904
- const metadata = await this.readWithInheritance(metadataPath);
1905
- // v5.4.0: Extract standard fields from metadata to top-level (like nouns)
1906
- results.push({
1907
- ...verb,
1908
- weight: metadata?.weight,
1909
- confidence: metadata?.confidence,
1910
- createdAt: metadata?.createdAt
1911
- ? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
1912
- : Date.now(),
1913
- updatedAt: metadata?.updatedAt
1914
- ? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
1915
- : Date.now(),
1916
- service: metadata?.service,
1917
- createdBy: metadata?.createdBy,
1918
- metadata: metadata || {}
1919
- });
1920
- }
1921
- }
1922
- catch (error) {
1923
- // Skip verbs that fail to load
1924
- }
1920
+ const verb = await this.getVerb(verbId);
1921
+ if (verb) {
1922
+ results.push(verb);
1925
1923
  }
1926
1924
  }
1927
1925
  catch (error) {
1928
- // Skip types that have no data
1926
+ // Skip verbs that fail to load (handles deleted/corrupted verbs gracefully)
1929
1927
  }
1930
1928
  }
1929
+ const elapsed = performance.now() - startTime;
1930
+ // Performance monitoring - should be 100-10,000x faster than old O(n) scan
1931
+ if (elapsed > 50.0) {
1932
+ prodLog.warn(`getVerbsByTarget_internal: Slow query for ${targetId} ` +
1933
+ `(${verbIds.length} verbs, ${elapsed.toFixed(2)}ms). ` +
1934
+ `Expected <50ms with index optimization.`);
1935
+ }
1931
1936
  return results;
1932
1937
  }
1933
1938
  /**
@@ -1980,7 +1985,7 @@ export class BaseStorage extends BaseStorageAdapter {
1980
1985
  verbs.push(verbWithMetadata);
1981
1986
  }
1982
1987
  catch (error) {
1983
- console.warn(`[BaseStorage] Failed to load verb from ${path}:`, error);
1988
+ prodLog.warn(`[BaseStorage] Failed to load verb from ${path}:`, error);
1984
1989
  }
1985
1990
  }
1986
1991
  return verbs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "5.6.2",
3
+ "version": "5.7.0",
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",