@soulcraft/brainy 4.1.1 β†’ 4.1.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,46 @@
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
+ ### [4.1.3](https://github.com/soulcraftlabs/brainy/compare/v4.1.2...v4.1.3) (2025-10-21)
6
+
7
+ - perf: make getRelations() pagination consistent and efficient (54d819c)
8
+ - fix: resolve getRelations() empty array bug and add string ID shorthand (8d217f3)
9
+
10
+
11
+ ### [4.1.3](https://github.com/soulcraftlabs/brainy/compare/v4.1.2...v4.1.3) (2025-10-21)
12
+
13
+
14
+ ### πŸ› Bug Fixes
15
+
16
+ * **api**: fix getRelations() returning empty array when called without parameters
17
+ - Fixed critical bug where `brain.getRelations()` returned `[]` instead of all relationships
18
+ - Added support for retrieving all relationships with pagination (default limit: 100)
19
+ - Added string ID shorthand syntax: `brain.getRelations(entityId)` as alias for `brain.getRelations({ from: entityId })`
20
+ - **Performance**: Made pagination consistent - now ALL query patterns paginate at storage layer
21
+ - **Efficiency**: `getRelations({ from: id, limit: 10 })` now fetches only 10 instead of fetching ALL then slicing
22
+ - Fixed storage.getVerbs() offset handling - now properly converts offset to cursor for adapters
23
+ - Production safety: Warns when fetching >10k relationships without filters
24
+ - Fixed broken method calls in improvedNeuralAPI.ts (replaced non-existent `getVerbsForNoun` with `getRelations`)
25
+ - Fixed property access bugs: `verb.target` β†’ `verb.to`, `verb.verb` β†’ `verb.type`
26
+ - Added comprehensive integration tests (14 tests covering all query patterns)
27
+ - Updated JSDoc documentation with usage examples
28
+ - **Impact**: Resolves Workshop team bug where 524 imported relationships were inaccessible
29
+ - **Breaking**: None - fully backward compatible
30
+
31
+ ### [4.1.2](https://github.com/soulcraftlabs/brainy/compare/v4.1.1...v4.1.2) (2025-10-21)
32
+
33
+
34
+ ### πŸ› Bug Fixes
35
+
36
+ * **storage**: resolve count synchronization race condition across all storage adapters ([798a694](https://github.com/soulcraftlabs/brainy/commit/798a694))
37
+ - Fixed critical bug where entity and relationship counts were not tracked correctly during add(), relate(), and import()
38
+ - Root cause: Race condition where count increment tried to read metadata before it was saved
39
+ - Fixed in baseStorage for all storage adapters (FileSystem, GCS, R2, Azure, Memory, OPFS, S3, TypeAware)
40
+ - Added verb type to VerbMetadata for proper count tracking
41
+ - Refactored verb count methods to prevent mutex deadlocks
42
+ - Added rebuildCounts utility to repair corrupted counts from actual storage data
43
+ - Added comprehensive integration tests (11 tests covering all operations)
44
+
5
45
  ### [4.1.1](https://github.com/soulcraftlabs/brainy/compare/v4.1.0...v4.1.1) (2025-10-20)
6
46
 
7
47
 
package/dist/brainy.d.ts CHANGED
@@ -314,9 +314,38 @@ export declare class Brainy<T = any> implements BrainyInterface<T> {
314
314
  */
315
315
  unrelate(id: string): Promise<void>;
316
316
  /**
317
- * Get relationships
317
+ * Get relationships between entities
318
+ *
319
+ * Supports multiple query patterns:
320
+ * - No parameters: Returns all relationships (paginated, default limit: 100)
321
+ * - String ID: Returns relationships from that entity (shorthand for { from: id })
322
+ * - Parameters object: Fine-grained filtering and pagination
323
+ *
324
+ * @param paramsOrId - Optional string ID or parameters object
325
+ * @returns Promise resolving to array of relationships
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * // Get all relationships (first 100)
330
+ * const all = await brain.getRelations()
331
+ *
332
+ * // Get relationships from specific entity (shorthand syntax)
333
+ * const fromEntity = await brain.getRelations(entityId)
334
+ *
335
+ * // Get relationships with filters
336
+ * const filtered = await brain.getRelations({
337
+ * type: VerbType.FriendOf,
338
+ * limit: 50
339
+ * })
340
+ *
341
+ * // Pagination
342
+ * const page2 = await brain.getRelations({ offset: 100, limit: 100 })
343
+ * ```
344
+ *
345
+ * @since v4.1.3 - Fixed bug where calling without parameters returned empty array
346
+ * @since v4.1.3 - Added string ID shorthand syntax: getRelations(id)
318
347
  */
319
- getRelations(params?: GetRelationsParams): Promise<Relation<T>[]>;
348
+ getRelations(paramsOrId?: string | GetRelationsParams): Promise<Relation<T>[]>;
320
349
  /**
321
350
  * Unified find method - supports natural language and structured queries
322
351
  * Implements Triple Intelligence with parallel search optimization
package/dist/brainy.js CHANGED
@@ -648,7 +648,9 @@ export class Brainy {
648
648
  const relationVector = fromEntity.vector.map((v, i) => (v + toEntity.vector[i]) / 2);
649
649
  return this.augmentationRegistry.execute('relate', params, async () => {
650
650
  // v4.0.0: Prepare verb metadata
651
+ // CRITICAL (v4.1.2): Include verb type in metadata for count tracking
651
652
  const verbMetadata = {
653
+ verb: params.type, // Store verb type for count synchronization
652
654
  weight: params.weight ?? 1.0,
653
655
  ...(params.metadata || {}),
654
656
  createdAt: Date.now()
@@ -717,33 +719,75 @@ export class Brainy {
717
719
  });
718
720
  }
719
721
  /**
720
- * Get relationships
722
+ * Get relationships between entities
723
+ *
724
+ * Supports multiple query patterns:
725
+ * - No parameters: Returns all relationships (paginated, default limit: 100)
726
+ * - String ID: Returns relationships from that entity (shorthand for { from: id })
727
+ * - Parameters object: Fine-grained filtering and pagination
728
+ *
729
+ * @param paramsOrId - Optional string ID or parameters object
730
+ * @returns Promise resolving to array of relationships
731
+ *
732
+ * @example
733
+ * ```typescript
734
+ * // Get all relationships (first 100)
735
+ * const all = await brain.getRelations()
736
+ *
737
+ * // Get relationships from specific entity (shorthand syntax)
738
+ * const fromEntity = await brain.getRelations(entityId)
739
+ *
740
+ * // Get relationships with filters
741
+ * const filtered = await brain.getRelations({
742
+ * type: VerbType.FriendOf,
743
+ * limit: 50
744
+ * })
745
+ *
746
+ * // Pagination
747
+ * const page2 = await brain.getRelations({ offset: 100, limit: 100 })
748
+ * ```
749
+ *
750
+ * @since v4.1.3 - Fixed bug where calling without parameters returned empty array
751
+ * @since v4.1.3 - Added string ID shorthand syntax: getRelations(id)
721
752
  */
722
- async getRelations(params = {}) {
753
+ async getRelations(paramsOrId) {
723
754
  await this.ensureInitialized();
724
- const relations = [];
755
+ // Handle string ID shorthand: getRelations(id) -> getRelations({ from: id })
756
+ const params = typeof paramsOrId === 'string'
757
+ ? { from: paramsOrId }
758
+ : (paramsOrId || {});
759
+ const limit = params.limit || 100;
760
+ const offset = params.offset || 0;
761
+ // Production safety: warn for large unfiltered queries
762
+ if (!params.from && !params.to && !params.type && limit > 10000) {
763
+ console.warn(`[Brainy] getRelations(): Fetching ${limit} relationships without filters. ` +
764
+ `Consider adding 'from', 'to', or 'type' filter for better performance.`);
765
+ }
766
+ // Build filter for storage query
767
+ const filter = {};
725
768
  if (params.from) {
726
- const verbs = await this.storage.getVerbsBySource(params.from);
727
- relations.push(...this.verbsToRelations(verbs));
769
+ filter.sourceId = params.from;
728
770
  }
729
771
  if (params.to) {
730
- const verbs = await this.storage.getVerbsByTarget(params.to);
731
- relations.push(...this.verbsToRelations(verbs));
772
+ filter.targetId = params.to;
732
773
  }
733
- // Filter by type
734
- let filtered = relations;
735
774
  if (params.type) {
736
- const types = Array.isArray(params.type) ? params.type : [params.type];
737
- filtered = relations.filter((r) => types.includes(r.type));
775
+ filter.verbType = Array.isArray(params.type) ? params.type : [params.type];
738
776
  }
739
- // Filter by service
740
777
  if (params.service) {
741
- filtered = filtered.filter((r) => r.service === params.service);
742
- }
743
- // Apply pagination
744
- const limit = params.limit || 100;
745
- const offset = params.offset || 0;
746
- return filtered.slice(offset, offset + limit);
778
+ filter.service = params.service;
779
+ }
780
+ // Fetch from storage with pagination at storage layer (efficient!)
781
+ const result = await this.storage.getVerbs({
782
+ pagination: {
783
+ limit,
784
+ offset,
785
+ cursor: params.cursor
786
+ },
787
+ filter: Object.keys(filter).length > 0 ? filter : undefined
788
+ });
789
+ // Convert to Relation format
790
+ return this.verbsToRelations(result.items);
747
791
  }
748
792
  // ============= SEARCH & DISCOVERY =============
749
793
  /**
@@ -1310,14 +1310,14 @@ export class ImprovedNeuralAPI {
1310
1310
  for (const sourceId of itemIds) {
1311
1311
  const sourceVerbs = await this.brain.getRelations(sourceId);
1312
1312
  for (const verb of sourceVerbs) {
1313
- const targetId = verb.target;
1313
+ const targetId = verb.to;
1314
1314
  if (nodes.has(targetId) && sourceId !== targetId) {
1315
1315
  // Initialize edge map if needed
1316
1316
  if (!edges.has(sourceId)) {
1317
1317
  edges.set(sourceId, new Map());
1318
1318
  }
1319
1319
  // Calculate edge weight from verb type and metadata
1320
- const verbType = verb.verb;
1320
+ const verbType = verb.type;
1321
1321
  const baseWeight = relationshipWeights[verbType] || 0.5;
1322
1322
  const confidenceWeight = verb.confidence || 1.0;
1323
1323
  const weight = baseWeight * confidenceWeight;
@@ -2743,7 +2743,7 @@ export class ImprovedNeuralAPI {
2743
2743
  const sampleSize = Math.min(50, itemIds.length);
2744
2744
  for (let i = 0; i < sampleSize; i++) {
2745
2745
  try {
2746
- const verbs = await this.brain.getVerbsForNoun(itemIds[i]);
2746
+ const verbs = await this.brain.getRelations({ from: itemIds[i] });
2747
2747
  connectionCount += verbs.length;
2748
2748
  }
2749
2749
  catch (error) {
@@ -2797,9 +2797,9 @@ export class ImprovedNeuralAPI {
2797
2797
  if (fromType !== toType) {
2798
2798
  for (const fromItem of fromItems.slice(0, 10)) { // Sample to avoid N^2
2799
2799
  try {
2800
- const verbs = await this.brain.getVerbsForNoun(fromItem.id);
2800
+ const verbs = await this.brain.getRelations({ from: fromItem.id });
2801
2801
  for (const verb of verbs) {
2802
- const toItem = toItems.find(item => item.id === verb.target);
2802
+ const toItem = toItems.find(item => item.id === verb.to);
2803
2803
  if (toItem) {
2804
2804
  connections.push({
2805
2805
  from: fromItem.id,
@@ -791,11 +791,8 @@ export class AzureBlobStorage extends BaseStorage {
791
791
  });
792
792
  // Update cache
793
793
  this.verbCacheManager.set(edge.id, edge);
794
- // Increment verb count
795
- const metadata = await this.getVerbMetadata(edge.id);
796
- if (metadata && metadata.type) {
797
- await this.incrementVerbCount(metadata.type);
798
- }
794
+ // Count tracking happens in baseStorage.saveVerbMetadata_internal (v4.1.2)
795
+ // This fixes the race condition where metadata didn't exist yet
799
796
  this.logger.trace(`Edge ${edge.id} saved successfully`);
800
797
  this.releaseBackpressure(true, requestId);
801
798
  }
@@ -320,15 +320,27 @@ export declare abstract class BaseStorageAdapter implements StorageAdapter {
320
320
  */
321
321
  protected decrementEntityCountSafe(type: string): Promise<void>;
322
322
  /**
323
- * Increment verb count - O(1) operation with mutex protection
323
+ * Increment verb count - O(1) operation (v4.1.2: now synchronous)
324
+ * Protected by storage-specific mechanisms (mutex, distributed consensus, etc.)
325
+ * @param type The verb type
326
+ */
327
+ protected incrementVerbCount(type: string): void;
328
+ /**
329
+ * Thread-safe increment for verb counts (v4.1.2)
330
+ * Uses mutex for single-node, distributed consensus for multi-node
331
+ * @param type The verb type
332
+ */
333
+ protected incrementVerbCountSafe(type: string): Promise<void>;
334
+ /**
335
+ * Decrement verb count - O(1) operation (v4.1.2: now synchronous)
324
336
  * @param type The verb type
325
337
  */
326
- protected incrementVerbCount(type: string): Promise<void>;
338
+ protected decrementVerbCount(type: string): void;
327
339
  /**
328
- * Decrement verb count - O(1) operation with mutex protection
340
+ * Thread-safe decrement for verb counts (v4.1.2)
329
341
  * @param type The verb type
330
342
  */
331
- protected decrementVerbCount(type: string): Promise<void>;
343
+ protected decrementVerbCountSafe(type: string): Promise<void>;
332
344
  /**
333
345
  * Detect if this storage adapter uses cloud storage (network I/O)
334
346
  * Cloud storage benefits from batching; local storage does not.
@@ -709,45 +709,61 @@ export class BaseStorageAdapter {
709
709
  });
710
710
  }
711
711
  /**
712
- * Increment verb count - O(1) operation with mutex protection
712
+ * Increment verb count - O(1) operation (v4.1.2: now synchronous)
713
+ * Protected by storage-specific mechanisms (mutex, distributed consensus, etc.)
714
+ * @param type The verb type
715
+ */
716
+ incrementVerbCount(type) {
717
+ this.verbCounts.set(type, (this.verbCounts.get(type) || 0) + 1);
718
+ this.totalVerbCount++;
719
+ // Update cache
720
+ this.countCache.set('verbs_count', {
721
+ count: this.totalVerbCount,
722
+ timestamp: Date.now()
723
+ });
724
+ }
725
+ /**
726
+ * Thread-safe increment for verb counts (v4.1.2)
727
+ * Uses mutex for single-node, distributed consensus for multi-node
713
728
  * @param type The verb type
714
729
  */
715
- async incrementVerbCount(type) {
730
+ async incrementVerbCountSafe(type) {
716
731
  const mutex = getGlobalMutex();
717
732
  await mutex.runExclusive(`count-verb-${type}`, async () => {
718
- this.verbCounts.set(type, (this.verbCounts.get(type) || 0) + 1);
719
- this.totalVerbCount++;
720
- // Update cache
721
- this.countCache.set('verbs_count', {
722
- count: this.totalVerbCount,
723
- timestamp: Date.now()
724
- });
733
+ this.incrementVerbCount(type);
725
734
  // Smart batching (v3.32.3+): Adapts to storage type
726
735
  await this.scheduleCountPersist();
727
736
  });
728
737
  }
729
738
  /**
730
- * Decrement verb count - O(1) operation with mutex protection
739
+ * Decrement verb count - O(1) operation (v4.1.2: now synchronous)
740
+ * @param type The verb type
741
+ */
742
+ decrementVerbCount(type) {
743
+ const current = this.verbCounts.get(type) || 0;
744
+ if (current > 1) {
745
+ this.verbCounts.set(type, current - 1);
746
+ }
747
+ else {
748
+ this.verbCounts.delete(type);
749
+ }
750
+ if (this.totalVerbCount > 0) {
751
+ this.totalVerbCount--;
752
+ }
753
+ // Update cache
754
+ this.countCache.set('verbs_count', {
755
+ count: this.totalVerbCount,
756
+ timestamp: Date.now()
757
+ });
758
+ }
759
+ /**
760
+ * Thread-safe decrement for verb counts (v4.1.2)
731
761
  * @param type The verb type
732
762
  */
733
- async decrementVerbCount(type) {
763
+ async decrementVerbCountSafe(type) {
734
764
  const mutex = getGlobalMutex();
735
765
  await mutex.runExclusive(`count-verb-${type}`, async () => {
736
- const current = this.verbCounts.get(type) || 0;
737
- if (current > 1) {
738
- this.verbCounts.set(type, current - 1);
739
- }
740
- else {
741
- this.verbCounts.delete(type);
742
- }
743
- if (this.totalVerbCount > 0) {
744
- this.totalVerbCount--;
745
- }
746
- // Update cache
747
- this.countCache.set('verbs_count', {
748
- count: this.totalVerbCount,
749
- timestamp: Date.now()
750
- });
766
+ this.decrementVerbCount(type);
751
767
  // Smart batching (v3.32.3+): Adapts to storage type
752
768
  await this.scheduleCountPersist();
753
769
  });
@@ -179,8 +179,6 @@ export class FileSystemStorage extends BaseStorage {
179
179
  */
180
180
  async saveNode(node) {
181
181
  await this.ensureInitialized();
182
- // Check if this is a new node to update counts
183
- const isNew = !(await this.fileExists(this.getNodePath(node.id)));
184
182
  // Convert connections Map to a serializable format
185
183
  // CRITICAL: Only save lightweight vector data (no metadata)
186
184
  // Metadata is saved separately via saveNounMetadata() (2-file system)
@@ -194,17 +192,8 @@ export class FileSystemStorage extends BaseStorage {
194
192
  const filePath = this.getNodePath(node.id);
195
193
  await this.ensureDirectoryExists(path.dirname(filePath));
196
194
  await fs.promises.writeFile(filePath, JSON.stringify(serializableNode, null, 2));
197
- // Update counts for new nodes (v4.0.0: load metadata separately)
198
- if (isNew) {
199
- // v4.0.0: Get type from separate metadata storage
200
- const metadata = await this.getNounMetadata(node.id);
201
- const type = metadata?.noun || 'default';
202
- this.incrementEntityCount(type);
203
- // Persist counts periodically (every 10 operations for efficiency)
204
- if (this.totalNounCount % 10 === 0) {
205
- await this.persistCounts();
206
- }
207
- }
195
+ // Count tracking happens in baseStorage.saveNounMetadata_internal (v4.1.2)
196
+ // This fixes the race condition where metadata didn't exist yet
208
197
  }
209
198
  /**
210
199
  * Get a node from storage
@@ -354,8 +343,6 @@ export class FileSystemStorage extends BaseStorage {
354
343
  */
355
344
  async saveEdge(edge) {
356
345
  await this.ensureInitialized();
357
- // Check if this is a new edge to update counts
358
- const isNew = !(await this.fileExists(this.getVerbPath(edge.id)));
359
346
  // Convert connections Map to a serializable format
360
347
  // ARCHITECTURAL FIX (v3.50.1): Include core relational fields in verb vector file
361
348
  // These fields are essential for 90% of operations - no metadata lookup needed
@@ -373,14 +360,8 @@ export class FileSystemStorage extends BaseStorage {
373
360
  const filePath = this.getVerbPath(edge.id);
374
361
  await this.ensureDirectoryExists(path.dirname(filePath));
375
362
  await fs.promises.writeFile(filePath, JSON.stringify(serializableEdge, null, 2));
376
- // Update verb count for new edges (production-scale optimizations)
377
- if (isNew) {
378
- this.totalVerbCount++;
379
- // Persist counts periodically (every 10 operations for efficiency)
380
- if (this.totalVerbCount % 10 === 0) {
381
- this.persistCounts(); // Async persist, don't await
382
- }
383
- }
363
+ // Count tracking happens in baseStorage.saveVerbMetadata_internal (v4.1.2)
364
+ // This fixes the race condition where metadata didn't exist yet
384
365
  }
385
366
  /**
386
367
  * Get an edge from storage
@@ -353,11 +353,8 @@ export class GcsStorage extends BaseStorage {
353
353
  this.nounCacheManager.set(node.id, node);
354
354
  }
355
355
  // Note: Empty vectors are intentional during HNSW lazy mode - not logged
356
- // Increment noun count
357
- const metadata = await this.getNounMetadata(node.id);
358
- if (metadata && metadata.type) {
359
- await this.incrementEntityCountSafe(metadata.type);
360
- }
356
+ // Count tracking happens in baseStorage.saveNounMetadata_internal (v4.1.2)
357
+ // This fixes the race condition where metadata didn't exist yet
361
358
  this.logger.trace(`Node ${node.id} saved successfully`);
362
359
  this.releaseBackpressure(true, requestId);
363
360
  }
@@ -663,11 +660,8 @@ export class GcsStorage extends BaseStorage {
663
660
  });
664
661
  // Update cache
665
662
  this.verbCacheManager.set(edge.id, edge);
666
- // Increment verb count
667
- const metadata = await this.getVerbMetadata(edge.id);
668
- if (metadata && metadata.type) {
669
- await this.incrementVerbCount(metadata.type);
670
- }
663
+ // Count tracking happens in baseStorage.saveVerbMetadata_internal (v4.1.2)
664
+ // This fixes the race condition where metadata didn't exist yet
671
665
  this.logger.trace(`Edge ${edge.id} saved successfully`);
672
666
  this.releaseBackpressure(true, requestId);
673
667
  }
@@ -56,7 +56,8 @@ export class MemoryStorage extends BaseStorage {
56
56
  }
57
57
  // Save the noun directly in the nouns map
58
58
  this.nouns.set(noun.id, nounCopy);
59
- // Note: Count tracking happens in saveNounMetadata since type info is in metadata now
59
+ // Count tracking happens in baseStorage.saveNounMetadata_internal (v4.1.2)
60
+ // This fixes the race condition where metadata didn't exist yet
60
61
  }
61
62
  /**
62
63
  * Get a noun from storage (v4.0.0: returns pure vector only)
@@ -574,10 +574,8 @@ export class R2Storage extends BaseStorage {
574
574
  ContentType: 'application/json'
575
575
  }));
576
576
  this.verbCacheManager.set(edge.id, edge);
577
- const metadata = await this.getVerbMetadata(edge.id);
578
- if (metadata && metadata.type) {
579
- await this.incrementVerbCount(metadata.type);
580
- }
577
+ // Count tracking happens in baseStorage.saveVerbMetadata_internal (v4.1.2)
578
+ // This fixes the race condition where metadata didn't exist yet
581
579
  this.releaseBackpressure(true, requestId);
582
580
  }
583
581
  catch (error) {
@@ -231,6 +231,11 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
231
231
  /**
232
232
  * Internal method for saving noun metadata (v4.0.0: now typed)
233
233
  * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
234
+ *
235
+ * CRITICAL (v4.1.2): Count synchronization happens here
236
+ * This ensures counts are updated AFTER metadata exists, fixing the race condition
237
+ * where storage adapters tried to read metadata before it was saved.
238
+ *
234
239
  * @protected
235
240
  */
236
241
  protected saveNounMetadata_internal(id: string, metadata: NounMetadata): Promise<void>;
@@ -252,6 +257,13 @@ export declare abstract class BaseStorage extends BaseStorageAdapter {
252
257
  /**
253
258
  * Internal method for saving verb metadata (v4.0.0: now typed)
254
259
  * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
260
+ *
261
+ * CRITICAL (v4.1.2): Count synchronization happens here
262
+ * This ensures verb counts are updated AFTER metadata exists, fixing the race condition
263
+ * where storage adapters tried to read metadata before it was saved.
264
+ *
265
+ * Note: Verb type is now stored in both HNSWVerb (vector file) and VerbMetadata for count tracking
266
+ *
255
267
  * @protected
256
268
  */
257
269
  protected saveVerbMetadata_internal(id: string, metadata: VerbMetadata): Promise<void>;
@@ -600,13 +600,15 @@ export class BaseStorage extends BaseStorageAdapter {
600
600
  // Check if the adapter has a paginated method for getting verbs
601
601
  if (typeof this.getVerbsWithPagination === 'function') {
602
602
  // Use the adapter's paginated method
603
+ // Convert offset to cursor if no cursor provided (adapters use cursor for offset)
604
+ const effectiveCursor = cursor || (offset > 0 ? offset.toString() : undefined);
603
605
  const result = await this.getVerbsWithPagination({
604
606
  limit,
605
- cursor,
607
+ cursor: effectiveCursor,
606
608
  filter: options?.filter
607
609
  });
608
- // Apply offset if needed (some adapters might not support offset)
609
- const items = result.items.slice(offset);
610
+ // Items are already offset by the adapter via cursor, no need to slice
611
+ const items = result.items;
610
612
  // CRITICAL SAFETY CHECK: Prevent infinite loops
611
613
  // If we have no items but hasMore is true, force hasMore to false
612
614
  // This prevents pagination bugs from causing infinite loops
@@ -706,12 +708,32 @@ export class BaseStorage extends BaseStorageAdapter {
706
708
  /**
707
709
  * Internal method for saving noun metadata (v4.0.0: now typed)
708
710
  * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
711
+ *
712
+ * CRITICAL (v4.1.2): Count synchronization happens here
713
+ * This ensures counts are updated AFTER metadata exists, fixing the race condition
714
+ * where storage adapters tried to read metadata before it was saved.
715
+ *
709
716
  * @protected
710
717
  */
711
718
  async saveNounMetadata_internal(id, metadata) {
712
719
  await this.ensureInitialized();
720
+ // Determine if this is a new entity by checking if metadata already exists
713
721
  const keyInfo = this.analyzeKey(id, 'noun-metadata');
714
- return this.writeObjectToPath(keyInfo.fullPath, metadata);
722
+ const existingMetadata = await this.readObjectFromPath(keyInfo.fullPath);
723
+ const isNew = !existingMetadata;
724
+ // Save the metadata
725
+ await this.writeObjectToPath(keyInfo.fullPath, metadata);
726
+ // CRITICAL FIX (v4.1.2): Increment count for new entities
727
+ // This runs AFTER metadata is saved, guaranteeing type information is available
728
+ // Uses synchronous increment since storage operations are already serialized
729
+ // Fixes Bug #1: Count synchronization failure during add() and import()
730
+ if (isNew && metadata.noun) {
731
+ this.incrementEntityCount(metadata.noun);
732
+ // Persist counts asynchronously (fire and forget)
733
+ this.scheduleCountPersist().catch(() => {
734
+ // Ignore persist errors - will retry on next operation
735
+ });
736
+ }
715
737
  }
716
738
  /**
717
739
  * Get noun metadata from storage (v4.0.0: now typed)
@@ -742,12 +764,35 @@ export class BaseStorage extends BaseStorageAdapter {
742
764
  /**
743
765
  * Internal method for saving verb metadata (v4.0.0: now typed)
744
766
  * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
767
+ *
768
+ * CRITICAL (v4.1.2): Count synchronization happens here
769
+ * This ensures verb counts are updated AFTER metadata exists, fixing the race condition
770
+ * where storage adapters tried to read metadata before it was saved.
771
+ *
772
+ * Note: Verb type is now stored in both HNSWVerb (vector file) and VerbMetadata for count tracking
773
+ *
745
774
  * @protected
746
775
  */
747
776
  async saveVerbMetadata_internal(id, metadata) {
748
777
  await this.ensureInitialized();
778
+ // Determine if this is a new verb by checking if metadata already exists
749
779
  const keyInfo = this.analyzeKey(id, 'verb-metadata');
750
- return this.writeObjectToPath(keyInfo.fullPath, metadata);
780
+ const existingMetadata = await this.readObjectFromPath(keyInfo.fullPath);
781
+ const isNew = !existingMetadata;
782
+ // Save the metadata
783
+ await this.writeObjectToPath(keyInfo.fullPath, metadata);
784
+ // CRITICAL FIX (v4.1.2): Increment verb count for new relationships
785
+ // This runs AFTER metadata is saved
786
+ // Verb type is now stored in metadata (as of v4.1.2) to avoid loading HNSWVerb
787
+ // Uses synchronous increment since storage operations are already serialized
788
+ // Fixes Bug #2: Count synchronization failure during relate() and import()
789
+ if (isNew && metadata.verb) {
790
+ this.incrementVerbCount(metadata.verb);
791
+ // Persist counts asynchronously (fire and forget)
792
+ this.scheduleCountPersist().catch(() => {
793
+ // Ignore persist errors - will retry on next operation
794
+ });
795
+ }
751
796
  }
752
797
  /**
753
798
  * Get verb metadata from storage (v4.0.0: now typed)
@@ -172,14 +172,83 @@ export interface SimilarParams<T = any> {
172
172
  }
173
173
  /**
174
174
  * Parameters for getting relationships
175
+ *
176
+ * All parameters are optional. When called without parameters, returns all relationships
177
+ * with pagination (default limit: 100).
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * // Get all relationships (default limit: 100)
182
+ * const all = await brain.getRelations()
183
+ *
184
+ * // Get relationships from a specific entity (string shorthand)
185
+ * const fromEntity = await brain.getRelations(entityId)
186
+ *
187
+ * // Equivalent to:
188
+ * const fromEntity2 = await brain.getRelations({ from: entityId })
189
+ *
190
+ * // Get relationships to a specific entity
191
+ * const toEntity = await brain.getRelations({ to: entityId })
192
+ *
193
+ * // Filter by relationship type
194
+ * const friends = await brain.getRelations({ type: VerbType.FriendOf })
195
+ *
196
+ * // Pagination
197
+ * const page2 = await brain.getRelations({ offset: 100, limit: 50 })
198
+ *
199
+ * // Combined filters
200
+ * const filtered = await brain.getRelations({
201
+ * from: entityId,
202
+ * type: VerbType.WorksWith,
203
+ * limit: 20
204
+ * })
205
+ * ```
206
+ *
207
+ * @since v4.1.3 - Fixed bug where calling without parameters returned empty array
208
+ * @since v4.1.3 - Added string ID shorthand syntax
175
209
  */
176
210
  export interface GetRelationsParams {
211
+ /**
212
+ * Filter by source entity ID
213
+ *
214
+ * Returns all relationships originating from this entity.
215
+ */
177
216
  from?: string;
217
+ /**
218
+ * Filter by target entity ID
219
+ *
220
+ * Returns all relationships pointing to this entity.
221
+ */
178
222
  to?: string;
223
+ /**
224
+ * Filter by relationship type(s)
225
+ *
226
+ * Can be a single VerbType or array of VerbTypes.
227
+ */
179
228
  type?: VerbType | VerbType[];
229
+ /**
230
+ * Maximum number of results to return
231
+ *
232
+ * @default 100
233
+ */
180
234
  limit?: number;
235
+ /**
236
+ * Number of results to skip (offset-based pagination)
237
+ *
238
+ * @default 0
239
+ */
181
240
  offset?: number;
241
+ /**
242
+ * Cursor for cursor-based pagination
243
+ *
244
+ * More efficient than offset for large result sets.
245
+ */
182
246
  cursor?: string;
247
+ /**
248
+ * Filter by service (multi-tenancy)
249
+ *
250
+ * Only return relationships belonging to this service.
251
+ */
183
252
  service?: string;
184
253
  }
185
254
  /**
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Rebuild Counts Utility
3
+ *
4
+ * Scans storage and rebuilds counts.json from actual data
5
+ * Use this to fix databases affected by the v4.1.1 count synchronization bug
6
+ *
7
+ * NO MOCKS - Production-ready implementation
8
+ */
9
+ import type { BaseStorage } from '../storage/baseStorage.js';
10
+ export interface RebuildCountsResult {
11
+ /** Total number of entities (nouns) found */
12
+ nounCount: number;
13
+ /** Total number of relationships (verbs) found */
14
+ verbCount: number;
15
+ /** Entity counts by type */
16
+ entityCounts: Map<string, number>;
17
+ /** Verb counts by type */
18
+ verbCounts: Map<string, number>;
19
+ /** Processing time in milliseconds */
20
+ duration: number;
21
+ }
22
+ /**
23
+ * Rebuild counts.json from actual storage data
24
+ *
25
+ * This scans all entities and relationships in storage and reconstructs
26
+ * the counts index from scratch. Use this to fix count desynchronization.
27
+ *
28
+ * @param storage - The storage adapter to rebuild counts for
29
+ * @returns Promise that resolves to rebuild statistics
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const brain = new Brainy({ storage: { type: 'filesystem', path: './brainy-data' } })
34
+ * await brain.init()
35
+ *
36
+ * const result = await rebuildCounts(brain.storage)
37
+ * console.log(`Rebuilt counts: ${result.nounCount} nouns, ${result.verbCount} verbs`)
38
+ * ```
39
+ */
40
+ export declare function rebuildCounts(storage: BaseStorage): Promise<RebuildCountsResult>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Rebuild Counts Utility
3
+ *
4
+ * Scans storage and rebuilds counts.json from actual data
5
+ * Use this to fix databases affected by the v4.1.1 count synchronization bug
6
+ *
7
+ * NO MOCKS - Production-ready implementation
8
+ */
9
+ /**
10
+ * Rebuild counts.json from actual storage data
11
+ *
12
+ * This scans all entities and relationships in storage and reconstructs
13
+ * the counts index from scratch. Use this to fix count desynchronization.
14
+ *
15
+ * @param storage - The storage adapter to rebuild counts for
16
+ * @returns Promise that resolves to rebuild statistics
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const brain = new Brainy({ storage: { type: 'filesystem', path: './brainy-data' } })
21
+ * await brain.init()
22
+ *
23
+ * const result = await rebuildCounts(brain.storage)
24
+ * console.log(`Rebuilt counts: ${result.nounCount} nouns, ${result.verbCount} verbs`)
25
+ * ```
26
+ */
27
+ export async function rebuildCounts(storage) {
28
+ const startTime = Date.now();
29
+ console.log('πŸ”§ Rebuilding counts from storage...');
30
+ const entityCounts = new Map();
31
+ const verbCounts = new Map();
32
+ let totalNouns = 0;
33
+ let totalVerbs = 0;
34
+ // Scan all nouns using pagination
35
+ console.log('πŸ“Š Scanning entities...');
36
+ // Check if pagination method exists
37
+ const storageWithPagination = storage;
38
+ if (typeof storageWithPagination.getNounsWithPagination !== 'function') {
39
+ throw new Error('Storage adapter does not support getNounsWithPagination');
40
+ }
41
+ let hasMore = true;
42
+ let cursor;
43
+ while (hasMore) {
44
+ const result = await storageWithPagination.getNounsWithPagination({ limit: 100, cursor });
45
+ for (const noun of result.items) {
46
+ const metadata = await storage.getNounMetadata(noun.id);
47
+ if (metadata?.noun) {
48
+ const entityType = metadata.noun;
49
+ entityCounts.set(entityType, (entityCounts.get(entityType) || 0) + 1);
50
+ totalNouns++;
51
+ }
52
+ }
53
+ hasMore = result.hasMore;
54
+ cursor = result.nextCursor;
55
+ }
56
+ console.log(` Found ${totalNouns} entities across ${entityCounts.size} types`);
57
+ // Scan all verbs using pagination
58
+ console.log('πŸ”— Scanning relationships...');
59
+ if (typeof storageWithPagination.getVerbsWithPagination !== 'function') {
60
+ throw new Error('Storage adapter does not support getVerbsWithPagination');
61
+ }
62
+ hasMore = true;
63
+ cursor = undefined;
64
+ while (hasMore) {
65
+ const result = await storageWithPagination.getVerbsWithPagination({ limit: 100, cursor });
66
+ for (const verb of result.items) {
67
+ if (verb.verb) {
68
+ const verbType = verb.verb;
69
+ verbCounts.set(verbType, (verbCounts.get(verbType) || 0) + 1);
70
+ totalVerbs++;
71
+ }
72
+ }
73
+ hasMore = result.hasMore;
74
+ cursor = result.nextCursor;
75
+ }
76
+ console.log(` Found ${totalVerbs} relationships across ${verbCounts.size} types`);
77
+ // Update storage adapter's in-memory counts FIRST
78
+ storageWithPagination.totalNounCount = totalNouns;
79
+ storageWithPagination.totalVerbCount = totalVerbs;
80
+ storageWithPagination.entityCounts = entityCounts;
81
+ storageWithPagination.verbCounts = verbCounts;
82
+ // Mark counts as pending persist (required for flushCounts to actually persist)
83
+ storageWithPagination.pendingCountPersist = true;
84
+ storageWithPagination.pendingCountOperations = 1;
85
+ // Persist counts using storage adapter's own persist method
86
+ // This ensures counts.json is written correctly (compressed or uncompressed)
87
+ await storageWithPagination.flushCounts();
88
+ const duration = Date.now() - startTime;
89
+ console.log(`βœ… Counts rebuilt successfully in ${duration}ms`);
90
+ console.log(` Entities: ${totalNouns}`);
91
+ console.log(` Relationships: ${totalVerbs}`);
92
+ console.log('');
93
+ console.log('Entity breakdown:');
94
+ entityCounts.forEach((count, entityType) => {
95
+ console.log(` ${entityType}: ${count}`);
96
+ });
97
+ if (verbCounts.size > 0) {
98
+ console.log('');
99
+ console.log('Relationship breakdown:');
100
+ verbCounts.forEach((count, verbType) => {
101
+ console.log(` ${verbType}: ${count}`);
102
+ });
103
+ }
104
+ return {
105
+ nounCount: totalNouns,
106
+ verbCount: totalVerbs,
107
+ entityCounts,
108
+ verbCounts,
109
+ duration
110
+ };
111
+ }
112
+ //# sourceMappingURL=rebuildCounts.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
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",