@soulcraft/brainy 5.7.9 → 5.7.11

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,13 @@
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.11](https://github.com/soulcraftlabs/brainy/compare/v5.7.10...v5.7.11) (2025-11-13)
6
+
7
+
8
+ ### 🐛 Bug Fixes
9
+
10
+ * resolve critical 378x pagination infinite loop bug (v5.7.11) ([e86f765](https://github.com/soulcraftlabs/brainy/commit/e86f765f3d30be41707e2ef7d07bb5c92d4ca3da))
11
+
5
12
  ### [5.7.9](https://github.com/soulcraftlabs/brainy/compare/v5.7.8...v5.7.9) (2025-11-13)
6
13
 
7
14
  - fix: implement exists: false and missing operators in MetadataIndexManager (b0f72ef)
@@ -858,12 +858,12 @@ export class HNSWIndex {
858
858
  // Cloud storage: Use pagination with native cloud APIs
859
859
  prodLog.info(`HNSW: Using cloud pagination strategy (${storageType})`);
860
860
  let hasMore = true;
861
- let cursor = undefined;
861
+ let offset = 0; // v5.7.11: Use offset-based pagination instead of cursor (bug fix for infinite loop)
862
862
  while (hasMore) {
863
863
  // Fetch batch of nouns from storage (cast needed as method is not in base interface)
864
864
  const result = await this.storage.getNounsWithPagination({
865
865
  limit: batchSize,
866
- cursor
866
+ offset // v5.7.11: Pass offset for proper pagination (previously passed cursor which was ignored)
867
867
  });
868
868
  // Set total count on first batch
869
869
  if (totalCount === undefined && result.totalCount !== undefined) {
@@ -912,7 +912,7 @@ export class HNSWIndex {
912
912
  }
913
913
  // Check for more data
914
914
  hasMore = result.hasMore;
915
- cursor = result.nextCursor;
915
+ offset += batchSize; // v5.7.11: Increment offset for next page
916
916
  }
917
917
  }
918
918
  const cacheInfo = shouldPreload
@@ -342,14 +342,14 @@ export class TypeAwareHNSWIndex {
342
342
  }
343
343
  // Load ALL nouns ONCE and route to correct type indexes
344
344
  // This is O(N) instead of O(42*N) from the previous parallel approach
345
- let cursor = undefined;
345
+ let offset = 0; // v5.7.11: Use offset-based pagination instead of cursor (bug fix for infinite loop)
346
346
  let hasMore = true;
347
347
  let totalLoaded = 0;
348
348
  const loadedByType = new Map();
349
349
  while (hasMore) {
350
350
  const result = await this.storage.getNounsWithPagination({
351
351
  limit: batchSize,
352
- cursor
352
+ offset // v5.7.11: Pass offset for proper pagination (previously passed cursor which was ignored)
353
353
  });
354
354
  // Route each noun to its type index
355
355
  for (const nounData of result.items) {
@@ -407,7 +407,7 @@ export class TypeAwareHNSWIndex {
407
407
  }
408
408
  }
409
409
  hasMore = result.hasMore;
410
- cursor = result.nextCursor;
410
+ offset += batchSize; // v5.7.11: Increment offset for next page
411
411
  // Progress logging
412
412
  if (totalLoaded % 1000 === 0) {
413
413
  prodLog.info(`Progress: ${totalLoaded.toLocaleString()} entities loaded...`);
@@ -48,7 +48,7 @@ export class OptimizedS3Search {
48
48
  }
49
49
  }
50
50
  // Determine if there are more items
51
- const hasMore = listResult.hasMore || nouns.length >= limit;
51
+ const hasMore = listResult.hasMore || nouns.length > limit; // v5.7.11: Fixed >= to > (was causing infinite loop)
52
52
  // Set next cursor
53
53
  let nextCursor;
54
54
  if (hasMore && nouns.length > 0) {
@@ -114,7 +114,7 @@ export class OptimizedS3Search {
114
114
  }
115
115
  }
116
116
  // Determine if there are more items
117
- const hasMore = listResult.hasMore || verbs.length >= limit;
117
+ const hasMore = listResult.hasMore || verbs.length > limit; // v5.7.11: Fixed >= to > (was causing infinite loop)
118
118
  // Set next cursor
119
119
  let nextCursor;
120
120
  if (hasMore && verbs.length > 0) {
@@ -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
  */
@@ -879,7 +879,7 @@ export class BaseStorage extends BaseStorageAdapter {
879
879
  */
880
880
  async getNounsWithPagination(options) {
881
881
  await this.ensureInitialized();
882
- const { limit, offset = 0, filter } = options;
882
+ const { limit, offset = 0, filter } = options; // cursor intentionally not extracted (not yet implemented)
883
883
  const collectedNouns = [];
884
884
  const targetCount = offset + limit; // Early termination target
885
885
  // v5.5.0 BUG FIX: Only use optimization if counts are reliable
@@ -912,8 +912,11 @@ export class BaseStorage extends BaseStorageAdapter {
912
912
  if (!nounPath.endsWith('.json'))
913
913
  continue;
914
914
  try {
915
- const noun = await this.readWithInheritance(nounPath);
916
- if (noun) {
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
- connections, // Use reconstructed Map instead of plain object
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,
@@ -971,7 +960,7 @@ export class BaseStorage extends BaseStorageAdapter {
971
960
  }
972
961
  // Apply pagination (v5.5.0: Efficient slicing after early termination)
973
962
  const paginatedNouns = collectedNouns.slice(offset, offset + limit);
974
- const hasMore = collectedNouns.length >= targetCount;
963
+ const hasMore = collectedNouns.length > targetCount; // v5.7.11: Fixed >= to > (was causing infinite loop)
975
964
  return {
976
965
  items: paginatedNouns,
977
966
  totalCount: collectedNouns.length, // Accurate count of collected results
@@ -998,7 +987,7 @@ export class BaseStorage extends BaseStorageAdapter {
998
987
  */
999
988
  async getVerbsWithPagination(options) {
1000
989
  await this.ensureInitialized();
1001
- const { limit, offset = 0, filter } = options;
990
+ const { limit, offset = 0, filter } = options; // cursor intentionally not extracted (not yet implemented)
1002
991
  const collectedVerbs = [];
1003
992
  const targetCount = offset + limit; // Early termination target
1004
993
  // v5.5.0 BUG FIX: Only use optimization if counts are reliable
@@ -1057,7 +1046,7 @@ export class BaseStorage extends BaseStorageAdapter {
1057
1046
  }
1058
1047
  // Apply pagination (v5.5.0: Efficient slicing after early termination)
1059
1048
  const paginatedVerbs = collectedVerbs.slice(offset, offset + limit);
1060
- const hasMore = collectedVerbs.length >= targetCount;
1049
+ const hasMore = collectedVerbs.length > targetCount; // v5.7.11: Fixed >= to > (was causing infinite loop)
1061
1050
  return {
1062
1051
  items: paginatedVerbs,
1063
1052
  totalCount: collectedVerbs.length, // Accurate count of collected results
@@ -1312,7 +1301,7 @@ export class BaseStorage extends BaseStorageAdapter {
1312
1301
  }
1313
1302
  // Apply pagination (slice for offset)
1314
1303
  const paginatedVerbs = collectedVerbs.slice(offset, offset + limit);
1315
- const hasMore = collectedVerbs.length >= targetCount;
1304
+ const hasMore = collectedVerbs.length > targetCount; // v5.7.11: Fixed >= to > (was causing infinite loop)
1316
1305
  return {
1317
1306
  items: paginatedVerbs,
1318
1307
  totalCount: collectedVerbs.length, // Accurate count of filtered results
@@ -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
- return await this.readWithInheritance(path);
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
- return noun;
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
- nouns.push(noun);
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
- return verb;
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
- return verb;
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 hnswVerb = await this.readWithInheritance(path);
2044
- if (!hnswVerb)
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: connectionsMap,
2113
+ connections: hnswVerb.connections, // v5.7.10: Already deserialized
2066
2114
  verb: hnswVerb.verb,
2067
2115
  sourceId: hnswVerb.sourceId,
2068
2116
  targetId: hnswVerb.targetId,
@@ -39,9 +39,12 @@ export async function rebuildCounts(storage) {
39
39
  throw new Error('Storage adapter does not support getNounsWithPagination');
40
40
  }
41
41
  let hasMore = true;
42
- let cursor;
42
+ let offset = 0; // v5.7.11: Use offset-based pagination instead of cursor (bug fix for infinite loop)
43
43
  while (hasMore) {
44
- const result = await storageWithPagination.getNounsWithPagination({ limit: 100, cursor });
44
+ const result = await storageWithPagination.getNounsWithPagination({
45
+ limit: 100,
46
+ offset // v5.7.11: Pass offset for proper pagination (previously passed cursor which was ignored)
47
+ });
45
48
  for (const noun of result.items) {
46
49
  const metadata = await storage.getNounMetadata(noun.id);
47
50
  if (metadata?.noun) {
@@ -51,7 +54,7 @@ export async function rebuildCounts(storage) {
51
54
  }
52
55
  }
53
56
  hasMore = result.hasMore;
54
- cursor = result.nextCursor;
57
+ offset += 100; // v5.7.11: Increment offset for next page
55
58
  }
56
59
  console.log(` Found ${totalNouns} entities across ${entityCounts.size} types`);
57
60
  // Scan all verbs using pagination
@@ -60,9 +63,12 @@ export async function rebuildCounts(storage) {
60
63
  throw new Error('Storage adapter does not support getVerbsWithPagination');
61
64
  }
62
65
  hasMore = true;
63
- cursor = undefined;
66
+ offset = 0; // v5.7.11: Reset offset for verbs pagination
64
67
  while (hasMore) {
65
- const result = await storageWithPagination.getVerbsWithPagination({ limit: 100, cursor });
68
+ const result = await storageWithPagination.getVerbsWithPagination({
69
+ limit: 100,
70
+ offset // v5.7.11: Pass offset for proper pagination (previously passed cursor which was ignored)
71
+ });
66
72
  for (const verb of result.items) {
67
73
  if (verb.verb) {
68
74
  const verbType = verb.verb;
@@ -71,7 +77,7 @@ export async function rebuildCounts(storage) {
71
77
  }
72
78
  }
73
79
  hasMore = result.hasMore;
74
- cursor = result.nextCursor;
80
+ offset += 100; // v5.7.11: Increment offset for next page
75
81
  }
76
82
  console.log(` Found ${totalVerbs} relationships across ${verbCounts.size} types`);
77
83
  // Update storage adapter's in-memory counts FIRST
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "5.7.9",
3
+ "version": "5.7.11",
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",