@utaba/deep-memory 0.2.0 → 0.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.
package/dist/index.cjs CHANGED
@@ -332,6 +332,12 @@ var EntityManager = class {
332
332
  this.storage = storage;
333
333
  this.embedding = embedding;
334
334
  }
335
+ repositoryId;
336
+ vocabularyEngine;
337
+ provenanceTracker;
338
+ eventBus;
339
+ storage;
340
+ embedding;
335
341
  /** Create one or more entities with vocabulary validation, ID generation, provenance, and events */
336
342
  async create(inputs) {
337
343
  const results = [];
@@ -695,7 +701,7 @@ function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
695
701
  }
696
702
  return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
697
703
  }
698
- function normaliseTypeName(name) {
704
+ function normalizeTypeName(name) {
699
705
  return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
700
706
  }
701
707
  function toScreamingSnakeCase(name) {
@@ -711,6 +717,11 @@ var RelationshipManager = class {
711
717
  this.eventBus = eventBus;
712
718
  this.storage = storage;
713
719
  }
720
+ repositoryId;
721
+ vocabularyEngine;
722
+ provenanceTracker;
723
+ eventBus;
724
+ storage;
714
725
  /** Create one or more relationships with vocabulary validation, provenance, and events */
715
726
  async create(inputs) {
716
727
  const results = [];
@@ -850,6 +861,7 @@ var DEFAULT_MAX_LIMIT = 200;
850
861
  var DEFAULT_FALLBACK_MAX_DEPTH = 10;
851
862
  function validateTraversalSpec(spec, vocabulary, capabilities) {
852
863
  const errors = [];
864
+ const steps = spec.steps ?? [];
853
865
  if (!spec.start) {
854
866
  errors.push("start is required");
855
867
  } else {
@@ -863,37 +875,45 @@ function validateTraversalSpec(spec, vocabulary, capabilities) {
863
875
  errors.push("start.entityType without entityId requires limit on the spec to prevent full type scans");
864
876
  }
865
877
  }
866
- if (!spec.steps || spec.steps.length === 0) {
867
- errors.push("steps must contain at least one step");
868
- }
869
878
  const maxSteps = capabilities?.maxTraversalDepth ?? DEFAULT_MAX_STEPS;
870
- if (spec.steps && spec.steps.length > maxSteps) {
871
- errors.push(`steps length ${spec.steps.length} exceeds maximum ${maxSteps}`);
872
- }
873
- if (spec.steps) {
874
- for (let i = 0; i < spec.steps.length; i++) {
875
- const step = spec.steps[i];
876
- if (!step.direction || !["out", "in", "both"].includes(step.direction)) {
877
- errors.push(`steps[${i}].direction must be 'out', 'in', or 'both'`);
878
- }
879
- if (step.repeat) {
880
- if (step.repeat.maxDepth === void 0 || step.repeat.maxDepth <= 0) {
881
- errors.push(`steps[${i}].repeat.maxDepth must be a positive number`);
882
- }
879
+ if (steps.length > maxSteps) {
880
+ errors.push(`steps length ${steps.length} exceeds maximum ${maxSteps}`);
881
+ }
882
+ for (let i = 0; i < steps.length; i++) {
883
+ const step = steps[i];
884
+ if (!step.direction || !["out", "in", "both"].includes(step.direction)) {
885
+ errors.push(`steps[${i}].direction must be 'out', 'in', or 'both'`);
886
+ }
887
+ if (step.repeat) {
888
+ if (step.repeat.maxDepth === void 0 || step.repeat.maxDepth <= 0) {
889
+ errors.push(`steps[${i}].repeat.maxDepth must be a positive number`);
883
890
  }
884
891
  }
892
+ }
893
+ if (steps.length > 0) {
885
894
  const maxProviderDepth = capabilities?.maxTraversalDepth ?? DEFAULT_FALLBACK_MAX_DEPTH;
886
895
  let totalDepth = 0;
887
- for (const step of spec.steps) {
896
+ for (const step of steps) {
888
897
  totalDepth += step.repeat ? step.repeat.maxDepth : 1;
889
898
  }
890
899
  if (totalDepth > maxProviderDepth) {
891
900
  errors.push(`total potential depth ${totalDepth} exceeds provider maximum ${maxProviderDepth}`);
892
901
  }
893
902
  }
903
+ if (spec.returnMode === "path" && steps.length === 0) {
904
+ errors.push("returnMode 'path' requires at least one step");
905
+ }
894
906
  if (spec.returnMode && !["terminal", "path", "all"].includes(spec.returnMode)) {
895
907
  errors.push(`returnMode must be 'terminal', 'path', or 'all'`);
896
908
  }
909
+ if (spec.projection) {
910
+ if (!spec.projection.properties || spec.projection.properties.length === 0) {
911
+ errors.push("projection.properties must contain at least one property name");
912
+ }
913
+ if (spec.projection.mode && !["count", "values"].includes(spec.projection.mode)) {
914
+ errors.push("projection.mode must be 'count' or 'values'");
915
+ }
916
+ }
897
917
  if (spec.limit !== void 0) {
898
918
  if (spec.limit < 1 || spec.limit > DEFAULT_MAX_LIMIT) {
899
919
  errors.push(`limit must be between 1 and ${DEFAULT_MAX_LIMIT}`);
@@ -902,15 +922,15 @@ function validateTraversalSpec(spec, vocabulary, capabilities) {
902
922
  if (spec.offset !== void 0 && spec.offset < 0) {
903
923
  errors.push("offset must be >= 0");
904
924
  }
905
- if (vocabulary && spec.steps) {
925
+ if (vocabulary) {
906
926
  const entityTypeSet = new Set(vocabulary.entityTypes.map((et) => et.type));
907
927
  const relationshipTypeSet = new Set(vocabulary.relationshipTypes.map((rt) => rt.type));
908
928
  const unknownTypes = [];
909
929
  if (spec.start?.entityType && !entityTypeSet.has(spec.start.entityType)) {
910
930
  unknownTypes.push(`entity type "${spec.start.entityType}"`);
911
931
  }
912
- for (let i = 0; i < spec.steps.length; i++) {
913
- const step = spec.steps[i];
932
+ for (let i = 0; i < steps.length; i++) {
933
+ const step = steps[i];
914
934
  if (step.relationshipTypes) {
915
935
  for (const rt of step.relationshipTypes) {
916
936
  if (!relationshipTypeSet.has(rt)) {
@@ -964,7 +984,8 @@ var GremlinCompiler = class {
964
984
  parts.push(compilePropertyFilter(f, nextParam));
965
985
  }
966
986
  }
967
- for (const step of spec.steps) {
987
+ const steps = spec.steps ?? [];
988
+ for (const step of steps) {
968
989
  if (step.repeat) {
969
990
  parts.push(compileRepeatStep(step, nextParam));
970
991
  estimatedFanOut *= step.repeat.maxDepth * DEFAULT_ESTIMATED_FANOUT_PER_HOP;
@@ -1146,8 +1167,9 @@ var CypherCompiler = class {
1146
1167
  }
1147
1168
  }
1148
1169
  let lastNode = startNode;
1149
- for (let i = 0; i < spec.steps.length; i++) {
1150
- const step = spec.steps[i];
1170
+ const steps = spec.steps ?? [];
1171
+ for (let i = 0; i < steps.length; i++) {
1172
+ const step = steps[i];
1151
1173
  const targetNode = `n${nodeIndex++}`;
1152
1174
  const relAlias = `r${i}`;
1153
1175
  const relTypes = step.relationshipTypes?.length ? step.relationshipTypes.join("|") : "";
@@ -1189,7 +1211,7 @@ WHERE ${whereClauses.join(" AND ")}` : "";
1189
1211
  let returnClause;
1190
1212
  if (spec.returnMode === "path") {
1191
1213
  const allNodes = Array.from({ length: nodeIndex }, (_, i) => `n${i}`);
1192
- const allRels = spec.steps.map((_, i) => `r${i}`);
1214
+ const allRels = steps.map((_, i) => `r${i}`);
1193
1215
  returnClause = `RETURN ${[...allNodes, ...allRels].join(", ")}`;
1194
1216
  } else if (spec.returnMode === "all") {
1195
1217
  const allNodes = Array.from({ length: nodeIndex }, (_, i) => `n${i}`);
@@ -1270,6 +1292,7 @@ function matchesSingleFilter(properties, filter) {
1270
1292
 
1271
1293
  // src/relationships/FallbackTraversalExecutor.ts
1272
1294
  var MAX_STORAGE_CALLS = 500;
1295
+ var MAX_ENTITY_SCAN = 1e4;
1273
1296
  var REL_FETCH_LIMIT = 1e3;
1274
1297
  async function executeFallbackTraversal(repositoryId, storage, spec) {
1275
1298
  const startTime = Date.now();
@@ -1279,6 +1302,9 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1279
1302
  throw new Error(`Fallback traversal exceeded ${MAX_STORAGE_CALLS} storage calls. Consider using a GraphTraversalProvider for large traversals.`);
1280
1303
  }
1281
1304
  };
1305
+ const isVertexQuery = !spec.steps || spec.steps.length === 0;
1306
+ const needsFullScan = isVertexQuery || spec.projection !== void 0;
1307
+ const fetchLimit = needsFullScan ? MAX_ENTITY_SCAN : spec.limit ?? 50;
1282
1308
  let frontier = [];
1283
1309
  if (spec.start.entityId) {
1284
1310
  const entity = await resolveEntity(repositoryId, storage, spec.start.entityId);
@@ -1291,7 +1317,7 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1291
1317
  } else if (spec.start.entityType) {
1292
1318
  const result = await storage.findEntities(repositoryId, {
1293
1319
  entityTypes: [spec.start.entityType],
1294
- limit: spec.limit ?? 50,
1320
+ limit: fetchLimit,
1295
1321
  offset: 0
1296
1322
  });
1297
1323
  storageCalls++;
@@ -1302,7 +1328,7 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1302
1328
  }));
1303
1329
  } else if (spec.start.filter && spec.start.filter.length > 0) {
1304
1330
  const result = await storage.findEntities(repositoryId, {
1305
- limit: spec.limit ?? 50,
1331
+ limit: fetchLimit,
1306
1332
  offset: 0
1307
1333
  });
1308
1334
  storageCalls++;
@@ -1317,8 +1343,9 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1317
1343
  (entry) => matchesPropertyFilters(entry.entity.properties, spec.start.filter)
1318
1344
  );
1319
1345
  }
1346
+ const steps = spec.steps ?? [];
1320
1347
  const allCollected = spec.returnMode === "all" ? [...frontier] : [];
1321
- for (const step of spec.steps) {
1348
+ for (const step of steps) {
1322
1349
  checkCallLimit();
1323
1350
  if (step.repeat) {
1324
1351
  const result = await executeRepeatStep(
@@ -1371,10 +1398,89 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1371
1398
  }
1372
1399
  const total = resultEntries.length;
1373
1400
  const paged = resultEntries.slice(offset, offset + limit);
1374
- const entities = paged.map((entry) => projectEntity(entry.entity, detailLevel));
1401
+ const suppressEntities = spec.projection !== void 0 && spec.projection.includeEntities !== true;
1402
+ let entities;
1403
+ if (suppressEntities) {
1404
+ entities = [];
1405
+ } else {
1406
+ entities = paged.map((entry) => {
1407
+ const projected = projectEntity(entry.entity, detailLevel);
1408
+ if (!spec.includeProvenance) {
1409
+ delete projected["provenance"];
1410
+ }
1411
+ return projected;
1412
+ });
1413
+ if (spec.includeRelationshipSummary && entities.length > 0) {
1414
+ const summaryResults = await Promise.all(
1415
+ paged.map(async (entry) => {
1416
+ storageCalls++;
1417
+ const result = await storage.getEntityRelationships(repositoryId, entry.entity.id, {
1418
+ direction: "both",
1419
+ limit: 1e4
1420
+ });
1421
+ const summary = { outbound: {}, inbound: {} };
1422
+ for (const rel of result.items) {
1423
+ if (rel.sourceEntityId === entry.entity.id) {
1424
+ summary.outbound[rel.relationshipType] = (summary.outbound[rel.relationshipType] ?? 0) + 1;
1425
+ }
1426
+ if (rel.targetEntityId === entry.entity.id) {
1427
+ summary.inbound[rel.relationshipType] = (summary.inbound[rel.relationshipType] ?? 0) + 1;
1428
+ }
1429
+ }
1430
+ return summary;
1431
+ })
1432
+ );
1433
+ entities = entities.map((e, i) => ({ ...e, relationshipSummary: summaryResults[i] }));
1434
+ }
1435
+ }
1436
+ let aggregations;
1437
+ if (spec.projection) {
1438
+ const propNames = spec.projection.properties;
1439
+ const mode = spec.projection.mode ?? "values";
1440
+ const distinct = spec.projection.distinct ?? false;
1441
+ const sourceEntries = dedup ? (() => {
1442
+ const seen = /* @__PURE__ */ new Set();
1443
+ return (spec.returnMode === "all" ? allCollected : frontier).filter((e) => {
1444
+ if (seen.has(e.entity.id)) return false;
1445
+ seen.add(e.entity.id);
1446
+ return true;
1447
+ });
1448
+ })() : spec.returnMode === "all" ? allCollected : frontier;
1449
+ if (mode === "count" || distinct) {
1450
+ const groups = /* @__PURE__ */ new Map();
1451
+ for (const entry of sourceEntries) {
1452
+ const vals = {};
1453
+ for (const prop of propNames) {
1454
+ vals[prop] = entry.entity.properties[prop] ?? null;
1455
+ }
1456
+ const key = JSON.stringify(vals);
1457
+ const existing = groups.get(key);
1458
+ if (existing) {
1459
+ existing.count++;
1460
+ } else {
1461
+ groups.set(key, { values: vals, count: 1 });
1462
+ }
1463
+ }
1464
+ aggregations = Array.from(groups.values()).map((g) => ({
1465
+ values: g.values,
1466
+ ...mode === "count" ? { count: g.count } : {}
1467
+ }));
1468
+ if (mode === "count") {
1469
+ aggregations.sort((a, b) => (b.count ?? 0) - (a.count ?? 0));
1470
+ }
1471
+ } else {
1472
+ aggregations = sourceEntries.map((entry) => {
1473
+ const vals = {};
1474
+ for (const prop of propNames) {
1475
+ vals[prop] = entry.entity.properties[prop] ?? null;
1476
+ }
1477
+ return { values: vals };
1478
+ });
1479
+ }
1480
+ }
1375
1481
  let relationships;
1376
1482
  let paths;
1377
- if (spec.returnMode === "path" || spec.returnMode === "all") {
1483
+ if (!suppressEntities && (spec.returnMode === "path" || spec.returnMode === "all")) {
1378
1484
  const relMap = /* @__PURE__ */ new Map();
1379
1485
  for (const entry of paged) {
1380
1486
  for (const rel of entry.relationshipPath) {
@@ -1392,10 +1498,14 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1392
1498
  }
1393
1499
  relationships = Array.from(relMap.values());
1394
1500
  }
1395
- if (spec.returnMode === "path") {
1501
+ if (!suppressEntities && spec.returnMode === "path") {
1396
1502
  paths = paged.map((entry) => ({
1397
1503
  length: entry.entityPath.length - 1,
1398
- entities: [projectEntity(entry.entity, detailLevel)],
1504
+ entities: [(() => {
1505
+ const e = projectEntity(entry.entity, detailLevel);
1506
+ if (!spec.includeProvenance) delete e["provenance"];
1507
+ return e;
1508
+ })()],
1399
1509
  relationships: entry.relationshipPath.map((rel) => ({
1400
1510
  id: rel.id,
1401
1511
  type: rel.relationshipType,
@@ -1411,6 +1521,7 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1411
1521
  entities,
1412
1522
  relationships,
1413
1523
  paths,
1524
+ aggregations,
1414
1525
  total,
1415
1526
  returned: paged.length,
1416
1527
  hasMore: offset + limit < total,
@@ -1545,8 +1656,12 @@ var GraphTraversal = class {
1545
1656
  this.graphTraversalProvider = graphTraversalProvider;
1546
1657
  this.vocabularyEngine = vocabularyEngine;
1547
1658
  }
1548
- /** BFS neighbourhood exploration from a centre entity */
1549
- async exploreNeighbourhood(entityId, options) {
1659
+ repositoryId;
1660
+ storage;
1661
+ graphTraversalProvider;
1662
+ vocabularyEngine;
1663
+ /** BFS neighborhood exploration from a center entity */
1664
+ async exploreNeighborhood(entityId, options) {
1550
1665
  const storageOptions = {
1551
1666
  depth: options?.depth ?? 1,
1552
1667
  relationshipTypes: options?.relationshipTypes,
@@ -1556,13 +1671,13 @@ var GraphTraversal = class {
1556
1671
  offsetPerType: options?.offsetPerType ?? 0,
1557
1672
  relationshipPropertyFilters: options?.relationshipPropertyFilters
1558
1673
  };
1559
- const storageResult = await this.storage.exploreNeighbourhood(
1674
+ const storageResult = await this.storage.exploreNeighborhood(
1560
1675
  this.repositoryId,
1561
1676
  entityId,
1562
1677
  storageOptions
1563
1678
  );
1564
- const centreEntity = await this.storage.getEntity(this.repositoryId, entityId);
1565
- if (!centreEntity) {
1679
+ const centerEntity = await this.storage.getEntity(this.repositoryId, entityId);
1680
+ if (!centerEntity) {
1566
1681
  throw new EntityNotFoundError(entityId);
1567
1682
  }
1568
1683
  const detailLevel = options?.detailLevel ?? "summary";
@@ -1586,11 +1701,11 @@ var GraphTraversal = class {
1586
1701
  return layer;
1587
1702
  });
1588
1703
  return {
1589
- centre: {
1590
- id: centreEntity.id,
1591
- slug: centreEntity.slug,
1592
- entityType: centreEntity.entityType,
1593
- label: centreEntity.label
1704
+ center: {
1705
+ id: centerEntity.id,
1706
+ slug: centerEntity.slug,
1707
+ entityType: centerEntity.entityType,
1708
+ label: centerEntity.label
1594
1709
  },
1595
1710
  layers,
1596
1711
  statistics: {
@@ -2144,8 +2259,8 @@ var MemoryRepository = class {
2144
2259
  };
2145
2260
  }
2146
2261
  // ─── Graph Traversal ───────────────────────────────────────────────
2147
- async exploreNeighbourhood(entityId, options) {
2148
- return this.graphTraversal.exploreNeighbourhood(entityId, options);
2262
+ async exploreNeighborhood(entityId, options) {
2263
+ return this.graphTraversal.exploreNeighborhood(entityId, options);
2149
2264
  }
2150
2265
  async findPaths(sourceId, targetId, options) {
2151
2266
  return this.graphTraversal.findPaths(sourceId, targetId, options);
@@ -2175,7 +2290,7 @@ var MemoryRepository = class {
2175
2290
  storageOptions
2176
2291
  );
2177
2292
  const entity = await this.storage.getEntity(this.repositoryId, entityId);
2178
- const centreRef = {
2293
+ const centerRef = {
2179
2294
  id: entityId,
2180
2295
  slug: entity?.slug ?? entityId,
2181
2296
  label: entity?.label ?? entityId
@@ -2221,7 +2336,7 @@ var MemoryRepository = class {
2221
2336
  timestamp: e.timestamp,
2222
2337
  eventType: e.eventType,
2223
2338
  description: `${rel.type} relationship created ${direction} ${otherRef.label}`,
2224
- relatedEntities: [centreRef, otherRef],
2339
+ relatedEntities: [centerRef, otherRef],
2225
2340
  relationship: relDetail
2226
2341
  };
2227
2342
  }
@@ -2229,13 +2344,13 @@ var MemoryRepository = class {
2229
2344
  return {
2230
2345
  timestamp: e.timestamp,
2231
2346
  eventType: e.eventType,
2232
- description: `${centreRef.label} ${action}`,
2233
- relatedEntities: [centreRef]
2347
+ description: `${centerRef.label} ${action}`,
2348
+ relatedEntities: [centerRef]
2234
2349
  };
2235
2350
  });
2236
2351
  return {
2237
2352
  id: entityId,
2238
- slug: centreRef.slug,
2353
+ slug: centerRef.slug,
2239
2354
  totalEvents: storageResult.total,
2240
2355
  returned: events.length,
2241
2356
  events
@@ -2245,6 +2360,58 @@ var MemoryRepository = class {
2245
2360
  async getStats() {
2246
2361
  return this.storage.getRepositoryStats(this.repositoryId);
2247
2362
  }
2363
+ // ─── Bulk Operations ──────────────────────────────────────────────
2364
+ /** Delete all entities and relationships in this repository, preserving the repository and vocabulary */
2365
+ async deleteAllContents() {
2366
+ return this.storage.deleteAllContents(this.repositoryId);
2367
+ }
2368
+ /**
2369
+ * Returns a static markdown guide describing the recommended query strategy
2370
+ * for AI agents and other consumers of the repository.
2371
+ */
2372
+ getQueryGuide() {
2373
+ return `## Query Strategy Guide
2374
+
2375
+ ### Step 1 \u2014 Discover before you traverse
2376
+
2377
+ Before following relationships from an entity, check that it actually has relationships.
2378
+ \`memory_query_graph\` includes \`relationshipSummary\` on every returned entity by default
2379
+ (outbound and inbound counts by type). Inspect this before traversing \u2014 entities with
2380
+ zero outbound counts for the relationship type you need have no connections to follow.
2381
+ To skip summaries and reduce response size, set \`includeRelationshipSummary: false\`.
2382
+
2383
+ ### Step 2 \u2014 Use projection for aggregation
2384
+
2385
+ To understand what data exists (e.g. all distinct values of a property, or counts by
2386
+ category), use \`memory_query_graph\` with \`projection\` instead of fetching full entities:
2387
+ \`{ start: { entityType: "..." }, projection: { properties: ["propName"], distinct: true }, limit: 200 }\`
2388
+
2389
+ This returns only the aggregated values \u2014 no entity objects \u2014 keeping responses lightweight.
2390
+
2391
+ ### Step 3 \u2014 Traverse efficiently
2392
+
2393
+ - **Omit relationshipTypes** in a traversal step to follow all relationship types at once,
2394
+ rather than making separate calls per type.
2395
+ - **Use multi-step traversals** for multi-hop patterns. A two-step query
2396
+ (e.g. Equipment \u2192 Component \u2192 MaintenanceProcedure) is one call, not two sequential calls.
2397
+ - **Use returnMode "all"** when you need intermediate entities along the path, not just
2398
+ the terminal nodes.
2399
+
2400
+ ### Step 4 \u2014 Use semantic search for known-unknowns
2401
+
2402
+ \`memory_search_by_concept\` is best for finding specific entities when you know roughly
2403
+ what you're looking for but not the exact label or type. It is not efficient for broad
2404
+ discovery \u2014 use projection or find_entities for that.
2405
+
2406
+ ### Common anti-patterns to avoid
2407
+
2408
+ - **Don't traverse from entities with zero relationship counts.** The \`relationshipSummary\` on each entity tells you what is connected before you traverse.
2409
+ - **Don't split queries by relationship type.** One step with no type filter is better
2410
+ than three separate single-type calls.
2411
+ - **Don't use sequential single-hop traversals** when a multi-step query achieves the
2412
+ same result in one call.
2413
+ - **Don't fetch full entities when you only need property values.** Use projection.`;
2414
+ }
2248
2415
  // ─── Events ────────────────────────────────────────────────────────
2249
2416
  on(event, handler) {
2250
2417
  return this.eventBus.on(event, handler);
@@ -2266,14 +2433,14 @@ var SemanticDeduplicator = class {
2266
2433
  /**
2267
2434
  * Check whether a proposed type name is a duplicate of an existing type.
2268
2435
  * Uses embedding similarity if an EmbeddingProvider is available, otherwise falls back
2269
- * to Jaro-Winkler string similarity on normalised type names.
2436
+ * to Jaro-Winkler string similarity on normalized type names.
2270
2437
  */
2271
2438
  async checkDuplicate(proposedType, proposedDescription, existingTypes) {
2272
2439
  if (existingTypes.length === 0) {
2273
2440
  return { isDuplicate: false, matches: [] };
2274
2441
  }
2275
- const normProposed = normaliseTypeName(proposedType);
2276
- const exactMatch = existingTypes.find((t) => normaliseTypeName(t.type) === normProposed);
2442
+ const normProposed = normalizeTypeName(proposedType);
2443
+ const exactMatch = existingTypes.find((t) => normalizeTypeName(t.type) === normProposed);
2277
2444
  if (exactMatch) {
2278
2445
  return {
2279
2446
  isDuplicate: true,
@@ -2309,10 +2476,10 @@ var SemanticDeduplicator = class {
2309
2476
  };
2310
2477
  }
2311
2478
  checkWithStringSimilarity(proposedType, existingTypes) {
2312
- const normProposed = normaliseTypeName(proposedType);
2479
+ const normProposed = normalizeTypeName(proposedType);
2313
2480
  const matches = [];
2314
2481
  for (const existing of existingTypes) {
2315
- const normExisting = normaliseTypeName(existing.type);
2482
+ const normExisting = normalizeTypeName(existing.type);
2316
2483
  const similarity = jaroWinklerSimilarity(normProposed, normExisting);
2317
2484
  if (similarity >= this.threshold) {
2318
2485
  matches.push({
@@ -2473,7 +2640,7 @@ function canPropose(governanceConfig, _proposal) {
2473
2640
  case "locked":
2474
2641
  return {
2475
2642
  allowed: false,
2476
- reason: "Vocabulary is locked. Only organisation admins can modify the vocabulary via the admin API."
2643
+ reason: "Vocabulary is locked. Only organization admins can modify the vocabulary via the admin API."
2477
2644
  };
2478
2645
  case "managed":
2479
2646
  case "open":
@@ -2913,9 +3080,9 @@ function validateEntityUpdate(input, entityTypeDef) {
2913
3080
  return ok();
2914
3081
  }
2915
3082
  function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
2916
- const normalisedType = toScreamingSnakeCase(input.relationshipType);
3083
+ const normalizedType = toScreamingSnakeCase(input.relationshipType);
2917
3084
  const relTypeDef = vocabulary.relationshipTypes.find(
2918
- (rt) => rt.type === normalisedType
3085
+ (rt) => rt.type === normalizedType
2919
3086
  );
2920
3087
  if (!relTypeDef) {
2921
3088
  const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
@@ -3240,7 +3407,7 @@ var EventBus = class {
3240
3407
  };
3241
3408
 
3242
3409
  // src/portability/RepositoryExporter.ts
3243
- var LIBRARY_VERSION = true ? "0.2.0" : "0.1.0";
3410
+ var LIBRARY_VERSION = true ? "0.4.0" : "0.1.0";
3244
3411
  var RepositoryExporter = class {
3245
3412
  storage;
3246
3413
  provenance;
@@ -3456,7 +3623,7 @@ function diffArrays(from, to) {
3456
3623
  // src/portability/MigrationEngine.ts
3457
3624
  var MigrationEngine = class {
3458
3625
  /**
3459
- * Analyse and migrate vocabulary differences between source and target.
3626
+ * Analyze and migrate vocabulary differences between source and target.
3460
3627
  *
3461
3628
  * @param sourceVocabulary - vocabulary from the export archive
3462
3629
  * @param targetVocabulary - vocabulary in the target repository
@@ -3583,13 +3750,22 @@ function describeChange(change) {
3583
3750
  }
3584
3751
 
3585
3752
  // src/portability/RepositoryImporter.ts
3753
+ function estimateChunkCount(totalEntities, totalRelationships) {
3754
+ const ESTIMATED_CHUNK_SIZE = 100;
3755
+ return Math.max(
3756
+ 1,
3757
+ Math.ceil(totalEntities / ESTIMATED_CHUNK_SIZE) + Math.ceil(totalRelationships / ESTIMATED_CHUNK_SIZE)
3758
+ );
3759
+ }
3586
3760
  var RepositoryImporter = class {
3587
3761
  storage;
3588
3762
  actorId;
3763
+ onProgress;
3589
3764
  migrationEngine = new MigrationEngine();
3590
3765
  constructor(config) {
3591
3766
  this.storage = config.storage;
3592
3767
  this.actorId = config.actorId;
3768
+ this.onProgress = config.onProgress;
3593
3769
  }
3594
3770
  /**
3595
3771
  * Import an export archive according to the provided options.
@@ -3640,26 +3816,34 @@ var RepositoryImporter = class {
3640
3816
  return this.importStreamMerge(header, chunks, options, warnings);
3641
3817
  }
3642
3818
  }
3643
- /** Streaming create mode — set up repo then pipe chunks through importBulk */
3819
+ /** Streaming create mode — fast bulk inserts with no existence checks */
3644
3820
  async importStreamCreate(header, chunks, options, warnings) {
3645
3821
  const target = options.target;
3646
- const now = (/* @__PURE__ */ new Date()).toISOString();
3647
- await this.storage.createRepository({
3648
- repositoryId: target.repositoryId,
3649
- type: target.config.type,
3650
- label: target.config.label,
3651
- description: target.config.description,
3652
- governanceConfig: target.config.governance ?? { mode: "open" },
3653
- createdAt: now,
3654
- createdBy: this.actorId
3655
- });
3822
+ const existing = await this.storage.getRepository(target.repositoryId);
3823
+ if (!existing) {
3824
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3825
+ await this.storage.createRepository({
3826
+ repositoryId: target.repositoryId,
3827
+ type: target.config.type,
3828
+ label: target.config.label,
3829
+ description: target.config.description,
3830
+ governanceConfig: target.config.governance ?? { mode: "open" },
3831
+ createdAt: now,
3832
+ createdBy: this.actorId
3833
+ });
3834
+ }
3656
3835
  await this.storage.saveVocabulary(target.repositoryId, header.vocabulary);
3836
+ const totalEntities = header.manifest?.statistics?.entityCount ?? 0;
3837
+ const totalRelationships = header.manifest?.statistics?.relationshipCount ?? 0;
3838
+ const totalChunks = estimateChunkCount(totalEntities, totalRelationships);
3657
3839
  let entitiesImported = 0;
3658
3840
  let relationshipsImported = 0;
3841
+ let chunksCompleted = 0;
3659
3842
  for await (const chunk of chunks) {
3660
- const bulkResult = await this.storage.importBulk(target.repositoryId, [chunk]);
3843
+ const bulkResult = await this.storage.importBulk(target.repositoryId, [chunk], { skipExistenceCheck: true });
3661
3844
  entitiesImported += bulkResult.entitiesImported;
3662
3845
  relationshipsImported += bulkResult.relationshipsImported;
3846
+ chunksCompleted++;
3663
3847
  for (const e of bulkResult.errors) {
3664
3848
  warnings.push({
3665
3849
  code: "import_error",
@@ -3667,6 +3851,15 @@ var RepositoryImporter = class {
3667
3851
  id: e.item
3668
3852
  });
3669
3853
  }
3854
+ await this.onProgress?.({
3855
+ repositoryId: target.repositoryId,
3856
+ entitiesImported,
3857
+ relationshipsImported,
3858
+ totalEntities,
3859
+ totalRelationships,
3860
+ chunksCompleted,
3861
+ totalChunks
3862
+ });
3670
3863
  }
3671
3864
  return {
3672
3865
  success: true,
@@ -3714,11 +3907,15 @@ var RepositoryImporter = class {
3714
3907
  if (migrationResult.mergedVocabulary && migrationResult.mergedVocabulary !== targetVocabulary) {
3715
3908
  await this.storage.saveVocabulary(repositoryId, migrationResult.mergedVocabulary);
3716
3909
  }
3910
+ const totalEntities = header.manifest?.statistics?.entityCount ?? 0;
3911
+ const totalRelationships = header.manifest?.statistics?.relationshipCount ?? 0;
3912
+ const totalChunks = estimateChunkCount(totalEntities, totalRelationships);
3717
3913
  const entityConflict = options.entityConflict ?? "skip";
3718
3914
  let entitiesImported = 0;
3719
3915
  let entitiesSkipped = 0;
3720
3916
  let relationshipsImported = 0;
3721
3917
  let relationshipsSkipped = 0;
3918
+ let chunksCompleted = 0;
3722
3919
  for await (const chunk of chunks) {
3723
3920
  if (chunk.entities) {
3724
3921
  for (const entity of chunk.entities) {
@@ -3800,6 +3997,16 @@ var RepositoryImporter = class {
3800
3997
  }
3801
3998
  }
3802
3999
  }
4000
+ chunksCompleted++;
4001
+ await this.onProgress?.({
4002
+ repositoryId,
4003
+ entitiesImported,
4004
+ relationshipsImported,
4005
+ totalEntities,
4006
+ totalRelationships,
4007
+ chunksCompleted,
4008
+ totalChunks
4009
+ });
3803
4010
  }
3804
4011
  const vocabExtensions = migrationResult.warnings.filter(
3805
4012
  (w) => w.code === "entity_type_added" || w.code === "relationship_type_added"
@@ -3856,7 +4063,7 @@ var DeepMemory = class {
3856
4063
  graphTraversalProvider;
3857
4064
  provenance;
3858
4065
  globalEventBus;
3859
- initialised = false;
4066
+ initialized = false;
3860
4067
  constructor(config) {
3861
4068
  this.storage = config.storage;
3862
4069
  this.search = config.search;
@@ -3866,16 +4073,16 @@ var DeepMemory = class {
3866
4073
  this.provenance = new ProvenanceTracker(config.provenance);
3867
4074
  this.globalEventBus = new EventBus(config.provenance);
3868
4075
  }
3869
- /** Initialise the storage provider and optional providers (call once before use) */
3870
- async ensureInitialised() {
3871
- if (!this.initialised) {
3872
- if (this.storage.initialise) {
3873
- await this.storage.initialise();
4076
+ /** Initialize the storage provider and optional providers (call once before use) */
4077
+ async ensureInitialized() {
4078
+ if (!this.initialized) {
4079
+ if (this.storage.initialize) {
4080
+ await this.storage.initialize();
3874
4081
  }
3875
- if (this.graphTraversalProvider?.initialise) {
3876
- await this.graphTraversalProvider.initialise();
4082
+ if (this.graphTraversalProvider?.initialize) {
4083
+ await this.graphTraversalProvider.initialize();
3877
4084
  }
3878
- this.initialised = true;
4085
+ this.initialized = true;
3879
4086
  }
3880
4087
  }
3881
4088
  /** Create or migrate the storage schema. Call once at deployment / startup, not per-request. */
@@ -3887,15 +4094,15 @@ var DeepMemory = class {
3887
4094
  schemaVersion: 0
3888
4095
  };
3889
4096
  try {
3890
- await this.ensureInitialised();
4097
+ await this.ensureInitialized();
3891
4098
  } catch {
3892
4099
  }
3893
4100
  let result = noOpResult;
3894
4101
  if (this.storage.ensureSchema) {
3895
4102
  result = await this.storage.ensureSchema();
3896
4103
  }
3897
- if (!this.initialised) {
3898
- await this.ensureInitialised();
4104
+ if (!this.initialized) {
4105
+ await this.ensureInitialized();
3899
4106
  }
3900
4107
  return result;
3901
4108
  }
@@ -3910,7 +4117,7 @@ var DeepMemory = class {
3910
4117
  }
3911
4118
  /** Create a new memory repository */
3912
4119
  async createRepository(config) {
3913
- await this.ensureInitialised();
4120
+ await this.ensureInitialized();
3914
4121
  const repositoryId = config.repositoryId ?? generateId();
3915
4122
  this.validateRepositoryId(repositoryId);
3916
4123
  const context = this.provenance.getContext();
@@ -3967,7 +4174,7 @@ var DeepMemory = class {
3967
4174
  }
3968
4175
  /** Open an existing repository */
3969
4176
  async openRepository(repositoryId) {
3970
- await this.ensureInitialised();
4177
+ await this.ensureInitialized();
3971
4178
  this.validateRepositoryId(repositoryId);
3972
4179
  const storedRepo = await this.storage.getRepository(repositoryId);
3973
4180
  if (!storedRepo) {
@@ -3996,7 +4203,7 @@ var DeepMemory = class {
3996
4203
  }
3997
4204
  /** List all repositories */
3998
4205
  async listRepositories(filter) {
3999
- await this.ensureInitialised();
4206
+ await this.ensureInitialized();
4000
4207
  const result = await this.storage.listRepositories(filter);
4001
4208
  return {
4002
4209
  items: result.items.map((stored) => ({
@@ -4014,7 +4221,7 @@ var DeepMemory = class {
4014
4221
  }
4015
4222
  /** Update repository metadata and settings */
4016
4223
  async updateRepository(repositoryId, updates) {
4017
- await this.ensureInitialised();
4224
+ await this.ensureInitialized();
4018
4225
  this.validateRepositoryId(repositoryId);
4019
4226
  const updated = await this.storage.updateRepository(repositoryId, updates);
4020
4227
  await this.globalEventBus.emit("repository:updated", { repositoryId });
@@ -4022,14 +4229,55 @@ var DeepMemory = class {
4022
4229
  }
4023
4230
  /** Delete a repository */
4024
4231
  async deleteRepository(repositoryId) {
4025
- await this.ensureInitialised();
4232
+ await this.ensureInitialized();
4026
4233
  this.validateRepositoryId(repositoryId);
4027
- await this.storage.deleteRepository(repositoryId);
4234
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4235
+ await this.globalEventBus.emit("delete:started", {
4236
+ repositoryId,
4237
+ totalEntities: stats.entityCount,
4238
+ totalRelationships: stats.relationshipCount
4239
+ });
4240
+ await this.storage.deleteRepository(
4241
+ repositoryId,
4242
+ (progress) => this.globalEventBus.emit("delete:progress", { repositoryId, ...progress })
4243
+ );
4244
+ await this.globalEventBus.emit("delete:completed", {
4245
+ repositoryId,
4246
+ entitiesDeleted: stats.entityCount,
4247
+ relationshipsDeleted: stats.relationshipCount
4248
+ });
4028
4249
  await this.globalEventBus.emit("repository:deleted", { repositoryId });
4029
4250
  }
4251
+ /** Delete all entities and relationships in a repository, preserving the repository and vocabulary */
4252
+ async deleteAllContents(repositoryId) {
4253
+ await this.ensureInitialized();
4254
+ this.validateRepositoryId(repositoryId);
4255
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4256
+ await this.globalEventBus.emit("delete:started", {
4257
+ repositoryId,
4258
+ totalEntities: stats.entityCount,
4259
+ totalRelationships: stats.relationshipCount
4260
+ });
4261
+ const result = await this.storage.deleteAllContents(
4262
+ repositoryId,
4263
+ (progress) => this.globalEventBus.emit("delete:progress", { repositoryId, ...progress })
4264
+ );
4265
+ await this.globalEventBus.emit("delete:completed", {
4266
+ repositoryId,
4267
+ entitiesDeleted: result.deletedEntities,
4268
+ relationshipsDeleted: result.deletedRelationships
4269
+ });
4270
+ return result;
4271
+ }
4030
4272
  /** Export a repository to a portable archive */
4031
4273
  async exportRepository(repositoryId, options) {
4032
- await this.ensureInitialised();
4274
+ await this.ensureInitialized();
4275
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4276
+ await this.globalEventBus.emit("export:started", {
4277
+ repositoryId,
4278
+ totalEntities: stats.entityCount,
4279
+ totalRelationships: stats.relationshipCount
4280
+ });
4033
4281
  const exporter = new RepositoryExporter({
4034
4282
  storage: this.storage,
4035
4283
  provenance: this.provenance.getContext(),
@@ -4045,10 +4293,12 @@ var DeepMemory = class {
4045
4293
  }
4046
4294
  /** Import a repository from a portable archive */
4047
4295
  async importRepository(archive, options) {
4048
- await this.ensureInitialised();
4296
+ await this.ensureInitialized();
4297
+ await this.globalEventBus.emit("import:started", { repositoryId: options.target.repositoryId });
4049
4298
  const importer = new RepositoryImporter({
4050
4299
  storage: this.storage,
4051
- actorId: this.provenance.getContext().actorId
4300
+ actorId: this.provenance.getContext().actorId,
4301
+ onProgress: (progress) => this.globalEventBus.emit("import:progress", progress)
4052
4302
  });
4053
4303
  const result = await importer.import(archive, options);
4054
4304
  if (result.success) {
@@ -4071,21 +4321,50 @@ var DeepMemory = class {
4071
4321
  * Use for large repositories to avoid loading everything into memory.
4072
4322
  */
4073
4323
  async *exportRepositoryStream(repositoryId, options) {
4074
- await this.ensureInitialised();
4324
+ await this.ensureInitialized();
4075
4325
  const exporter = new RepositoryExporter({
4076
4326
  storage: this.storage,
4077
4327
  provenance: this.provenance.getContext(),
4078
4328
  legal: options?.legal
4079
4329
  });
4080
- await this.globalEventBus.emit("export:started", { repositoryId });
4330
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4331
+ const totalEntities = stats.entityCount;
4332
+ const totalRelationships = stats.relationshipCount;
4333
+ const estimatedChunkSize = 100;
4334
+ const totalChunks = Math.max(
4335
+ 1,
4336
+ Math.ceil(totalEntities / estimatedChunkSize) + Math.ceil(totalRelationships / estimatedChunkSize)
4337
+ );
4338
+ await this.globalEventBus.emit("export:started", { repositoryId, totalEntities, totalRelationships });
4081
4339
  let entityCount = 0;
4082
4340
  let relationshipCount = 0;
4341
+ let chunksCompleted = 0;
4083
4342
  for await (const item of exporter.exportStream(repositoryId)) {
4084
4343
  yield item;
4085
4344
  if (item.type === "entities") {
4086
4345
  entityCount += item.data.length;
4346
+ chunksCompleted++;
4347
+ await this.globalEventBus.emit("export:progress", {
4348
+ repositoryId,
4349
+ entitiesExported: entityCount,
4350
+ relationshipsExported: relationshipCount,
4351
+ totalEntities,
4352
+ totalRelationships,
4353
+ chunksCompleted,
4354
+ totalChunks
4355
+ });
4087
4356
  } else if (item.type === "relationships") {
4088
4357
  relationshipCount += item.data.length;
4358
+ chunksCompleted++;
4359
+ await this.globalEventBus.emit("export:progress", {
4360
+ repositoryId,
4361
+ entitiesExported: entityCount,
4362
+ relationshipsExported: relationshipCount,
4363
+ totalEntities,
4364
+ totalRelationships,
4365
+ chunksCompleted,
4366
+ totalChunks
4367
+ });
4089
4368
  }
4090
4369
  }
4091
4370
  await this.globalEventBus.emit("export:completed", {
@@ -4099,10 +4378,11 @@ var DeepMemory = class {
4099
4378
  * Use for large repositories to avoid loading everything into memory.
4100
4379
  */
4101
4380
  async importRepositoryStream(header, chunks, options) {
4102
- await this.ensureInitialised();
4381
+ await this.ensureInitialized();
4103
4382
  const importer = new RepositoryImporter({
4104
4383
  storage: this.storage,
4105
- actorId: this.provenance.getContext().actorId
4384
+ actorId: this.provenance.getContext().actorId,
4385
+ onProgress: (progress) => this.globalEventBus.emit("import:progress", progress)
4106
4386
  });
4107
4387
  await this.globalEventBus.emit("import:started", { repositoryId: options.target.repositoryId });
4108
4388
  const result = await importer.importStream(header, chunks, options);
@@ -4138,7 +4418,7 @@ var DeepMemory = class {
4138
4418
  if (this.storage.dispose) {
4139
4419
  await this.storage.dispose();
4140
4420
  }
4141
- this.initialised = false;
4421
+ this.initialized = false;
4142
4422
  }
4143
4423
  };
4144
4424
 
@@ -4153,7 +4433,7 @@ var InMemoryStorageProvider = class {
4153
4433
  return store;
4154
4434
  }
4155
4435
  // ─── Lifecycle ─────────────────────────────────────────────────────
4156
- async initialise() {
4436
+ async initialize() {
4157
4437
  }
4158
4438
  async dispose() {
4159
4439
  this.stores.clear();
@@ -4226,12 +4506,21 @@ var InMemoryStorageProvider = class {
4226
4506
  if (updates.metadata !== void 0) repo.metadata = { ...repo.metadata, ...updates.metadata };
4227
4507
  return repo;
4228
4508
  }
4229
- async deleteRepository(repositoryId) {
4509
+ async deleteRepository(repositoryId, _onProgress) {
4230
4510
  if (!this.stores.has(repositoryId)) {
4231
4511
  throw new RepositoryNotFoundError(repositoryId);
4232
4512
  }
4233
4513
  this.stores.delete(repositoryId);
4234
4514
  }
4515
+ async deleteAllContents(repositoryId, _onProgress) {
4516
+ const store = this.getStore(repositoryId);
4517
+ const deletedEntities = store.entities.size;
4518
+ const deletedRelationships = store.relationships.size;
4519
+ store.entities.clear();
4520
+ store.slugIndex.clear();
4521
+ store.relationships.clear();
4522
+ return { deletedEntities, deletedRelationships };
4523
+ }
4235
4524
  async getRepositoryStats(repositoryId) {
4236
4525
  const store = this.getStore(repositoryId);
4237
4526
  const entityTypeBreakdown = {};
@@ -4461,7 +4750,7 @@ var InMemoryStorageProvider = class {
4461
4750
  return { deletedRelationships };
4462
4751
  }
4463
4752
  // ─── Graph Traversal ───────────────────────────────────────────────
4464
- async exploreNeighbourhood(repositoryId, entityId, options) {
4753
+ async exploreNeighborhood(repositoryId, entityId, options) {
4465
4754
  const store = this.getStore(repositoryId);
4466
4755
  if (!store.entities.has(entityId)) {
4467
4756
  throw new EntityNotFoundError(entityId);
@@ -4527,7 +4816,7 @@ var InMemoryStorageProvider = class {
4527
4816
  if (nextFrontier.size === 0) break;
4528
4817
  }
4529
4818
  return {
4530
- centreId: entityId,
4819
+ centerId: entityId,
4531
4820
  layers
4532
4821
  };
4533
4822
  }
@@ -4686,7 +4975,7 @@ var InMemoryStorageProvider = class {
4686
4975
  yield { type: "relationships", data: [], sequence: 0, isLast: true };
4687
4976
  }
4688
4977
  }
4689
- async importBulk(repositoryId, data) {
4978
+ async importBulk(repositoryId, data, _options) {
4690
4979
  const store = this.getStore(repositoryId);
4691
4980
  let entitiesImported = 0;
4692
4981
  let relationshipsImported = 0;