@soulcraft/brainy 5.3.5 → 5.4.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.
@@ -2,7 +2,6 @@
2
2
  * Memory Storage Adapter
3
3
  * In-memory storage adapter for environments where persistent storage is not available or needed
4
4
  */
5
- import { NounType } from '../../coreTypes.js';
6
5
  import { BaseStorage } from '../baseStorage.js';
7
6
  // No type aliases needed - using the original types directly
8
7
  /**
@@ -22,9 +21,7 @@ export class MemoryStorage extends BaseStorage {
22
21
  }
23
22
  constructor() {
24
23
  super();
25
- // Single map of noun ID to noun
26
- this.nouns = new Map();
27
- this.verbs = new Map();
24
+ // v5.4.0: Removed redundant Maps (nouns, verbs) - objectStore handles all storage via type-first paths
28
25
  this.statistics = null;
29
26
  // Unified object store for primitive operations (replaces metadata, nounMetadata, verbMetadata)
30
27
  this.objectStore = new Map();
@@ -63,456 +60,17 @@ export class MemoryStorage extends BaseStorage {
63
60
  async init() {
64
61
  this.isInitialized = true;
65
62
  }
66
- /**
67
- * Save a noun to storage (v4.0.0: pure vector only, no metadata)
68
- * v5.0.1: COW-aware - uses branch-prefixed paths for fork isolation
69
- */
70
- async saveNoun_internal(noun) {
71
- const isNew = !this.nouns.has(noun.id);
72
- // Create a deep copy to avoid reference issues
73
- // v4.0.0: Store ONLY vector data (no metadata field)
74
- // Metadata is saved separately via saveNounMetadata() by base class
75
- const nounCopy = {
76
- id: noun.id,
77
- vector: [...noun.vector],
78
- connections: new Map(),
79
- level: noun.level || 0
80
- // ✅ NO metadata field in v4.0.0
81
- };
82
- // Copy connections
83
- for (const [level, connections] of noun.connections.entries()) {
84
- nounCopy.connections.set(level, new Set(connections));
85
- }
86
- // v5.0.1: COW-aware write using branch-prefixed path
87
- // Use synthetic path for vector storage (nouns don't have types in standalone mode)
88
- const path = `hnsw/nouns/${noun.id}.json`;
89
- await this.writeObjectToBranch(path, nounCopy);
90
- // ALSO store in nouns Map for fast iteration (getNouns, initializeCounts)
91
- // This is redundant but maintains backward compatibility
92
- this.nouns.set(noun.id, nounCopy);
93
- // Count tracking happens in baseStorage.saveNounMetadata_internal (v4.1.2)
94
- // This fixes the race condition where metadata didn't exist yet
95
- }
96
- /**
97
- * Get a noun from storage (v4.0.0: returns pure vector only)
98
- * Base class handles combining with metadata
99
- * v5.0.1: COW-aware - reads from branch-prefixed paths with inheritance
100
- */
101
- async getNoun_internal(id) {
102
- // v5.0.1: COW-aware read using branch-prefixed path with inheritance
103
- const path = `hnsw/nouns/${id}.json`;
104
- const noun = await this.readWithInheritance(path);
105
- // If not found, return null
106
- if (!noun) {
107
- return null;
108
- }
109
- // Return a deep copy to avoid reference issues
110
- // v4.0.0: Return ONLY vector data (no metadata field)
111
- const nounCopy = {
112
- id: noun.id,
113
- vector: [...noun.vector],
114
- connections: new Map(),
115
- level: noun.level || 0
116
- // ✅ NO metadata field in v4.0.0
117
- };
118
- // Copy connections (handle both Map and plain object from JSON)
119
- const connections = noun.connections instanceof Map ? noun.connections : new Map(Object.entries(noun.connections || {}));
120
- for (const [level, conns] of connections.entries()) {
121
- nounCopy.connections.set(Number(level), new Set(conns));
122
- }
123
- return nounCopy;
124
- }
63
+ // v5.4.0: Removed saveNoun_internal and getNoun_internal - using BaseStorage's type-first implementation
125
64
  /**
126
65
  * Get nouns with pagination and filtering
127
66
  * v4.0.0: Returns HNSWNounWithMetadata[] (includes metadata field)
128
67
  * @param options Pagination and filtering options
129
68
  * @returns Promise that resolves to a paginated result of nouns with metadata
130
69
  */
131
- async getNouns(options = {}) {
132
- const pagination = options.pagination || {};
133
- const filter = options.filter || {};
134
- // Default values
135
- const offset = pagination.offset || 0;
136
- const limit = pagination.limit || 100;
137
- // Convert string types to arrays for consistent handling
138
- const nounTypes = filter.nounType
139
- ? Array.isArray(filter.nounType) ? filter.nounType : [filter.nounType]
140
- : undefined;
141
- const services = filter.service
142
- ? Array.isArray(filter.service) ? filter.service : [filter.service]
143
- : undefined;
144
- // First, collect all noun IDs that match the filter criteria
145
- const matchingIds = [];
146
- // Iterate through all nouns to find matches
147
- // v4.0.0: Load metadata from separate storage (no embedded metadata field)
148
- for (const [nounId, noun] of this.nouns.entries()) {
149
- // Get metadata from separate storage
150
- const metadata = await this.getNounMetadata(nounId);
151
- // Skip if no metadata (shouldn't happen in v4.0.0 but be defensive)
152
- if (!metadata) {
153
- continue;
154
- }
155
- // Filter by noun type if specified
156
- if (nounTypes && metadata.noun && !nounTypes.includes(metadata.noun)) {
157
- continue;
158
- }
159
- // Filter by service if specified
160
- if (services && metadata.service && !services.includes(metadata.service)) {
161
- continue;
162
- }
163
- // Filter by metadata fields if specified
164
- if (filter.metadata) {
165
- let metadataMatch = true;
166
- for (const [key, value] of Object.entries(filter.metadata)) {
167
- if (metadata[key] !== value) {
168
- metadataMatch = false;
169
- break;
170
- }
171
- }
172
- if (!metadataMatch)
173
- continue;
174
- }
175
- // If we got here, the noun matches all filters
176
- matchingIds.push(nounId);
177
- }
178
- // Calculate pagination
179
- const totalCount = matchingIds.length;
180
- const paginatedIds = matchingIds.slice(offset, offset + limit);
181
- const hasMore = offset + limit < totalCount;
182
- // Create cursor for next page if there are more results
183
- const nextCursor = hasMore ? `${offset + limit}` : undefined;
184
- // Fetch the actual nouns for the current page
185
- // v4.0.0: Return HNSWNounWithMetadata (includes metadata field)
186
- const items = [];
187
- for (const id of paginatedIds) {
188
- const noun = this.nouns.get(id);
189
- if (!noun)
190
- continue;
191
- // Get metadata from separate storage
192
- // FIX v4.7.4: Don't skip nouns without metadata - metadata is optional in v4.0.0
193
- const metadata = await this.getNounMetadata(id);
194
- // v4.8.0: Extract standard fields from metadata to top-level
195
- const metadataObj = (metadata || {});
196
- const { noun: nounType, createdAt, updatedAt, confidence, weight, service, data, createdBy, ...customMetadata } = metadataObj;
197
- // v4.8.0: Create HNSWNounWithMetadata with standard fields at top-level
198
- const nounWithMetadata = {
199
- id: noun.id,
200
- vector: [...noun.vector],
201
- connections: new Map(),
202
- level: noun.level || 0,
203
- // v4.8.0: Standard fields at top-level
204
- type: nounType || NounType.Thing,
205
- createdAt: createdAt || Date.now(),
206
- updatedAt: updatedAt || Date.now(),
207
- confidence: confidence,
208
- weight: weight,
209
- service: service,
210
- data: data,
211
- createdBy,
212
- // Only custom user fields in metadata
213
- metadata: customMetadata
214
- };
215
- // Copy connections
216
- for (const [level, connections] of noun.connections.entries()) {
217
- nounWithMetadata.connections.set(level, new Set(connections));
218
- }
219
- items.push(nounWithMetadata);
220
- }
221
- return {
222
- items,
223
- totalCount,
224
- hasMore,
225
- nextCursor
226
- };
227
- }
228
- /**
229
- * Get nouns with pagination - simplified interface for compatibility
230
- * v4.0.0: Returns HNSWNounWithMetadata[] (includes metadata field)
231
- */
232
- async getNounsWithPagination(options = {}) {
233
- // Convert to the getNouns format
234
- const result = await this.getNouns({
235
- pagination: {
236
- offset: options.cursor ? parseInt(options.cursor) : 0,
237
- limit: options.limit || 100
238
- },
239
- filter: options.filter
240
- });
241
- return {
242
- items: result.items,
243
- totalCount: result.totalCount || 0,
244
- hasMore: result.hasMore,
245
- nextCursor: result.nextCursor
246
- };
247
- }
248
- /**
249
- * Get nouns by noun type
250
- * @param nounType The noun type to filter by
251
- * @returns Promise that resolves to an array of nouns of the specified noun type
252
- * @deprecated Use getNouns() with filter.nounType instead
253
- */
254
- async getNounsByNounType_internal(nounType) {
255
- const result = await this.getNouns({
256
- filter: {
257
- nounType
258
- }
259
- });
260
- return result.items;
261
- }
262
- /**
263
- * Delete a noun from storage (v4.0.0)
264
- * v5.0.1: COW-aware - deletes from branch-prefixed paths
265
- */
266
- async deleteNoun_internal(id) {
267
- // v4.0.0: Get type from separate metadata storage
268
- const metadata = await this.getNounMetadata(id);
269
- if (metadata) {
270
- const type = metadata.noun || 'default';
271
- this.decrementEntityCount(type);
272
- }
273
- // v5.0.1: COW-aware delete using branch-prefixed path
274
- const path = `hnsw/nouns/${id}.json`;
275
- await this.deleteObjectFromBranch(path);
276
- // Also remove from nouns Map for fast iteration
277
- this.nouns.delete(id);
278
- }
279
- /**
280
- * Save a verb to storage (v4.0.0: pure vector + core fields, no metadata)
281
- * v5.0.1: COW-aware - uses branch-prefixed paths for fork isolation
282
- */
283
- async saveVerb_internal(verb) {
284
- const isNew = !this.verbs.has(verb.id);
285
- // Create a deep copy to avoid reference issues
286
- // v4.0.0: Include core relational fields but NO metadata field
287
- const verbCopy = {
288
- id: verb.id,
289
- vector: [...verb.vector],
290
- connections: new Map(),
291
- // CORE RELATIONAL DATA (part of HNSWVerb in v4.0.0)
292
- verb: verb.verb,
293
- sourceId: verb.sourceId,
294
- targetId: verb.targetId
295
- // ✅ NO metadata field in v4.0.0
296
- };
297
- // Copy connections
298
- for (const [level, connections] of verb.connections.entries()) {
299
- verbCopy.connections.set(level, new Set(connections));
300
- }
301
- // v5.0.1: COW-aware write using branch-prefixed path
302
- const path = `hnsw/verbs/${verb.id}.json`;
303
- await this.writeObjectToBranch(path, verbCopy);
304
- // ALSO store in verbs Map for fast iteration (getVerbs, initializeCounts)
305
- this.verbs.set(verb.id, verbCopy);
306
- // Note: Count tracking happens in saveVerbMetadata since metadata is separate
307
- }
308
- /**
309
- * Get a verb from storage (v4.0.0: returns pure vector + core fields)
310
- * Base class handles combining with metadata
311
- * v5.0.1: COW-aware - reads from branch-prefixed paths with inheritance
312
- */
313
- async getVerb_internal(id) {
314
- // v5.0.1: COW-aware read using branch-prefixed path with inheritance
315
- const path = `hnsw/verbs/${id}.json`;
316
- const verb = await this.readWithInheritance(path);
317
- // If not found, return null
318
- if (!verb) {
319
- return null;
320
- }
321
- // Return a deep copy of the HNSWVerb
322
- // v4.0.0: Include core relational fields but NO metadata field
323
- const verbCopy = {
324
- id: verb.id,
325
- vector: [...verb.vector],
326
- connections: new Map(),
327
- // CORE RELATIONAL DATA (part of HNSWVerb in v4.0.0)
328
- verb: verb.verb,
329
- sourceId: verb.sourceId,
330
- targetId: verb.targetId
331
- // ✅ NO metadata field in v4.0.0
332
- };
333
- // Copy connections (handle both Map and plain object from JSON)
334
- const connections = verb.connections instanceof Map ? verb.connections : new Map(Object.entries(verb.connections || {}));
335
- for (const [level, conns] of connections.entries()) {
336
- verbCopy.connections.set(Number(level), new Set(conns));
337
- }
338
- return verbCopy;
339
- }
340
- /**
341
- * Get verbs with pagination and filtering
342
- * v4.0.0: Returns HNSWVerbWithMetadata[] (includes metadata field)
343
- * @param options Pagination and filtering options
344
- * @returns Promise that resolves to a paginated result of verbs with metadata
345
- */
346
- async getVerbs(options = {}) {
347
- const pagination = options.pagination || {};
348
- const filter = options.filter || {};
349
- // Default values
350
- const offset = pagination.offset || 0;
351
- const limit = pagination.limit || 100;
352
- // Convert string types to arrays for consistent handling
353
- const verbTypes = filter.verbType
354
- ? Array.isArray(filter.verbType) ? filter.verbType : [filter.verbType]
355
- : undefined;
356
- const sourceIds = filter.sourceId
357
- ? Array.isArray(filter.sourceId) ? filter.sourceId : [filter.sourceId]
358
- : undefined;
359
- const targetIds = filter.targetId
360
- ? Array.isArray(filter.targetId) ? filter.targetId : [filter.targetId]
361
- : undefined;
362
- const services = filter.service
363
- ? Array.isArray(filter.service) ? filter.service : [filter.service]
364
- : undefined;
365
- // First, collect all verb IDs that match the filter criteria
366
- const matchingIds = [];
367
- // Iterate through all verbs to find matches
368
- // v4.0.0: Core fields (verb, sourceId, targetId) are in HNSWVerb, not metadata
369
- for (const [verbId, hnswVerb] of this.verbs.entries()) {
370
- // Get the metadata for service/data filtering
371
- const metadata = await this.getVerbMetadata(verbId);
372
- // Filter by verb type if specified
373
- // v4.0.0: verb type is in HNSWVerb.verb
374
- if (verbTypes && !verbTypes.includes(hnswVerb.verb || '')) {
375
- continue;
376
- }
377
- // Filter by source ID if specified
378
- // v4.0.0: sourceId is in HNSWVerb.sourceId
379
- if (sourceIds && !sourceIds.includes(hnswVerb.sourceId || '')) {
380
- continue;
381
- }
382
- // Filter by target ID if specified
383
- // v4.0.0: targetId is in HNSWVerb.targetId
384
- if (targetIds && !targetIds.includes(hnswVerb.targetId || '')) {
385
- continue;
386
- }
387
- // Filter by metadata fields if specified
388
- if (filter.metadata && metadata) {
389
- let metadataMatch = true;
390
- for (const [key, value] of Object.entries(filter.metadata)) {
391
- const metadataValue = metadata[key];
392
- if (metadataValue !== value) {
393
- metadataMatch = false;
394
- break;
395
- }
396
- }
397
- if (!metadataMatch)
398
- continue;
399
- }
400
- // Filter by service if specified
401
- if (services && metadata && metadata.service && !services.includes(metadata.service)) {
402
- continue;
403
- }
404
- // If we got here, the verb matches all filters
405
- matchingIds.push(verbId);
406
- }
407
- // Calculate pagination
408
- const totalCount = matchingIds.length;
409
- const paginatedIds = matchingIds.slice(offset, offset + limit);
410
- const hasMore = offset + limit < totalCount;
411
- // Create cursor for next page if there are more results
412
- const nextCursor = hasMore ? `${offset + limit}` : undefined;
413
- // Fetch the actual verbs for the current page
414
- // v4.0.0: Return HNSWVerbWithMetadata (includes metadata field)
415
- const items = [];
416
- for (const id of paginatedIds) {
417
- const hnswVerb = this.verbs.get(id);
418
- if (!hnswVerb)
419
- continue;
420
- // Get metadata from separate storage
421
- // FIX v4.7.4: Don't skip verbs without metadata - metadata is optional in v4.0.0
422
- // Core fields (verb, sourceId, targetId) are in HNSWVerb itself
423
- const metadata = await this.getVerbMetadata(id);
424
- // v4.8.0: Extract standard fields from metadata to top-level
425
- const metadataObj = metadata || {};
426
- const { createdAt, updatedAt, confidence, weight, service, data, createdBy, ...customMetadata } = metadataObj;
427
- // v4.8.0: Create HNSWVerbWithMetadata with standard fields at top-level
428
- const verbWithMetadata = {
429
- id: hnswVerb.id,
430
- vector: [...hnswVerb.vector],
431
- connections: new Map(),
432
- // Core relational fields (part of HNSWVerb)
433
- verb: hnswVerb.verb,
434
- sourceId: hnswVerb.sourceId,
435
- targetId: hnswVerb.targetId,
436
- // v4.8.0: Standard fields at top-level
437
- createdAt: createdAt || Date.now(),
438
- updatedAt: updatedAt || Date.now(),
439
- confidence: confidence,
440
- weight: weight,
441
- service: service,
442
- data: data,
443
- createdBy,
444
- // Only custom user fields in metadata
445
- metadata: customMetadata
446
- };
447
- // Copy connections
448
- for (const [level, connections] of hnswVerb.connections.entries()) {
449
- verbWithMetadata.connections.set(level, new Set(connections));
450
- }
451
- items.push(verbWithMetadata);
452
- }
453
- return {
454
- items,
455
- totalCount,
456
- hasMore,
457
- nextCursor
458
- };
459
- }
460
- /**
461
- * Get verbs by source
462
- * @deprecated Use getVerbs() with filter.sourceId instead
463
- */
464
- async getVerbsBySource_internal(sourceId) {
465
- const result = await this.getVerbs({
466
- filter: {
467
- sourceId
468
- }
469
- });
470
- return result.items;
471
- }
472
- /**
473
- * Get verbs by target
474
- * @deprecated Use getVerbs() with filter.targetId instead
475
- */
476
- async getVerbsByTarget_internal(targetId) {
477
- const result = await this.getVerbs({
478
- filter: {
479
- targetId
480
- }
481
- });
482
- return result.items;
483
- }
484
- /**
485
- * Get verbs by type
486
- * @deprecated Use getVerbs() with filter.verbType instead
487
- */
488
- async getVerbsByType_internal(type) {
489
- const result = await this.getVerbs({
490
- filter: {
491
- verbType: type
492
- }
493
- });
494
- return result.items;
495
- }
496
- /**
497
- * Delete a verb from storage
498
- * v5.0.1: COW-aware - deletes from branch-prefixed paths
499
- */
500
- async deleteVerb_internal(id) {
501
- // CRITICAL: Also delete verb metadata - this is what getVerbs() uses to find verbs
502
- // Without this, getVerbsBySource() will still find "deleted" verbs via their metadata
503
- const metadata = await this.getVerbMetadata(id);
504
- if (metadata) {
505
- const verbType = metadata.verb || metadata.type || 'default';
506
- this.decrementVerbCount(verbType);
507
- // Delete the metadata using the base storage method
508
- await this.deleteVerbMetadata(id);
509
- }
510
- // v5.0.1: COW-aware delete using branch-prefixed path
511
- const path = `hnsw/verbs/${id}.json`;
512
- await this.deleteObjectFromBranch(path);
513
- // Also remove from verbs Map for fast iteration
514
- this.verbs.delete(id);
515
- }
70
+ // v5.4.0: Removed public method overrides (getNouns, getNounsWithPagination, getVerbs) - using BaseStorage's type-first implementation
71
+ // v5.4.0: Removed getNounsByNounType_internal and deleteNoun_internal - using BaseStorage's type-first implementation
72
+ // v5.4.0: Removed saveVerb_internal and getVerb_internal - using BaseStorage's type-first implementation
73
+ // v5.4.0: Removed verb *_internal method overrides - using BaseStorage's type-first implementation
516
74
  /**
517
75
  * Primitive operation: Write object to path
518
76
  * All metadata operations use this internally via base class routing
@@ -571,18 +129,22 @@ export class MemoryStorage extends BaseStorage {
571
129
  }
572
130
  /**
573
131
  * Clear all data from storage
132
+ * v5.4.0: Clears objectStore (type-first paths)
574
133
  */
575
134
  async clear() {
576
- this.nouns.clear();
577
- this.verbs.clear();
578
135
  this.objectStore.clear();
579
136
  this.statistics = null;
137
+ this.totalNounCount = 0;
138
+ this.totalVerbCount = 0;
139
+ this.entityCounts.clear();
140
+ this.verbCounts.clear();
580
141
  // Clear the statistics cache
581
142
  this.statisticsCache = null;
582
143
  this.statisticsModified = false;
583
144
  }
584
145
  /**
585
146
  * Get information about storage usage and capacity
147
+ * v5.4.0: Uses BaseStorage counts
586
148
  */
587
149
  async getStorageStatus() {
588
150
  return {
@@ -590,9 +152,9 @@ export class MemoryStorage extends BaseStorage {
590
152
  used: 0, // In-memory storage doesn't have a meaningful size
591
153
  quota: null, // In-memory storage doesn't have a quota
592
154
  details: {
593
- nodeCount: this.nouns.size,
594
- edgeCount: this.verbs.size,
595
- metadataCount: this.objectStore.size
155
+ nodeCount: this.totalNounCount,
156
+ edgeCount: this.totalVerbCount,
157
+ objectStoreSize: this.objectStore.size
596
158
  }
597
159
  };
598
160
  }
@@ -676,30 +238,30 @@ export class MemoryStorage extends BaseStorage {
676
238
  * Initialize counts from in-memory storage - O(1) operation (v4.0.0)
677
239
  */
678
240
  async initializeCounts() {
679
- // For memory storage, initialize counts from current in-memory state
680
- this.totalNounCount = this.nouns.size;
681
- this.totalVerbCount = this.verbs.size;
682
- // Initialize type-based counts by scanning metadata storage (v4.0.0)
241
+ // v5.4.0: Scan objectStore paths (type-first structure) to count entities
683
242
  this.entityCounts.clear();
684
243
  this.verbCounts.clear();
685
- // Count nouns by loading metadata for each
686
- for (const [nounId, noun] of this.nouns.entries()) {
687
- const metadata = await this.getNounMetadata(nounId);
688
- if (metadata) {
689
- const type = metadata.noun || 'default';
244
+ let totalNouns = 0;
245
+ let totalVerbs = 0;
246
+ // Scan all paths in objectStore
247
+ for (const path of this.objectStore.keys()) {
248
+ // Count nouns by type (entities/nouns/{type}/vectors/{shard}/{id}.json)
249
+ const nounMatch = path.match(/^entities\/nouns\/([^/]+)\/vectors\//);
250
+ if (nounMatch) {
251
+ const type = nounMatch[1];
690
252
  this.entityCounts.set(type, (this.entityCounts.get(type) || 0) + 1);
253
+ totalNouns++;
691
254
  }
692
- }
693
- // Count verbs by loading metadata for each
694
- for (const [verbId, verb] of this.verbs.entries()) {
695
- const metadata = await this.getVerbMetadata(verbId);
696
- if (metadata) {
697
- // VerbMetadata doesn't have verb type - that's in HNSWVerb now
698
- // Use the verb's type from the HNSWVerb itself
699
- const type = verb.verb || 'default';
255
+ // Count verbs by type (entities/verbs/{type}/vectors/{shard}/{id}.json)
256
+ const verbMatch = path.match(/^entities\/verbs\/([^/]+)\/vectors\//);
257
+ if (verbMatch) {
258
+ const type = verbMatch[1];
700
259
  this.verbCounts.set(type, (this.verbCounts.get(type) || 0) + 1);
260
+ totalVerbs++;
701
261
  }
702
262
  }
263
+ this.totalNounCount = totalNouns;
264
+ this.totalVerbCount = totalVerbs;
703
265
  }
704
266
  /**
705
267
  * Persist counts to storage - no-op for memory storage
@@ -713,9 +275,10 @@ export class MemoryStorage extends BaseStorage {
713
275
  // =============================================
714
276
  /**
715
277
  * Get vector for a noun
278
+ * v5.4.0: Uses BaseStorage's type-first implementation
716
279
  */
717
280
  async getNounVector(id) {
718
- const noun = this.nouns.get(id);
281
+ const noun = await this.getNoun(id);
719
282
  return noun ? [...noun.vector] : null;
720
283
  }
721
284
  /**