@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.
@@ -5,7 +5,7 @@
5
5
  import { GraphAdjacencyIndex } from '../graph/graphAdjacencyIndex.js';
6
6
  import { BaseStorageAdapter } from './adapters/baseStorageAdapter.js';
7
7
  import { validateNounType, validateVerbType } from '../utils/typeValidation.js';
8
- import { NounType } from '../types/graphTypes.js';
8
+ import { NounType, TypeUtils, NOUN_TYPE_COUNT, VERB_TYPE_COUNT } from '../types/graphTypes.js';
9
9
  import { getShardIdFromUuid } from './sharding.js';
10
10
  import { RefManager } from './cow/RefManager.js';
11
11
  import { BlobStorage } from './cow/BlobStorage.js';
@@ -32,6 +32,38 @@ export function getDirectoryPath(entityType, dataType) {
32
32
  return dataType === 'vector' ? VERBS_DIR : VERBS_METADATA_DIR;
33
33
  }
34
34
  }
35
+ /**
36
+ * Type-first path generators (v5.4.0)
37
+ * Built-in type-aware organization for all storage adapters
38
+ */
39
+ /**
40
+ * Get type-first path for noun vectors
41
+ */
42
+ function getNounVectorPath(type, id) {
43
+ const shard = getShardIdFromUuid(id);
44
+ return `entities/nouns/${type}/vectors/${shard}/${id}.json`;
45
+ }
46
+ /**
47
+ * Get type-first path for noun metadata
48
+ */
49
+ function getNounMetadataPath(type, id) {
50
+ const shard = getShardIdFromUuid(id);
51
+ return `entities/nouns/${type}/metadata/${shard}/${id}.json`;
52
+ }
53
+ /**
54
+ * Get type-first path for verb vectors
55
+ */
56
+ function getVerbVectorPath(type, id) {
57
+ const shard = getShardIdFromUuid(id);
58
+ return `entities/verbs/${type}/vectors/${shard}/${id}.json`;
59
+ }
60
+ /**
61
+ * Get type-first path for verb metadata
62
+ */
63
+ function getVerbMetadataPath(type, id) {
64
+ const shard = getShardIdFromUuid(id);
65
+ return `entities/verbs/${type}/metadata/${shard}/${id}.json`;
66
+ }
35
67
  /**
36
68
  * Base storage adapter that implements common functionality
37
69
  * This is an abstract class that should be extended by specific storage adapters
@@ -43,6 +75,14 @@ export class BaseStorage extends BaseStorageAdapter {
43
75
  this.readOnly = false;
44
76
  this.currentBranch = 'main';
45
77
  this.cowEnabled = false;
78
+ // Type-first indexing support (v5.4.0)
79
+ // Built into all storage adapters for billion-scale efficiency
80
+ this.nounCountsByType = new Uint32Array(NOUN_TYPE_COUNT); // 124 bytes
81
+ this.verbCountsByType = new Uint32Array(VERB_TYPE_COUNT); // 160 bytes
82
+ // Total: 284 bytes (99.76% reduction vs Map-based tracking)
83
+ // Type cache for O(1) lookups after first access
84
+ this.nounTypeCache = new Map();
85
+ this.verbTypeCache = new Map();
46
86
  }
47
87
  /**
48
88
  * Analyze a storage key to determine its routing and path
@@ -116,6 +156,17 @@ export class BaseStorage extends BaseStorageAdapter {
116
156
  };
117
157
  }
118
158
  }
159
+ /**
160
+ * Initialize the storage adapter (v5.4.0)
161
+ * Loads type statistics for built-in type-aware indexing
162
+ *
163
+ * IMPORTANT: If your adapter overrides init(), call await super.init() first!
164
+ */
165
+ async init() {
166
+ // Load type statistics from storage (if they exist)
167
+ await this.loadTypeStatistics();
168
+ this.isInitialized = true;
169
+ }
119
170
  /**
120
171
  * Ensure the storage adapter is initialized
121
172
  */
@@ -274,6 +325,13 @@ export class BaseStorage extends BaseStorageAdapter {
274
325
  * @protected - Available to subclasses for COW implementation
275
326
  */
276
327
  resolveBranchPath(basePath, branch) {
328
+ // CRITICAL FIX (v5.3.6): COW metadata (_cow/*) must NEVER be branch-scoped
329
+ // Refs, commits, and blobs are global metadata with their own internal branching.
330
+ // Branch-scoping COW paths causes fork() to write refs to wrong locations,
331
+ // leading to "Branch does not exist" errors on checkout (see Workshop bug report).
332
+ if (basePath.startsWith('_cow/')) {
333
+ return basePath; // COW metadata is global across all branches
334
+ }
277
335
  if (!this.cowEnabled) {
278
336
  return basePath; // COW disabled, use direct path
279
337
  }
@@ -763,6 +821,90 @@ export class BaseStorage extends BaseStorageAdapter {
763
821
  };
764
822
  }
765
823
  }
824
+ /**
825
+ * Get nouns with pagination (v5.4.0: Type-first implementation)
826
+ *
827
+ * CRITICAL: This method is required for brain.find() to work!
828
+ * Iterates through all noun types to find entities.
829
+ */
830
+ async getNounsWithPagination(options) {
831
+ await this.ensureInitialized();
832
+ const { limit, offset, filter } = options;
833
+ const allNouns = [];
834
+ // v5.4.0: Iterate through all noun types (type-first architecture)
835
+ for (let i = 0; i < NOUN_TYPE_COUNT; i++) {
836
+ const type = TypeUtils.getNounFromIndex(i);
837
+ // If filtering by type, skip other types
838
+ if (filter?.nounType) {
839
+ const filterTypes = Array.isArray(filter.nounType) ? filter.nounType : [filter.nounType];
840
+ if (!filterTypes.includes(type)) {
841
+ continue;
842
+ }
843
+ }
844
+ const typeDir = `entities/nouns/${type}/vectors`;
845
+ try {
846
+ // List all noun files for this type
847
+ const nounFiles = await this.listObjectsInBranch(typeDir);
848
+ for (const nounPath of nounFiles) {
849
+ // Skip if not a .json file
850
+ if (!nounPath.endsWith('.json'))
851
+ continue;
852
+ try {
853
+ const noun = await this.readWithInheritance(nounPath);
854
+ if (noun) {
855
+ // Load metadata
856
+ const metadataPath = getNounMetadataPath(type, noun.id);
857
+ const metadata = await this.readWithInheritance(metadataPath);
858
+ if (metadata) {
859
+ // Apply service filter if specified
860
+ if (filter?.service) {
861
+ const services = Array.isArray(filter.service) ? filter.service : [filter.service];
862
+ if (metadata.service && !services.includes(metadata.service)) {
863
+ continue;
864
+ }
865
+ }
866
+ // Combine noun + metadata (v5.4.0: Extract standard fields to top-level)
867
+ allNouns.push({
868
+ ...noun,
869
+ type: metadata.noun || type, // Required: Extract type from metadata
870
+ confidence: metadata.confidence,
871
+ weight: metadata.weight,
872
+ createdAt: metadata.createdAt
873
+ ? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
874
+ : Date.now(),
875
+ updatedAt: metadata.updatedAt
876
+ ? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
877
+ : Date.now(),
878
+ service: metadata.service,
879
+ data: metadata.data,
880
+ createdBy: metadata.createdBy,
881
+ metadata: metadata || {}
882
+ });
883
+ }
884
+ }
885
+ }
886
+ catch (error) {
887
+ // Skip nouns that fail to load
888
+ }
889
+ }
890
+ }
891
+ catch (error) {
892
+ // Skip types that have no data
893
+ }
894
+ }
895
+ // Apply pagination
896
+ const totalCount = allNouns.length;
897
+ const paginatedNouns = allNouns.slice(offset, offset + limit);
898
+ const hasMore = offset + limit < totalCount;
899
+ return {
900
+ items: paginatedNouns,
901
+ totalCount,
902
+ hasMore,
903
+ nextCursor: hasMore && paginatedNouns.length > 0
904
+ ? paginatedNouns[paginatedNouns.length - 1].id
905
+ : undefined
906
+ };
907
+ }
766
908
  /**
767
909
  * Get verbs with pagination and filtering
768
910
  * @param options Pagination and filtering options
@@ -1026,12 +1168,16 @@ export class BaseStorage extends BaseStorageAdapter {
1026
1168
  */
1027
1169
  async saveNounMetadata_internal(id, metadata) {
1028
1170
  await this.ensureInitialized();
1171
+ // v5.4.0: Extract and cache type for type-first routing
1172
+ const type = (metadata.noun || 'thing');
1173
+ this.nounTypeCache.set(id, type);
1174
+ // v5.4.0: Use type-first path
1175
+ const path = getNounMetadataPath(type, id);
1029
1176
  // Determine if this is a new entity by checking if metadata already exists
1030
- const keyInfo = this.analyzeKey(id, 'noun-metadata');
1031
- const existingMetadata = await this.readWithInheritance(keyInfo.fullPath);
1177
+ const existingMetadata = await this.readWithInheritance(path);
1032
1178
  const isNew = !existingMetadata;
1033
1179
  // Save the metadata (COW-aware - writes to branch-specific path)
1034
- await this.writeObjectToBranch(keyInfo.fullPath, metadata);
1180
+ await this.writeObjectToBranch(path, metadata);
1035
1181
  // CRITICAL FIX (v4.1.2): Increment count for new entities
1036
1182
  // This runs AFTER metadata is saved, guaranteeing type information is available
1037
1183
  // Uses synchronous increment since storage operations are already serialized
@@ -1046,21 +1192,65 @@ export class BaseStorage extends BaseStorageAdapter {
1046
1192
  }
1047
1193
  /**
1048
1194
  * Get noun metadata from storage (v4.0.0: now typed)
1049
- * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
1195
+ * v5.4.0: Uses type-first paths (must match saveNounMetadata_internal)
1050
1196
  */
1051
1197
  async getNounMetadata(id) {
1052
1198
  await this.ensureInitialized();
1053
- const keyInfo = this.analyzeKey(id, 'noun-metadata');
1054
- return this.readWithInheritance(keyInfo.fullPath);
1199
+ // v5.4.0: Check type cache first (populated during save)
1200
+ const cachedType = this.nounTypeCache.get(id);
1201
+ if (cachedType) {
1202
+ const path = getNounMetadataPath(cachedType, id);
1203
+ return this.readWithInheritance(path);
1204
+ }
1205
+ // Fallback: search across all types (expensive but necessary if cache miss)
1206
+ for (let i = 0; i < NOUN_TYPE_COUNT; i++) {
1207
+ const type = TypeUtils.getNounFromIndex(i);
1208
+ const path = getNounMetadataPath(type, id);
1209
+ try {
1210
+ const metadata = await this.readWithInheritance(path);
1211
+ if (metadata) {
1212
+ // Cache the type for next time
1213
+ this.nounTypeCache.set(id, type);
1214
+ return metadata;
1215
+ }
1216
+ }
1217
+ catch (error) {
1218
+ // Not in this type, continue searching
1219
+ }
1220
+ }
1221
+ return null;
1055
1222
  }
1056
1223
  /**
1057
1224
  * Delete noun metadata from storage
1058
- * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
1225
+ * v5.4.0: Uses type-first paths (must match saveNounMetadata_internal)
1059
1226
  */
1060
1227
  async deleteNounMetadata(id) {
1061
1228
  await this.ensureInitialized();
1062
- const keyInfo = this.analyzeKey(id, 'noun-metadata');
1063
- return this.deleteObjectFromBranch(keyInfo.fullPath);
1229
+ // v5.4.0: Use cached type for path
1230
+ const cachedType = this.nounTypeCache.get(id);
1231
+ if (cachedType) {
1232
+ const path = getNounMetadataPath(cachedType, id);
1233
+ await this.deleteObjectFromBranch(path);
1234
+ // Remove from cache after deletion
1235
+ this.nounTypeCache.delete(id);
1236
+ return;
1237
+ }
1238
+ // If not in cache, search all types to find and delete
1239
+ for (let i = 0; i < NOUN_TYPE_COUNT; i++) {
1240
+ const type = TypeUtils.getNounFromIndex(i);
1241
+ const path = getNounMetadataPath(type, id);
1242
+ try {
1243
+ // Check if exists before deleting
1244
+ const exists = await this.readWithInheritance(path);
1245
+ if (exists) {
1246
+ await this.deleteObjectFromBranch(path);
1247
+ return;
1248
+ }
1249
+ }
1250
+ catch (error) {
1251
+ // Not in this type, continue searching
1252
+ }
1253
+ }
1064
1254
  }
1065
1255
  /**
1066
1256
  * Save verb metadata to storage (v4.0.0: now typed)
@@ -1072,7 +1262,7 @@ export class BaseStorage extends BaseStorageAdapter {
1072
1262
  }
1073
1263
  /**
1074
1264
  * Internal method for saving verb metadata (v4.0.0: now typed)
1075
- * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
1265
+ * v5.4.0: Uses type-first paths (must match getVerbMetadata)
1076
1266
  *
1077
1267
  * CRITICAL (v4.1.2): Count synchronization happens here
1078
1268
  * This ensures verb counts are updated AFTER metadata exists, fixing the race condition
@@ -1084,19 +1274,29 @@ export class BaseStorage extends BaseStorageAdapter {
1084
1274
  */
1085
1275
  async saveVerbMetadata_internal(id, metadata) {
1086
1276
  await this.ensureInitialized();
1277
+ // v5.4.0: Extract verb type from metadata for type-first path
1278
+ const verbType = metadata.verb;
1279
+ if (!verbType) {
1280
+ // Backward compatibility: fallback to old path if no verb type
1281
+ const keyInfo = this.analyzeKey(id, 'verb-metadata');
1282
+ await this.writeObjectToBranch(keyInfo.fullPath, metadata);
1283
+ return;
1284
+ }
1285
+ // v5.4.0: Use type-first path
1286
+ const path = getVerbMetadataPath(verbType, id);
1087
1287
  // Determine if this is a new verb by checking if metadata already exists
1088
- const keyInfo = this.analyzeKey(id, 'verb-metadata');
1089
- const existingMetadata = await this.readWithInheritance(keyInfo.fullPath);
1288
+ const existingMetadata = await this.readWithInheritance(path);
1090
1289
  const isNew = !existingMetadata;
1091
1290
  // Save the metadata (COW-aware - writes to branch-specific path)
1092
- await this.writeObjectToBranch(keyInfo.fullPath, metadata);
1291
+ await this.writeObjectToBranch(path, metadata);
1292
+ // v5.4.0: Cache verb type for faster lookups
1293
+ this.verbTypeCache.set(id, verbType);
1093
1294
  // CRITICAL FIX (v4.1.2): Increment verb count for new relationships
1094
1295
  // This runs AFTER metadata is saved
1095
- // Verb type is now stored in metadata (as of v4.1.2) to avoid loading HNSWVerb
1096
1296
  // Uses synchronous increment since storage operations are already serialized
1097
1297
  // Fixes Bug #2: Count synchronization failure during relate() and import()
1098
- if (isNew && metadata.verb) {
1099
- this.incrementVerbCount(metadata.verb);
1298
+ if (isNew) {
1299
+ this.incrementVerbCount(verbType);
1100
1300
  // Persist counts asynchronously (fire and forget)
1101
1301
  this.scheduleCountPersist().catch(() => {
1102
1302
  // Ignore persist errors - will retry on next operation
@@ -1105,21 +1305,497 @@ export class BaseStorage extends BaseStorageAdapter {
1105
1305
  }
1106
1306
  /**
1107
1307
  * Get verb metadata from storage (v4.0.0: now typed)
1108
- * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
1308
+ * v5.4.0: Uses type-first paths (must match saveVerbMetadata_internal)
1109
1309
  */
1110
1310
  async getVerbMetadata(id) {
1111
1311
  await this.ensureInitialized();
1112
- const keyInfo = this.analyzeKey(id, 'verb-metadata');
1113
- return this.readWithInheritance(keyInfo.fullPath);
1312
+ // v5.4.0: Check type cache first (populated during save)
1313
+ const cachedType = this.verbTypeCache.get(id);
1314
+ if (cachedType) {
1315
+ const path = getVerbMetadataPath(cachedType, id);
1316
+ return this.readWithInheritance(path);
1317
+ }
1318
+ // Fallback: search across all types (expensive but necessary if cache miss)
1319
+ for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1320
+ const type = TypeUtils.getVerbFromIndex(i);
1321
+ const path = getVerbMetadataPath(type, id);
1322
+ try {
1323
+ const metadata = await this.readWithInheritance(path);
1324
+ if (metadata) {
1325
+ // Cache the type for next time
1326
+ this.verbTypeCache.set(id, type);
1327
+ return metadata;
1328
+ }
1329
+ }
1330
+ catch (error) {
1331
+ // Not in this type, continue searching
1332
+ }
1333
+ }
1334
+ return null;
1114
1335
  }
1115
1336
  /**
1116
1337
  * Delete verb metadata from storage
1117
- * Uses routing logic to handle both UUIDs (sharded) and system keys (unsharded)
1338
+ * v5.4.0: Uses type-first paths (must match saveVerbMetadata_internal)
1118
1339
  */
1119
1340
  async deleteVerbMetadata(id) {
1120
1341
  await this.ensureInitialized();
1121
- const keyInfo = this.analyzeKey(id, 'verb-metadata');
1122
- return this.deleteObjectFromBranch(keyInfo.fullPath);
1342
+ // v5.4.0: Use cached type for path
1343
+ const cachedType = this.verbTypeCache.get(id);
1344
+ if (cachedType) {
1345
+ const path = getVerbMetadataPath(cachedType, id);
1346
+ await this.deleteObjectFromBranch(path);
1347
+ // Remove from cache after deletion
1348
+ this.verbTypeCache.delete(id);
1349
+ return;
1350
+ }
1351
+ // If not in cache, search all types to find and delete
1352
+ for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1353
+ const type = TypeUtils.getVerbFromIndex(i);
1354
+ const path = getVerbMetadataPath(type, id);
1355
+ try {
1356
+ // Check if exists before deleting
1357
+ const exists = await this.readWithInheritance(path);
1358
+ if (exists) {
1359
+ await this.deleteObjectFromBranch(path);
1360
+ return;
1361
+ }
1362
+ }
1363
+ catch (error) {
1364
+ // Not in this type, continue searching
1365
+ }
1366
+ }
1367
+ }
1368
+ // ============================================================================
1369
+ // TYPE-FIRST HELPER METHODS (v5.4.0)
1370
+ // Built-in type-aware support for all storage adapters
1371
+ // ============================================================================
1372
+ /**
1373
+ * Load type statistics from storage
1374
+ * Rebuilds type counts if needed (called during init)
1375
+ */
1376
+ async loadTypeStatistics() {
1377
+ try {
1378
+ const stats = await this.readObjectFromPath(`${SYSTEM_DIR}/type-statistics.json`);
1379
+ if (stats) {
1380
+ // Restore counts from saved statistics
1381
+ if (stats.nounCounts && stats.nounCounts.length === NOUN_TYPE_COUNT) {
1382
+ this.nounCountsByType = new Uint32Array(stats.nounCounts);
1383
+ }
1384
+ if (stats.verbCounts && stats.verbCounts.length === VERB_TYPE_COUNT) {
1385
+ this.verbCountsByType = new Uint32Array(stats.verbCounts);
1386
+ }
1387
+ }
1388
+ }
1389
+ catch (error) {
1390
+ // No existing type statistics, starting fresh
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Save type statistics to storage
1395
+ * Periodically called when counts are updated
1396
+ */
1397
+ async saveTypeStatistics() {
1398
+ const stats = {
1399
+ nounCounts: Array.from(this.nounCountsByType),
1400
+ verbCounts: Array.from(this.verbCountsByType),
1401
+ updatedAt: Date.now()
1402
+ };
1403
+ await this.writeObjectToPath(`${SYSTEM_DIR}/type-statistics.json`, stats);
1404
+ }
1405
+ /**
1406
+ * Get noun type from cache or metadata
1407
+ * Relies on nounTypeCache populated during metadata saves
1408
+ */
1409
+ getNounType(noun) {
1410
+ // Check cache (populated when metadata is saved)
1411
+ const cached = this.nounTypeCache.get(noun.id);
1412
+ if (cached) {
1413
+ return cached;
1414
+ }
1415
+ // Default to 'thing' if unknown
1416
+ // This should only happen if saveNoun_internal is called before saveNounMetadata
1417
+ console.warn(`[BaseStorage] Unknown noun type for ${noun.id}, defaulting to 'thing'`);
1418
+ return 'thing';
1419
+ }
1420
+ /**
1421
+ * Get verb type from verb object
1422
+ * Verb type is a required field in HNSWVerb
1423
+ */
1424
+ getVerbType(verb) {
1425
+ // v3.50.1+: verb is a required field in HNSWVerb
1426
+ if ('verb' in verb && verb.verb) {
1427
+ return verb.verb;
1428
+ }
1429
+ // Fallback for GraphVerb (type alias)
1430
+ if ('type' in verb && verb.type) {
1431
+ return verb.type;
1432
+ }
1433
+ // This should never happen with current data
1434
+ console.warn(`[BaseStorage] Verb missing type field for ${verb.id}, defaulting to 'relatedTo'`);
1435
+ return 'relatedTo';
1436
+ }
1437
+ // ============================================================================
1438
+ // ABSTRACT METHOD IMPLEMENTATIONS (v5.4.0)
1439
+ // Converted from abstract to concrete - all adapters now have built-in type-aware
1440
+ // ============================================================================
1441
+ /**
1442
+ * Save a noun to storage (type-first path)
1443
+ */
1444
+ async saveNoun_internal(noun) {
1445
+ const type = this.getNounType(noun);
1446
+ const path = getNounVectorPath(type, noun.id);
1447
+ // Update type tracking
1448
+ const typeIndex = TypeUtils.getNounIndex(type);
1449
+ this.nounCountsByType[typeIndex]++;
1450
+ this.nounTypeCache.set(noun.id, type);
1451
+ // COW-aware write (v5.0.1): Use COW helper for branch isolation
1452
+ await this.writeObjectToBranch(path, noun);
1453
+ // Periodically save statistics (every 100 saves)
1454
+ if (this.nounCountsByType[typeIndex] % 100 === 0) {
1455
+ await this.saveTypeStatistics();
1456
+ }
1457
+ }
1458
+ /**
1459
+ * Get a noun from storage (type-first path)
1460
+ */
1461
+ async getNoun_internal(id) {
1462
+ // Try cache first
1463
+ const cachedType = this.nounTypeCache.get(id);
1464
+ if (cachedType) {
1465
+ const path = getNounVectorPath(cachedType, id);
1466
+ // COW-aware read (v5.0.1): Use COW helper for branch isolation
1467
+ return await this.readWithInheritance(path);
1468
+ }
1469
+ // Need to search across all types (expensive, but cached after first access)
1470
+ for (let i = 0; i < NOUN_TYPE_COUNT; i++) {
1471
+ const type = TypeUtils.getNounFromIndex(i);
1472
+ const path = getNounVectorPath(type, id);
1473
+ try {
1474
+ // COW-aware read (v5.0.1): Use COW helper for branch isolation
1475
+ const noun = await this.readWithInheritance(path);
1476
+ if (noun) {
1477
+ // Cache the type for next time
1478
+ this.nounTypeCache.set(id, type);
1479
+ return noun;
1480
+ }
1481
+ }
1482
+ catch (error) {
1483
+ // Not in this type, continue searching
1484
+ }
1485
+ }
1486
+ return null;
1487
+ }
1488
+ /**
1489
+ * Get nouns by noun type (O(1) with type-first paths!)
1490
+ */
1491
+ async getNounsByNounType_internal(nounType) {
1492
+ const type = nounType;
1493
+ const prefix = `entities/nouns/${type}/vectors/`;
1494
+ // COW-aware list (v5.0.1): Use COW helper for branch isolation
1495
+ const paths = await this.listObjectsInBranch(prefix);
1496
+ // Load all nouns of this type
1497
+ const nouns = [];
1498
+ for (const path of paths) {
1499
+ try {
1500
+ // COW-aware read (v5.0.1): Use COW helper for branch isolation
1501
+ const noun = await this.readWithInheritance(path);
1502
+ if (noun) {
1503
+ nouns.push(noun);
1504
+ // Cache the type
1505
+ this.nounTypeCache.set(noun.id, type);
1506
+ }
1507
+ }
1508
+ catch (error) {
1509
+ console.warn(`[BaseStorage] Failed to load noun from ${path}:`, error);
1510
+ }
1511
+ }
1512
+ return nouns;
1513
+ }
1514
+ /**
1515
+ * Delete a noun from storage (type-first path)
1516
+ */
1517
+ async deleteNoun_internal(id) {
1518
+ // Try cache first
1519
+ const cachedType = this.nounTypeCache.get(id);
1520
+ if (cachedType) {
1521
+ const path = getNounVectorPath(cachedType, id);
1522
+ // COW-aware delete (v5.0.1): Use COW helper for branch isolation
1523
+ await this.deleteObjectFromBranch(path);
1524
+ // Update counts
1525
+ const typeIndex = TypeUtils.getNounIndex(cachedType);
1526
+ if (this.nounCountsByType[typeIndex] > 0) {
1527
+ this.nounCountsByType[typeIndex]--;
1528
+ }
1529
+ this.nounTypeCache.delete(id);
1530
+ return;
1531
+ }
1532
+ // Search across all types
1533
+ for (let i = 0; i < NOUN_TYPE_COUNT; i++) {
1534
+ const type = TypeUtils.getNounFromIndex(i);
1535
+ const path = getNounVectorPath(type, id);
1536
+ try {
1537
+ // COW-aware delete (v5.0.1): Use COW helper for branch isolation
1538
+ await this.deleteObjectFromBranch(path);
1539
+ // Update counts
1540
+ if (this.nounCountsByType[i] > 0) {
1541
+ this.nounCountsByType[i]--;
1542
+ }
1543
+ this.nounTypeCache.delete(id);
1544
+ return;
1545
+ }
1546
+ catch (error) {
1547
+ // Not in this type, continue
1548
+ }
1549
+ }
1550
+ }
1551
+ /**
1552
+ * Save a verb to storage (type-first path)
1553
+ */
1554
+ async saveVerb_internal(verb) {
1555
+ // Type is now a first-class field in HNSWVerb - no caching needed!
1556
+ const type = verb.verb;
1557
+ const path = getVerbVectorPath(type, verb.id);
1558
+ // Update type tracking
1559
+ const typeIndex = TypeUtils.getVerbIndex(type);
1560
+ this.verbCountsByType[typeIndex]++;
1561
+ this.verbTypeCache.set(verb.id, type);
1562
+ // COW-aware write (v5.0.1): Use COW helper for branch isolation
1563
+ await this.writeObjectToBranch(path, verb);
1564
+ // Periodically save statistics
1565
+ if (this.verbCountsByType[typeIndex] % 100 === 0) {
1566
+ await this.saveTypeStatistics();
1567
+ }
1568
+ }
1569
+ /**
1570
+ * Get a verb from storage (type-first path)
1571
+ */
1572
+ async getVerb_internal(id) {
1573
+ // Try cache first for O(1) retrieval
1574
+ const cachedType = this.verbTypeCache.get(id);
1575
+ if (cachedType) {
1576
+ const path = getVerbVectorPath(cachedType, id);
1577
+ // COW-aware read (v5.0.1): Use COW helper for branch isolation
1578
+ const verb = await this.readWithInheritance(path);
1579
+ return verb;
1580
+ }
1581
+ // Search across all types (only on first access)
1582
+ for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1583
+ const type = TypeUtils.getVerbFromIndex(i);
1584
+ const path = getVerbVectorPath(type, id);
1585
+ try {
1586
+ // COW-aware read (v5.0.1): Use COW helper for branch isolation
1587
+ const verb = await this.readWithInheritance(path);
1588
+ if (verb) {
1589
+ // Cache the type for next time (read from verb.verb field)
1590
+ this.verbTypeCache.set(id, verb.verb);
1591
+ return verb;
1592
+ }
1593
+ }
1594
+ catch (error) {
1595
+ // Not in this type, continue
1596
+ }
1597
+ }
1598
+ return null;
1599
+ }
1600
+ /**
1601
+ * Get verbs by source (COW-aware implementation)
1602
+ * v5.4.0: Fixed to directly list verb files instead of directories
1603
+ */
1604
+ async getVerbsBySource_internal(sourceId) {
1605
+ // v5.4.0: Type-first implementation - scan across all verb types
1606
+ // COW-aware: uses readWithInheritance for each verb
1607
+ await this.ensureInitialized();
1608
+ const results = [];
1609
+ // Iterate through all verb types
1610
+ for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1611
+ const type = TypeUtils.getVerbFromIndex(i);
1612
+ const typeDir = `entities/verbs/${type}/vectors`;
1613
+ try {
1614
+ // v5.4.0 FIX: List all verb files directly (not shard directories)
1615
+ // listObjectsInBranch returns full paths to .json files, not directories
1616
+ const verbFiles = await this.listObjectsInBranch(typeDir);
1617
+ for (const verbPath of verbFiles) {
1618
+ // Skip if not a .json file
1619
+ if (!verbPath.endsWith('.json'))
1620
+ continue;
1621
+ try {
1622
+ const verb = await this.readWithInheritance(verbPath);
1623
+ if (verb && verb.sourceId === sourceId) {
1624
+ // v5.4.0: Use proper path helper instead of string replacement
1625
+ const metadataPath = getVerbMetadataPath(type, verb.id);
1626
+ const metadata = await this.readWithInheritance(metadataPath);
1627
+ // v5.4.0: Extract standard fields from metadata to top-level (like nouns)
1628
+ results.push({
1629
+ ...verb,
1630
+ weight: metadata?.weight,
1631
+ confidence: metadata?.confidence,
1632
+ createdAt: metadata?.createdAt
1633
+ ? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
1634
+ : Date.now(),
1635
+ updatedAt: metadata?.updatedAt
1636
+ ? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
1637
+ : Date.now(),
1638
+ service: metadata?.service,
1639
+ createdBy: metadata?.createdBy,
1640
+ metadata: metadata || {}
1641
+ });
1642
+ }
1643
+ }
1644
+ catch (error) {
1645
+ // Skip verbs that fail to load
1646
+ }
1647
+ }
1648
+ }
1649
+ catch (error) {
1650
+ // Skip types that have no data
1651
+ }
1652
+ }
1653
+ return results;
1654
+ }
1655
+ /**
1656
+ * Get verbs by target (COW-aware implementation)
1657
+ * v5.4.0: Fixed to directly list verb files instead of directories
1658
+ */
1659
+ async getVerbsByTarget_internal(targetId) {
1660
+ // v5.4.0: Type-first implementation - scan across all verb types
1661
+ // COW-aware: uses readWithInheritance for each verb
1662
+ await this.ensureInitialized();
1663
+ const results = [];
1664
+ // Iterate through all verb types
1665
+ for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1666
+ const type = TypeUtils.getVerbFromIndex(i);
1667
+ const typeDir = `entities/verbs/${type}/vectors`;
1668
+ try {
1669
+ // v5.4.0 FIX: List all verb files directly (not shard directories)
1670
+ // listObjectsInBranch returns full paths to .json files, not directories
1671
+ const verbFiles = await this.listObjectsInBranch(typeDir);
1672
+ for (const verbPath of verbFiles) {
1673
+ // Skip if not a .json file
1674
+ if (!verbPath.endsWith('.json'))
1675
+ continue;
1676
+ try {
1677
+ const verb = await this.readWithInheritance(verbPath);
1678
+ if (verb && verb.targetId === targetId) {
1679
+ // v5.4.0: Use proper path helper instead of string replacement
1680
+ const metadataPath = getVerbMetadataPath(type, verb.id);
1681
+ const metadata = await this.readWithInheritance(metadataPath);
1682
+ // v5.4.0: Extract standard fields from metadata to top-level (like nouns)
1683
+ results.push({
1684
+ ...verb,
1685
+ weight: metadata?.weight,
1686
+ confidence: metadata?.confidence,
1687
+ createdAt: metadata?.createdAt
1688
+ ? (typeof metadata.createdAt === 'number' ? metadata.createdAt : metadata.createdAt.seconds * 1000)
1689
+ : Date.now(),
1690
+ updatedAt: metadata?.updatedAt
1691
+ ? (typeof metadata.updatedAt === 'number' ? metadata.updatedAt : metadata.updatedAt.seconds * 1000)
1692
+ : Date.now(),
1693
+ service: metadata?.service,
1694
+ createdBy: metadata?.createdBy,
1695
+ metadata: metadata || {}
1696
+ });
1697
+ }
1698
+ }
1699
+ catch (error) {
1700
+ // Skip verbs that fail to load
1701
+ }
1702
+ }
1703
+ }
1704
+ catch (error) {
1705
+ // Skip types that have no data
1706
+ }
1707
+ }
1708
+ return results;
1709
+ }
1710
+ /**
1711
+ * Get verbs by type (O(1) with type-first paths!)
1712
+ */
1713
+ async getVerbsByType_internal(verbType) {
1714
+ const type = verbType;
1715
+ const prefix = `entities/verbs/${type}/vectors/`;
1716
+ // COW-aware list (v5.0.1): Use COW helper for branch isolation
1717
+ const paths = await this.listObjectsInBranch(prefix);
1718
+ const verbs = [];
1719
+ for (const path of paths) {
1720
+ try {
1721
+ // COW-aware read (v5.0.1): Use COW helper for branch isolation
1722
+ const hnswVerb = await this.readWithInheritance(path);
1723
+ if (!hnswVerb)
1724
+ continue;
1725
+ // Cache type from HNSWVerb for future O(1) retrievals
1726
+ this.verbTypeCache.set(hnswVerb.id, hnswVerb.verb);
1727
+ // Load metadata separately (optional in v4.0.0!)
1728
+ // FIX: Don't skip verbs without metadata - metadata is optional!
1729
+ const metadata = await this.getVerbMetadata(hnswVerb.id);
1730
+ // Create HNSWVerbWithMetadata (verbs don't have level field)
1731
+ // Convert connections from plain object to Map<number, Set<string>>
1732
+ const connectionsMap = new Map();
1733
+ if (hnswVerb.connections && typeof hnswVerb.connections === 'object') {
1734
+ for (const [level, ids] of Object.entries(hnswVerb.connections)) {
1735
+ connectionsMap.set(Number(level), new Set(ids));
1736
+ }
1737
+ }
1738
+ // v4.8.0: Extract standard fields from metadata to top-level
1739
+ const metadataObj = (metadata || {});
1740
+ const { createdAt, updatedAt, confidence, weight, service, data, createdBy, ...customMetadata } = metadataObj;
1741
+ const verbWithMetadata = {
1742
+ id: hnswVerb.id,
1743
+ vector: [...hnswVerb.vector],
1744
+ connections: connectionsMap,
1745
+ verb: hnswVerb.verb,
1746
+ sourceId: hnswVerb.sourceId,
1747
+ targetId: hnswVerb.targetId,
1748
+ createdAt: createdAt || Date.now(),
1749
+ updatedAt: updatedAt || Date.now(),
1750
+ confidence: confidence,
1751
+ weight: weight,
1752
+ service: service,
1753
+ data: data,
1754
+ createdBy,
1755
+ metadata: customMetadata
1756
+ };
1757
+ verbs.push(verbWithMetadata);
1758
+ }
1759
+ catch (error) {
1760
+ console.warn(`[BaseStorage] Failed to load verb from ${path}:`, error);
1761
+ }
1762
+ }
1763
+ return verbs;
1764
+ }
1765
+ /**
1766
+ * Delete a verb from storage (type-first path)
1767
+ */
1768
+ async deleteVerb_internal(id) {
1769
+ // Try cache first
1770
+ const cachedType = this.verbTypeCache.get(id);
1771
+ if (cachedType) {
1772
+ const path = getVerbVectorPath(cachedType, id);
1773
+ // COW-aware delete (v5.0.1): Use COW helper for branch isolation
1774
+ await this.deleteObjectFromBranch(path);
1775
+ const typeIndex = TypeUtils.getVerbIndex(cachedType);
1776
+ if (this.verbCountsByType[typeIndex] > 0) {
1777
+ this.verbCountsByType[typeIndex]--;
1778
+ }
1779
+ this.verbTypeCache.delete(id);
1780
+ return;
1781
+ }
1782
+ // Search across all types
1783
+ for (let i = 0; i < VERB_TYPE_COUNT; i++) {
1784
+ const type = TypeUtils.getVerbFromIndex(i);
1785
+ const path = getVerbVectorPath(type, id);
1786
+ try {
1787
+ // COW-aware delete (v5.0.1): Use COW helper for branch isolation
1788
+ await this.deleteObjectFromBranch(path);
1789
+ if (this.verbCountsByType[i] > 0) {
1790
+ this.verbCountsByType[i]--;
1791
+ }
1792
+ this.verbTypeCache.delete(id);
1793
+ return;
1794
+ }
1795
+ catch (error) {
1796
+ // Continue
1797
+ }
1798
+ }
1123
1799
  }
1124
1800
  /**
1125
1801
  * Helper method to convert a Map to a plain object for serialization