@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.js CHANGED
@@ -277,6 +277,12 @@ var EntityManager = class {
277
277
  this.storage = storage;
278
278
  this.embedding = embedding;
279
279
  }
280
+ repositoryId;
281
+ vocabularyEngine;
282
+ provenanceTracker;
283
+ eventBus;
284
+ storage;
285
+ embedding;
280
286
  /** Create one or more entities with vocabulary validation, ID generation, provenance, and events */
281
287
  async create(inputs) {
282
288
  const results = [];
@@ -640,7 +646,7 @@ function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
640
646
  }
641
647
  return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
642
648
  }
643
- function normaliseTypeName(name) {
649
+ function normalizeTypeName(name) {
644
650
  return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
645
651
  }
646
652
  function toScreamingSnakeCase(name) {
@@ -656,6 +662,11 @@ var RelationshipManager = class {
656
662
  this.eventBus = eventBus;
657
663
  this.storage = storage;
658
664
  }
665
+ repositoryId;
666
+ vocabularyEngine;
667
+ provenanceTracker;
668
+ eventBus;
669
+ storage;
659
670
  /** Create one or more relationships with vocabulary validation, provenance, and events */
660
671
  async create(inputs) {
661
672
  const results = [];
@@ -795,6 +806,7 @@ var DEFAULT_MAX_LIMIT = 200;
795
806
  var DEFAULT_FALLBACK_MAX_DEPTH = 10;
796
807
  function validateTraversalSpec(spec, vocabulary, capabilities) {
797
808
  const errors = [];
809
+ const steps = spec.steps ?? [];
798
810
  if (!spec.start) {
799
811
  errors.push("start is required");
800
812
  } else {
@@ -808,37 +820,45 @@ function validateTraversalSpec(spec, vocabulary, capabilities) {
808
820
  errors.push("start.entityType without entityId requires limit on the spec to prevent full type scans");
809
821
  }
810
822
  }
811
- if (!spec.steps || spec.steps.length === 0) {
812
- errors.push("steps must contain at least one step");
813
- }
814
823
  const maxSteps = capabilities?.maxTraversalDepth ?? DEFAULT_MAX_STEPS;
815
- if (spec.steps && spec.steps.length > maxSteps) {
816
- errors.push(`steps length ${spec.steps.length} exceeds maximum ${maxSteps}`);
817
- }
818
- if (spec.steps) {
819
- for (let i = 0; i < spec.steps.length; i++) {
820
- const step = spec.steps[i];
821
- if (!step.direction || !["out", "in", "both"].includes(step.direction)) {
822
- errors.push(`steps[${i}].direction must be 'out', 'in', or 'both'`);
823
- }
824
- if (step.repeat) {
825
- if (step.repeat.maxDepth === void 0 || step.repeat.maxDepth <= 0) {
826
- errors.push(`steps[${i}].repeat.maxDepth must be a positive number`);
827
- }
824
+ if (steps.length > maxSteps) {
825
+ errors.push(`steps length ${steps.length} exceeds maximum ${maxSteps}`);
826
+ }
827
+ for (let i = 0; i < steps.length; i++) {
828
+ const step = steps[i];
829
+ if (!step.direction || !["out", "in", "both"].includes(step.direction)) {
830
+ errors.push(`steps[${i}].direction must be 'out', 'in', or 'both'`);
831
+ }
832
+ if (step.repeat) {
833
+ if (step.repeat.maxDepth === void 0 || step.repeat.maxDepth <= 0) {
834
+ errors.push(`steps[${i}].repeat.maxDepth must be a positive number`);
828
835
  }
829
836
  }
837
+ }
838
+ if (steps.length > 0) {
830
839
  const maxProviderDepth = capabilities?.maxTraversalDepth ?? DEFAULT_FALLBACK_MAX_DEPTH;
831
840
  let totalDepth = 0;
832
- for (const step of spec.steps) {
841
+ for (const step of steps) {
833
842
  totalDepth += step.repeat ? step.repeat.maxDepth : 1;
834
843
  }
835
844
  if (totalDepth > maxProviderDepth) {
836
845
  errors.push(`total potential depth ${totalDepth} exceeds provider maximum ${maxProviderDepth}`);
837
846
  }
838
847
  }
848
+ if (spec.returnMode === "path" && steps.length === 0) {
849
+ errors.push("returnMode 'path' requires at least one step");
850
+ }
839
851
  if (spec.returnMode && !["terminal", "path", "all"].includes(spec.returnMode)) {
840
852
  errors.push(`returnMode must be 'terminal', 'path', or 'all'`);
841
853
  }
854
+ if (spec.projection) {
855
+ if (!spec.projection.properties || spec.projection.properties.length === 0) {
856
+ errors.push("projection.properties must contain at least one property name");
857
+ }
858
+ if (spec.projection.mode && !["count", "values"].includes(spec.projection.mode)) {
859
+ errors.push("projection.mode must be 'count' or 'values'");
860
+ }
861
+ }
842
862
  if (spec.limit !== void 0) {
843
863
  if (spec.limit < 1 || spec.limit > DEFAULT_MAX_LIMIT) {
844
864
  errors.push(`limit must be between 1 and ${DEFAULT_MAX_LIMIT}`);
@@ -847,15 +867,15 @@ function validateTraversalSpec(spec, vocabulary, capabilities) {
847
867
  if (spec.offset !== void 0 && spec.offset < 0) {
848
868
  errors.push("offset must be >= 0");
849
869
  }
850
- if (vocabulary && spec.steps) {
870
+ if (vocabulary) {
851
871
  const entityTypeSet = new Set(vocabulary.entityTypes.map((et) => et.type));
852
872
  const relationshipTypeSet = new Set(vocabulary.relationshipTypes.map((rt) => rt.type));
853
873
  const unknownTypes = [];
854
874
  if (spec.start?.entityType && !entityTypeSet.has(spec.start.entityType)) {
855
875
  unknownTypes.push(`entity type "${spec.start.entityType}"`);
856
876
  }
857
- for (let i = 0; i < spec.steps.length; i++) {
858
- const step = spec.steps[i];
877
+ for (let i = 0; i < steps.length; i++) {
878
+ const step = steps[i];
859
879
  if (step.relationshipTypes) {
860
880
  for (const rt of step.relationshipTypes) {
861
881
  if (!relationshipTypeSet.has(rt)) {
@@ -909,7 +929,8 @@ var GremlinCompiler = class {
909
929
  parts.push(compilePropertyFilter(f, nextParam));
910
930
  }
911
931
  }
912
- for (const step of spec.steps) {
932
+ const steps = spec.steps ?? [];
933
+ for (const step of steps) {
913
934
  if (step.repeat) {
914
935
  parts.push(compileRepeatStep(step, nextParam));
915
936
  estimatedFanOut *= step.repeat.maxDepth * DEFAULT_ESTIMATED_FANOUT_PER_HOP;
@@ -1091,8 +1112,9 @@ var CypherCompiler = class {
1091
1112
  }
1092
1113
  }
1093
1114
  let lastNode = startNode;
1094
- for (let i = 0; i < spec.steps.length; i++) {
1095
- const step = spec.steps[i];
1115
+ const steps = spec.steps ?? [];
1116
+ for (let i = 0; i < steps.length; i++) {
1117
+ const step = steps[i];
1096
1118
  const targetNode = `n${nodeIndex++}`;
1097
1119
  const relAlias = `r${i}`;
1098
1120
  const relTypes = step.relationshipTypes?.length ? step.relationshipTypes.join("|") : "";
@@ -1134,7 +1156,7 @@ WHERE ${whereClauses.join(" AND ")}` : "";
1134
1156
  let returnClause;
1135
1157
  if (spec.returnMode === "path") {
1136
1158
  const allNodes = Array.from({ length: nodeIndex }, (_, i) => `n${i}`);
1137
- const allRels = spec.steps.map((_, i) => `r${i}`);
1159
+ const allRels = steps.map((_, i) => `r${i}`);
1138
1160
  returnClause = `RETURN ${[...allNodes, ...allRels].join(", ")}`;
1139
1161
  } else if (spec.returnMode === "all") {
1140
1162
  const allNodes = Array.from({ length: nodeIndex }, (_, i) => `n${i}`);
@@ -1215,6 +1237,7 @@ function matchesSingleFilter(properties, filter) {
1215
1237
 
1216
1238
  // src/relationships/FallbackTraversalExecutor.ts
1217
1239
  var MAX_STORAGE_CALLS = 500;
1240
+ var MAX_ENTITY_SCAN = 1e4;
1218
1241
  var REL_FETCH_LIMIT = 1e3;
1219
1242
  async function executeFallbackTraversal(repositoryId, storage, spec) {
1220
1243
  const startTime = Date.now();
@@ -1224,6 +1247,9 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1224
1247
  throw new Error(`Fallback traversal exceeded ${MAX_STORAGE_CALLS} storage calls. Consider using a GraphTraversalProvider for large traversals.`);
1225
1248
  }
1226
1249
  };
1250
+ const isVertexQuery = !spec.steps || spec.steps.length === 0;
1251
+ const needsFullScan = isVertexQuery || spec.projection !== void 0;
1252
+ const fetchLimit = needsFullScan ? MAX_ENTITY_SCAN : spec.limit ?? 50;
1227
1253
  let frontier = [];
1228
1254
  if (spec.start.entityId) {
1229
1255
  const entity = await resolveEntity(repositoryId, storage, spec.start.entityId);
@@ -1236,7 +1262,7 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1236
1262
  } else if (spec.start.entityType) {
1237
1263
  const result = await storage.findEntities(repositoryId, {
1238
1264
  entityTypes: [spec.start.entityType],
1239
- limit: spec.limit ?? 50,
1265
+ limit: fetchLimit,
1240
1266
  offset: 0
1241
1267
  });
1242
1268
  storageCalls++;
@@ -1247,7 +1273,7 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1247
1273
  }));
1248
1274
  } else if (spec.start.filter && spec.start.filter.length > 0) {
1249
1275
  const result = await storage.findEntities(repositoryId, {
1250
- limit: spec.limit ?? 50,
1276
+ limit: fetchLimit,
1251
1277
  offset: 0
1252
1278
  });
1253
1279
  storageCalls++;
@@ -1262,8 +1288,9 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1262
1288
  (entry) => matchesPropertyFilters(entry.entity.properties, spec.start.filter)
1263
1289
  );
1264
1290
  }
1291
+ const steps = spec.steps ?? [];
1265
1292
  const allCollected = spec.returnMode === "all" ? [...frontier] : [];
1266
- for (const step of spec.steps) {
1293
+ for (const step of steps) {
1267
1294
  checkCallLimit();
1268
1295
  if (step.repeat) {
1269
1296
  const result = await executeRepeatStep(
@@ -1316,10 +1343,89 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1316
1343
  }
1317
1344
  const total = resultEntries.length;
1318
1345
  const paged = resultEntries.slice(offset, offset + limit);
1319
- const entities = paged.map((entry) => projectEntity(entry.entity, detailLevel));
1346
+ const suppressEntities = spec.projection !== void 0 && spec.projection.includeEntities !== true;
1347
+ let entities;
1348
+ if (suppressEntities) {
1349
+ entities = [];
1350
+ } else {
1351
+ entities = paged.map((entry) => {
1352
+ const projected = projectEntity(entry.entity, detailLevel);
1353
+ if (!spec.includeProvenance) {
1354
+ delete projected["provenance"];
1355
+ }
1356
+ return projected;
1357
+ });
1358
+ if (spec.includeRelationshipSummary && entities.length > 0) {
1359
+ const summaryResults = await Promise.all(
1360
+ paged.map(async (entry) => {
1361
+ storageCalls++;
1362
+ const result = await storage.getEntityRelationships(repositoryId, entry.entity.id, {
1363
+ direction: "both",
1364
+ limit: 1e4
1365
+ });
1366
+ const summary = { outbound: {}, inbound: {} };
1367
+ for (const rel of result.items) {
1368
+ if (rel.sourceEntityId === entry.entity.id) {
1369
+ summary.outbound[rel.relationshipType] = (summary.outbound[rel.relationshipType] ?? 0) + 1;
1370
+ }
1371
+ if (rel.targetEntityId === entry.entity.id) {
1372
+ summary.inbound[rel.relationshipType] = (summary.inbound[rel.relationshipType] ?? 0) + 1;
1373
+ }
1374
+ }
1375
+ return summary;
1376
+ })
1377
+ );
1378
+ entities = entities.map((e, i) => ({ ...e, relationshipSummary: summaryResults[i] }));
1379
+ }
1380
+ }
1381
+ let aggregations;
1382
+ if (spec.projection) {
1383
+ const propNames = spec.projection.properties;
1384
+ const mode = spec.projection.mode ?? "values";
1385
+ const distinct = spec.projection.distinct ?? false;
1386
+ const sourceEntries = dedup ? (() => {
1387
+ const seen = /* @__PURE__ */ new Set();
1388
+ return (spec.returnMode === "all" ? allCollected : frontier).filter((e) => {
1389
+ if (seen.has(e.entity.id)) return false;
1390
+ seen.add(e.entity.id);
1391
+ return true;
1392
+ });
1393
+ })() : spec.returnMode === "all" ? allCollected : frontier;
1394
+ if (mode === "count" || distinct) {
1395
+ const groups = /* @__PURE__ */ new Map();
1396
+ for (const entry of sourceEntries) {
1397
+ const vals = {};
1398
+ for (const prop of propNames) {
1399
+ vals[prop] = entry.entity.properties[prop] ?? null;
1400
+ }
1401
+ const key = JSON.stringify(vals);
1402
+ const existing = groups.get(key);
1403
+ if (existing) {
1404
+ existing.count++;
1405
+ } else {
1406
+ groups.set(key, { values: vals, count: 1 });
1407
+ }
1408
+ }
1409
+ aggregations = Array.from(groups.values()).map((g) => ({
1410
+ values: g.values,
1411
+ ...mode === "count" ? { count: g.count } : {}
1412
+ }));
1413
+ if (mode === "count") {
1414
+ aggregations.sort((a, b) => (b.count ?? 0) - (a.count ?? 0));
1415
+ }
1416
+ } else {
1417
+ aggregations = sourceEntries.map((entry) => {
1418
+ const vals = {};
1419
+ for (const prop of propNames) {
1420
+ vals[prop] = entry.entity.properties[prop] ?? null;
1421
+ }
1422
+ return { values: vals };
1423
+ });
1424
+ }
1425
+ }
1320
1426
  let relationships;
1321
1427
  let paths;
1322
- if (spec.returnMode === "path" || spec.returnMode === "all") {
1428
+ if (!suppressEntities && (spec.returnMode === "path" || spec.returnMode === "all")) {
1323
1429
  const relMap = /* @__PURE__ */ new Map();
1324
1430
  for (const entry of paged) {
1325
1431
  for (const rel of entry.relationshipPath) {
@@ -1337,10 +1443,14 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1337
1443
  }
1338
1444
  relationships = Array.from(relMap.values());
1339
1445
  }
1340
- if (spec.returnMode === "path") {
1446
+ if (!suppressEntities && spec.returnMode === "path") {
1341
1447
  paths = paged.map((entry) => ({
1342
1448
  length: entry.entityPath.length - 1,
1343
- entities: [projectEntity(entry.entity, detailLevel)],
1449
+ entities: [(() => {
1450
+ const e = projectEntity(entry.entity, detailLevel);
1451
+ if (!spec.includeProvenance) delete e["provenance"];
1452
+ return e;
1453
+ })()],
1344
1454
  relationships: entry.relationshipPath.map((rel) => ({
1345
1455
  id: rel.id,
1346
1456
  type: rel.relationshipType,
@@ -1356,6 +1466,7 @@ async function executeFallbackTraversal(repositoryId, storage, spec) {
1356
1466
  entities,
1357
1467
  relationships,
1358
1468
  paths,
1469
+ aggregations,
1359
1470
  total,
1360
1471
  returned: paged.length,
1361
1472
  hasMore: offset + limit < total,
@@ -1490,8 +1601,12 @@ var GraphTraversal = class {
1490
1601
  this.graphTraversalProvider = graphTraversalProvider;
1491
1602
  this.vocabularyEngine = vocabularyEngine;
1492
1603
  }
1493
- /** BFS neighbourhood exploration from a centre entity */
1494
- async exploreNeighbourhood(entityId, options) {
1604
+ repositoryId;
1605
+ storage;
1606
+ graphTraversalProvider;
1607
+ vocabularyEngine;
1608
+ /** BFS neighborhood exploration from a center entity */
1609
+ async exploreNeighborhood(entityId, options) {
1495
1610
  const storageOptions = {
1496
1611
  depth: options?.depth ?? 1,
1497
1612
  relationshipTypes: options?.relationshipTypes,
@@ -1501,13 +1616,13 @@ var GraphTraversal = class {
1501
1616
  offsetPerType: options?.offsetPerType ?? 0,
1502
1617
  relationshipPropertyFilters: options?.relationshipPropertyFilters
1503
1618
  };
1504
- const storageResult = await this.storage.exploreNeighbourhood(
1619
+ const storageResult = await this.storage.exploreNeighborhood(
1505
1620
  this.repositoryId,
1506
1621
  entityId,
1507
1622
  storageOptions
1508
1623
  );
1509
- const centreEntity = await this.storage.getEntity(this.repositoryId, entityId);
1510
- if (!centreEntity) {
1624
+ const centerEntity = await this.storage.getEntity(this.repositoryId, entityId);
1625
+ if (!centerEntity) {
1511
1626
  throw new EntityNotFoundError(entityId);
1512
1627
  }
1513
1628
  const detailLevel = options?.detailLevel ?? "summary";
@@ -1531,11 +1646,11 @@ var GraphTraversal = class {
1531
1646
  return layer;
1532
1647
  });
1533
1648
  return {
1534
- centre: {
1535
- id: centreEntity.id,
1536
- slug: centreEntity.slug,
1537
- entityType: centreEntity.entityType,
1538
- label: centreEntity.label
1649
+ center: {
1650
+ id: centerEntity.id,
1651
+ slug: centerEntity.slug,
1652
+ entityType: centerEntity.entityType,
1653
+ label: centerEntity.label
1539
1654
  },
1540
1655
  layers,
1541
1656
  statistics: {
@@ -2089,8 +2204,8 @@ var MemoryRepository = class {
2089
2204
  };
2090
2205
  }
2091
2206
  // ─── Graph Traversal ───────────────────────────────────────────────
2092
- async exploreNeighbourhood(entityId, options) {
2093
- return this.graphTraversal.exploreNeighbourhood(entityId, options);
2207
+ async exploreNeighborhood(entityId, options) {
2208
+ return this.graphTraversal.exploreNeighborhood(entityId, options);
2094
2209
  }
2095
2210
  async findPaths(sourceId, targetId, options) {
2096
2211
  return this.graphTraversal.findPaths(sourceId, targetId, options);
@@ -2120,7 +2235,7 @@ var MemoryRepository = class {
2120
2235
  storageOptions
2121
2236
  );
2122
2237
  const entity = await this.storage.getEntity(this.repositoryId, entityId);
2123
- const centreRef = {
2238
+ const centerRef = {
2124
2239
  id: entityId,
2125
2240
  slug: entity?.slug ?? entityId,
2126
2241
  label: entity?.label ?? entityId
@@ -2166,7 +2281,7 @@ var MemoryRepository = class {
2166
2281
  timestamp: e.timestamp,
2167
2282
  eventType: e.eventType,
2168
2283
  description: `${rel.type} relationship created ${direction} ${otherRef.label}`,
2169
- relatedEntities: [centreRef, otherRef],
2284
+ relatedEntities: [centerRef, otherRef],
2170
2285
  relationship: relDetail
2171
2286
  };
2172
2287
  }
@@ -2174,13 +2289,13 @@ var MemoryRepository = class {
2174
2289
  return {
2175
2290
  timestamp: e.timestamp,
2176
2291
  eventType: e.eventType,
2177
- description: `${centreRef.label} ${action}`,
2178
- relatedEntities: [centreRef]
2292
+ description: `${centerRef.label} ${action}`,
2293
+ relatedEntities: [centerRef]
2179
2294
  };
2180
2295
  });
2181
2296
  return {
2182
2297
  id: entityId,
2183
- slug: centreRef.slug,
2298
+ slug: centerRef.slug,
2184
2299
  totalEvents: storageResult.total,
2185
2300
  returned: events.length,
2186
2301
  events
@@ -2190,6 +2305,58 @@ var MemoryRepository = class {
2190
2305
  async getStats() {
2191
2306
  return this.storage.getRepositoryStats(this.repositoryId);
2192
2307
  }
2308
+ // ─── Bulk Operations ──────────────────────────────────────────────
2309
+ /** Delete all entities and relationships in this repository, preserving the repository and vocabulary */
2310
+ async deleteAllContents() {
2311
+ return this.storage.deleteAllContents(this.repositoryId);
2312
+ }
2313
+ /**
2314
+ * Returns a static markdown guide describing the recommended query strategy
2315
+ * for AI agents and other consumers of the repository.
2316
+ */
2317
+ getQueryGuide() {
2318
+ return `## Query Strategy Guide
2319
+
2320
+ ### Step 1 \u2014 Discover before you traverse
2321
+
2322
+ Before following relationships from an entity, check that it actually has relationships.
2323
+ \`memory_query_graph\` includes \`relationshipSummary\` on every returned entity by default
2324
+ (outbound and inbound counts by type). Inspect this before traversing \u2014 entities with
2325
+ zero outbound counts for the relationship type you need have no connections to follow.
2326
+ To skip summaries and reduce response size, set \`includeRelationshipSummary: false\`.
2327
+
2328
+ ### Step 2 \u2014 Use projection for aggregation
2329
+
2330
+ To understand what data exists (e.g. all distinct values of a property, or counts by
2331
+ category), use \`memory_query_graph\` with \`projection\` instead of fetching full entities:
2332
+ \`{ start: { entityType: "..." }, projection: { properties: ["propName"], distinct: true }, limit: 200 }\`
2333
+
2334
+ This returns only the aggregated values \u2014 no entity objects \u2014 keeping responses lightweight.
2335
+
2336
+ ### Step 3 \u2014 Traverse efficiently
2337
+
2338
+ - **Omit relationshipTypes** in a traversal step to follow all relationship types at once,
2339
+ rather than making separate calls per type.
2340
+ - **Use multi-step traversals** for multi-hop patterns. A two-step query
2341
+ (e.g. Equipment \u2192 Component \u2192 MaintenanceProcedure) is one call, not two sequential calls.
2342
+ - **Use returnMode "all"** when you need intermediate entities along the path, not just
2343
+ the terminal nodes.
2344
+
2345
+ ### Step 4 \u2014 Use semantic search for known-unknowns
2346
+
2347
+ \`memory_search_by_concept\` is best for finding specific entities when you know roughly
2348
+ what you're looking for but not the exact label or type. It is not efficient for broad
2349
+ discovery \u2014 use projection or find_entities for that.
2350
+
2351
+ ### Common anti-patterns to avoid
2352
+
2353
+ - **Don't traverse from entities with zero relationship counts.** The \`relationshipSummary\` on each entity tells you what is connected before you traverse.
2354
+ - **Don't split queries by relationship type.** One step with no type filter is better
2355
+ than three separate single-type calls.
2356
+ - **Don't use sequential single-hop traversals** when a multi-step query achieves the
2357
+ same result in one call.
2358
+ - **Don't fetch full entities when you only need property values.** Use projection.`;
2359
+ }
2193
2360
  // ─── Events ────────────────────────────────────────────────────────
2194
2361
  on(event, handler) {
2195
2362
  return this.eventBus.on(event, handler);
@@ -2211,14 +2378,14 @@ var SemanticDeduplicator = class {
2211
2378
  /**
2212
2379
  * Check whether a proposed type name is a duplicate of an existing type.
2213
2380
  * Uses embedding similarity if an EmbeddingProvider is available, otherwise falls back
2214
- * to Jaro-Winkler string similarity on normalised type names.
2381
+ * to Jaro-Winkler string similarity on normalized type names.
2215
2382
  */
2216
2383
  async checkDuplicate(proposedType, proposedDescription, existingTypes) {
2217
2384
  if (existingTypes.length === 0) {
2218
2385
  return { isDuplicate: false, matches: [] };
2219
2386
  }
2220
- const normProposed = normaliseTypeName(proposedType);
2221
- const exactMatch = existingTypes.find((t) => normaliseTypeName(t.type) === normProposed);
2387
+ const normProposed = normalizeTypeName(proposedType);
2388
+ const exactMatch = existingTypes.find((t) => normalizeTypeName(t.type) === normProposed);
2222
2389
  if (exactMatch) {
2223
2390
  return {
2224
2391
  isDuplicate: true,
@@ -2254,10 +2421,10 @@ var SemanticDeduplicator = class {
2254
2421
  };
2255
2422
  }
2256
2423
  checkWithStringSimilarity(proposedType, existingTypes) {
2257
- const normProposed = normaliseTypeName(proposedType);
2424
+ const normProposed = normalizeTypeName(proposedType);
2258
2425
  const matches = [];
2259
2426
  for (const existing of existingTypes) {
2260
- const normExisting = normaliseTypeName(existing.type);
2427
+ const normExisting = normalizeTypeName(existing.type);
2261
2428
  const similarity = jaroWinklerSimilarity(normProposed, normExisting);
2262
2429
  if (similarity >= this.threshold) {
2263
2430
  matches.push({
@@ -2418,7 +2585,7 @@ function canPropose(governanceConfig, _proposal) {
2418
2585
  case "locked":
2419
2586
  return {
2420
2587
  allowed: false,
2421
- reason: "Vocabulary is locked. Only organisation admins can modify the vocabulary via the admin API."
2588
+ reason: "Vocabulary is locked. Only organization admins can modify the vocabulary via the admin API."
2422
2589
  };
2423
2590
  case "managed":
2424
2591
  case "open":
@@ -2858,9 +3025,9 @@ function validateEntityUpdate(input, entityTypeDef) {
2858
3025
  return ok();
2859
3026
  }
2860
3027
  function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
2861
- const normalisedType = toScreamingSnakeCase(input.relationshipType);
3028
+ const normalizedType = toScreamingSnakeCase(input.relationshipType);
2862
3029
  const relTypeDef = vocabulary.relationshipTypes.find(
2863
- (rt) => rt.type === normalisedType
3030
+ (rt) => rt.type === normalizedType
2864
3031
  );
2865
3032
  if (!relTypeDef) {
2866
3033
  const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
@@ -3185,7 +3352,7 @@ var EventBus = class {
3185
3352
  };
3186
3353
 
3187
3354
  // src/portability/RepositoryExporter.ts
3188
- var LIBRARY_VERSION = true ? "0.2.0" : "0.1.0";
3355
+ var LIBRARY_VERSION = true ? "0.4.0" : "0.1.0";
3189
3356
  var RepositoryExporter = class {
3190
3357
  storage;
3191
3358
  provenance;
@@ -3401,7 +3568,7 @@ function diffArrays(from, to) {
3401
3568
  // src/portability/MigrationEngine.ts
3402
3569
  var MigrationEngine = class {
3403
3570
  /**
3404
- * Analyse and migrate vocabulary differences between source and target.
3571
+ * Analyze and migrate vocabulary differences between source and target.
3405
3572
  *
3406
3573
  * @param sourceVocabulary - vocabulary from the export archive
3407
3574
  * @param targetVocabulary - vocabulary in the target repository
@@ -3528,13 +3695,22 @@ function describeChange(change) {
3528
3695
  }
3529
3696
 
3530
3697
  // src/portability/RepositoryImporter.ts
3698
+ function estimateChunkCount(totalEntities, totalRelationships) {
3699
+ const ESTIMATED_CHUNK_SIZE = 100;
3700
+ return Math.max(
3701
+ 1,
3702
+ Math.ceil(totalEntities / ESTIMATED_CHUNK_SIZE) + Math.ceil(totalRelationships / ESTIMATED_CHUNK_SIZE)
3703
+ );
3704
+ }
3531
3705
  var RepositoryImporter = class {
3532
3706
  storage;
3533
3707
  actorId;
3708
+ onProgress;
3534
3709
  migrationEngine = new MigrationEngine();
3535
3710
  constructor(config) {
3536
3711
  this.storage = config.storage;
3537
3712
  this.actorId = config.actorId;
3713
+ this.onProgress = config.onProgress;
3538
3714
  }
3539
3715
  /**
3540
3716
  * Import an export archive according to the provided options.
@@ -3585,26 +3761,34 @@ var RepositoryImporter = class {
3585
3761
  return this.importStreamMerge(header, chunks, options, warnings);
3586
3762
  }
3587
3763
  }
3588
- /** Streaming create mode — set up repo then pipe chunks through importBulk */
3764
+ /** Streaming create mode — fast bulk inserts with no existence checks */
3589
3765
  async importStreamCreate(header, chunks, options, warnings) {
3590
3766
  const target = options.target;
3591
- const now = (/* @__PURE__ */ new Date()).toISOString();
3592
- await this.storage.createRepository({
3593
- repositoryId: target.repositoryId,
3594
- type: target.config.type,
3595
- label: target.config.label,
3596
- description: target.config.description,
3597
- governanceConfig: target.config.governance ?? { mode: "open" },
3598
- createdAt: now,
3599
- createdBy: this.actorId
3600
- });
3767
+ const existing = await this.storage.getRepository(target.repositoryId);
3768
+ if (!existing) {
3769
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3770
+ await this.storage.createRepository({
3771
+ repositoryId: target.repositoryId,
3772
+ type: target.config.type,
3773
+ label: target.config.label,
3774
+ description: target.config.description,
3775
+ governanceConfig: target.config.governance ?? { mode: "open" },
3776
+ createdAt: now,
3777
+ createdBy: this.actorId
3778
+ });
3779
+ }
3601
3780
  await this.storage.saveVocabulary(target.repositoryId, header.vocabulary);
3781
+ const totalEntities = header.manifest?.statistics?.entityCount ?? 0;
3782
+ const totalRelationships = header.manifest?.statistics?.relationshipCount ?? 0;
3783
+ const totalChunks = estimateChunkCount(totalEntities, totalRelationships);
3602
3784
  let entitiesImported = 0;
3603
3785
  let relationshipsImported = 0;
3786
+ let chunksCompleted = 0;
3604
3787
  for await (const chunk of chunks) {
3605
- const bulkResult = await this.storage.importBulk(target.repositoryId, [chunk]);
3788
+ const bulkResult = await this.storage.importBulk(target.repositoryId, [chunk], { skipExistenceCheck: true });
3606
3789
  entitiesImported += bulkResult.entitiesImported;
3607
3790
  relationshipsImported += bulkResult.relationshipsImported;
3791
+ chunksCompleted++;
3608
3792
  for (const e of bulkResult.errors) {
3609
3793
  warnings.push({
3610
3794
  code: "import_error",
@@ -3612,6 +3796,15 @@ var RepositoryImporter = class {
3612
3796
  id: e.item
3613
3797
  });
3614
3798
  }
3799
+ await this.onProgress?.({
3800
+ repositoryId: target.repositoryId,
3801
+ entitiesImported,
3802
+ relationshipsImported,
3803
+ totalEntities,
3804
+ totalRelationships,
3805
+ chunksCompleted,
3806
+ totalChunks
3807
+ });
3615
3808
  }
3616
3809
  return {
3617
3810
  success: true,
@@ -3659,11 +3852,15 @@ var RepositoryImporter = class {
3659
3852
  if (migrationResult.mergedVocabulary && migrationResult.mergedVocabulary !== targetVocabulary) {
3660
3853
  await this.storage.saveVocabulary(repositoryId, migrationResult.mergedVocabulary);
3661
3854
  }
3855
+ const totalEntities = header.manifest?.statistics?.entityCount ?? 0;
3856
+ const totalRelationships = header.manifest?.statistics?.relationshipCount ?? 0;
3857
+ const totalChunks = estimateChunkCount(totalEntities, totalRelationships);
3662
3858
  const entityConflict = options.entityConflict ?? "skip";
3663
3859
  let entitiesImported = 0;
3664
3860
  let entitiesSkipped = 0;
3665
3861
  let relationshipsImported = 0;
3666
3862
  let relationshipsSkipped = 0;
3863
+ let chunksCompleted = 0;
3667
3864
  for await (const chunk of chunks) {
3668
3865
  if (chunk.entities) {
3669
3866
  for (const entity of chunk.entities) {
@@ -3745,6 +3942,16 @@ var RepositoryImporter = class {
3745
3942
  }
3746
3943
  }
3747
3944
  }
3945
+ chunksCompleted++;
3946
+ await this.onProgress?.({
3947
+ repositoryId,
3948
+ entitiesImported,
3949
+ relationshipsImported,
3950
+ totalEntities,
3951
+ totalRelationships,
3952
+ chunksCompleted,
3953
+ totalChunks
3954
+ });
3748
3955
  }
3749
3956
  const vocabExtensions = migrationResult.warnings.filter(
3750
3957
  (w) => w.code === "entity_type_added" || w.code === "relationship_type_added"
@@ -3801,7 +4008,7 @@ var DeepMemory = class {
3801
4008
  graphTraversalProvider;
3802
4009
  provenance;
3803
4010
  globalEventBus;
3804
- initialised = false;
4011
+ initialized = false;
3805
4012
  constructor(config) {
3806
4013
  this.storage = config.storage;
3807
4014
  this.search = config.search;
@@ -3811,16 +4018,16 @@ var DeepMemory = class {
3811
4018
  this.provenance = new ProvenanceTracker(config.provenance);
3812
4019
  this.globalEventBus = new EventBus(config.provenance);
3813
4020
  }
3814
- /** Initialise the storage provider and optional providers (call once before use) */
3815
- async ensureInitialised() {
3816
- if (!this.initialised) {
3817
- if (this.storage.initialise) {
3818
- await this.storage.initialise();
4021
+ /** Initialize the storage provider and optional providers (call once before use) */
4022
+ async ensureInitialized() {
4023
+ if (!this.initialized) {
4024
+ if (this.storage.initialize) {
4025
+ await this.storage.initialize();
3819
4026
  }
3820
- if (this.graphTraversalProvider?.initialise) {
3821
- await this.graphTraversalProvider.initialise();
4027
+ if (this.graphTraversalProvider?.initialize) {
4028
+ await this.graphTraversalProvider.initialize();
3822
4029
  }
3823
- this.initialised = true;
4030
+ this.initialized = true;
3824
4031
  }
3825
4032
  }
3826
4033
  /** Create or migrate the storage schema. Call once at deployment / startup, not per-request. */
@@ -3832,15 +4039,15 @@ var DeepMemory = class {
3832
4039
  schemaVersion: 0
3833
4040
  };
3834
4041
  try {
3835
- await this.ensureInitialised();
4042
+ await this.ensureInitialized();
3836
4043
  } catch {
3837
4044
  }
3838
4045
  let result = noOpResult;
3839
4046
  if (this.storage.ensureSchema) {
3840
4047
  result = await this.storage.ensureSchema();
3841
4048
  }
3842
- if (!this.initialised) {
3843
- await this.ensureInitialised();
4049
+ if (!this.initialized) {
4050
+ await this.ensureInitialized();
3844
4051
  }
3845
4052
  return result;
3846
4053
  }
@@ -3855,7 +4062,7 @@ var DeepMemory = class {
3855
4062
  }
3856
4063
  /** Create a new memory repository */
3857
4064
  async createRepository(config) {
3858
- await this.ensureInitialised();
4065
+ await this.ensureInitialized();
3859
4066
  const repositoryId = config.repositoryId ?? generateId();
3860
4067
  this.validateRepositoryId(repositoryId);
3861
4068
  const context = this.provenance.getContext();
@@ -3912,7 +4119,7 @@ var DeepMemory = class {
3912
4119
  }
3913
4120
  /** Open an existing repository */
3914
4121
  async openRepository(repositoryId) {
3915
- await this.ensureInitialised();
4122
+ await this.ensureInitialized();
3916
4123
  this.validateRepositoryId(repositoryId);
3917
4124
  const storedRepo = await this.storage.getRepository(repositoryId);
3918
4125
  if (!storedRepo) {
@@ -3941,7 +4148,7 @@ var DeepMemory = class {
3941
4148
  }
3942
4149
  /** List all repositories */
3943
4150
  async listRepositories(filter) {
3944
- await this.ensureInitialised();
4151
+ await this.ensureInitialized();
3945
4152
  const result = await this.storage.listRepositories(filter);
3946
4153
  return {
3947
4154
  items: result.items.map((stored) => ({
@@ -3959,7 +4166,7 @@ var DeepMemory = class {
3959
4166
  }
3960
4167
  /** Update repository metadata and settings */
3961
4168
  async updateRepository(repositoryId, updates) {
3962
- await this.ensureInitialised();
4169
+ await this.ensureInitialized();
3963
4170
  this.validateRepositoryId(repositoryId);
3964
4171
  const updated = await this.storage.updateRepository(repositoryId, updates);
3965
4172
  await this.globalEventBus.emit("repository:updated", { repositoryId });
@@ -3967,14 +4174,55 @@ var DeepMemory = class {
3967
4174
  }
3968
4175
  /** Delete a repository */
3969
4176
  async deleteRepository(repositoryId) {
3970
- await this.ensureInitialised();
4177
+ await this.ensureInitialized();
3971
4178
  this.validateRepositoryId(repositoryId);
3972
- await this.storage.deleteRepository(repositoryId);
4179
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4180
+ await this.globalEventBus.emit("delete:started", {
4181
+ repositoryId,
4182
+ totalEntities: stats.entityCount,
4183
+ totalRelationships: stats.relationshipCount
4184
+ });
4185
+ await this.storage.deleteRepository(
4186
+ repositoryId,
4187
+ (progress) => this.globalEventBus.emit("delete:progress", { repositoryId, ...progress })
4188
+ );
4189
+ await this.globalEventBus.emit("delete:completed", {
4190
+ repositoryId,
4191
+ entitiesDeleted: stats.entityCount,
4192
+ relationshipsDeleted: stats.relationshipCount
4193
+ });
3973
4194
  await this.globalEventBus.emit("repository:deleted", { repositoryId });
3974
4195
  }
4196
+ /** Delete all entities and relationships in a repository, preserving the repository and vocabulary */
4197
+ async deleteAllContents(repositoryId) {
4198
+ await this.ensureInitialized();
4199
+ this.validateRepositoryId(repositoryId);
4200
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4201
+ await this.globalEventBus.emit("delete:started", {
4202
+ repositoryId,
4203
+ totalEntities: stats.entityCount,
4204
+ totalRelationships: stats.relationshipCount
4205
+ });
4206
+ const result = await this.storage.deleteAllContents(
4207
+ repositoryId,
4208
+ (progress) => this.globalEventBus.emit("delete:progress", { repositoryId, ...progress })
4209
+ );
4210
+ await this.globalEventBus.emit("delete:completed", {
4211
+ repositoryId,
4212
+ entitiesDeleted: result.deletedEntities,
4213
+ relationshipsDeleted: result.deletedRelationships
4214
+ });
4215
+ return result;
4216
+ }
3975
4217
  /** Export a repository to a portable archive */
3976
4218
  async exportRepository(repositoryId, options) {
3977
- await this.ensureInitialised();
4219
+ await this.ensureInitialized();
4220
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4221
+ await this.globalEventBus.emit("export:started", {
4222
+ repositoryId,
4223
+ totalEntities: stats.entityCount,
4224
+ totalRelationships: stats.relationshipCount
4225
+ });
3978
4226
  const exporter = new RepositoryExporter({
3979
4227
  storage: this.storage,
3980
4228
  provenance: this.provenance.getContext(),
@@ -3990,10 +4238,12 @@ var DeepMemory = class {
3990
4238
  }
3991
4239
  /** Import a repository from a portable archive */
3992
4240
  async importRepository(archive, options) {
3993
- await this.ensureInitialised();
4241
+ await this.ensureInitialized();
4242
+ await this.globalEventBus.emit("import:started", { repositoryId: options.target.repositoryId });
3994
4243
  const importer = new RepositoryImporter({
3995
4244
  storage: this.storage,
3996
- actorId: this.provenance.getContext().actorId
4245
+ actorId: this.provenance.getContext().actorId,
4246
+ onProgress: (progress) => this.globalEventBus.emit("import:progress", progress)
3997
4247
  });
3998
4248
  const result = await importer.import(archive, options);
3999
4249
  if (result.success) {
@@ -4016,21 +4266,50 @@ var DeepMemory = class {
4016
4266
  * Use for large repositories to avoid loading everything into memory.
4017
4267
  */
4018
4268
  async *exportRepositoryStream(repositoryId, options) {
4019
- await this.ensureInitialised();
4269
+ await this.ensureInitialized();
4020
4270
  const exporter = new RepositoryExporter({
4021
4271
  storage: this.storage,
4022
4272
  provenance: this.provenance.getContext(),
4023
4273
  legal: options?.legal
4024
4274
  });
4025
- await this.globalEventBus.emit("export:started", { repositoryId });
4275
+ const stats = await this.storage.getRepositoryStats(repositoryId);
4276
+ const totalEntities = stats.entityCount;
4277
+ const totalRelationships = stats.relationshipCount;
4278
+ const estimatedChunkSize = 100;
4279
+ const totalChunks = Math.max(
4280
+ 1,
4281
+ Math.ceil(totalEntities / estimatedChunkSize) + Math.ceil(totalRelationships / estimatedChunkSize)
4282
+ );
4283
+ await this.globalEventBus.emit("export:started", { repositoryId, totalEntities, totalRelationships });
4026
4284
  let entityCount = 0;
4027
4285
  let relationshipCount = 0;
4286
+ let chunksCompleted = 0;
4028
4287
  for await (const item of exporter.exportStream(repositoryId)) {
4029
4288
  yield item;
4030
4289
  if (item.type === "entities") {
4031
4290
  entityCount += item.data.length;
4291
+ chunksCompleted++;
4292
+ await this.globalEventBus.emit("export:progress", {
4293
+ repositoryId,
4294
+ entitiesExported: entityCount,
4295
+ relationshipsExported: relationshipCount,
4296
+ totalEntities,
4297
+ totalRelationships,
4298
+ chunksCompleted,
4299
+ totalChunks
4300
+ });
4032
4301
  } else if (item.type === "relationships") {
4033
4302
  relationshipCount += item.data.length;
4303
+ chunksCompleted++;
4304
+ await this.globalEventBus.emit("export:progress", {
4305
+ repositoryId,
4306
+ entitiesExported: entityCount,
4307
+ relationshipsExported: relationshipCount,
4308
+ totalEntities,
4309
+ totalRelationships,
4310
+ chunksCompleted,
4311
+ totalChunks
4312
+ });
4034
4313
  }
4035
4314
  }
4036
4315
  await this.globalEventBus.emit("export:completed", {
@@ -4044,10 +4323,11 @@ var DeepMemory = class {
4044
4323
  * Use for large repositories to avoid loading everything into memory.
4045
4324
  */
4046
4325
  async importRepositoryStream(header, chunks, options) {
4047
- await this.ensureInitialised();
4326
+ await this.ensureInitialized();
4048
4327
  const importer = new RepositoryImporter({
4049
4328
  storage: this.storage,
4050
- actorId: this.provenance.getContext().actorId
4329
+ actorId: this.provenance.getContext().actorId,
4330
+ onProgress: (progress) => this.globalEventBus.emit("import:progress", progress)
4051
4331
  });
4052
4332
  await this.globalEventBus.emit("import:started", { repositoryId: options.target.repositoryId });
4053
4333
  const result = await importer.importStream(header, chunks, options);
@@ -4083,7 +4363,7 @@ var DeepMemory = class {
4083
4363
  if (this.storage.dispose) {
4084
4364
  await this.storage.dispose();
4085
4365
  }
4086
- this.initialised = false;
4366
+ this.initialized = false;
4087
4367
  }
4088
4368
  };
4089
4369
 
@@ -4098,7 +4378,7 @@ var InMemoryStorageProvider = class {
4098
4378
  return store;
4099
4379
  }
4100
4380
  // ─── Lifecycle ─────────────────────────────────────────────────────
4101
- async initialise() {
4381
+ async initialize() {
4102
4382
  }
4103
4383
  async dispose() {
4104
4384
  this.stores.clear();
@@ -4171,12 +4451,21 @@ var InMemoryStorageProvider = class {
4171
4451
  if (updates.metadata !== void 0) repo.metadata = { ...repo.metadata, ...updates.metadata };
4172
4452
  return repo;
4173
4453
  }
4174
- async deleteRepository(repositoryId) {
4454
+ async deleteRepository(repositoryId, _onProgress) {
4175
4455
  if (!this.stores.has(repositoryId)) {
4176
4456
  throw new RepositoryNotFoundError(repositoryId);
4177
4457
  }
4178
4458
  this.stores.delete(repositoryId);
4179
4459
  }
4460
+ async deleteAllContents(repositoryId, _onProgress) {
4461
+ const store = this.getStore(repositoryId);
4462
+ const deletedEntities = store.entities.size;
4463
+ const deletedRelationships = store.relationships.size;
4464
+ store.entities.clear();
4465
+ store.slugIndex.clear();
4466
+ store.relationships.clear();
4467
+ return { deletedEntities, deletedRelationships };
4468
+ }
4180
4469
  async getRepositoryStats(repositoryId) {
4181
4470
  const store = this.getStore(repositoryId);
4182
4471
  const entityTypeBreakdown = {};
@@ -4406,7 +4695,7 @@ var InMemoryStorageProvider = class {
4406
4695
  return { deletedRelationships };
4407
4696
  }
4408
4697
  // ─── Graph Traversal ───────────────────────────────────────────────
4409
- async exploreNeighbourhood(repositoryId, entityId, options) {
4698
+ async exploreNeighborhood(repositoryId, entityId, options) {
4410
4699
  const store = this.getStore(repositoryId);
4411
4700
  if (!store.entities.has(entityId)) {
4412
4701
  throw new EntityNotFoundError(entityId);
@@ -4472,7 +4761,7 @@ var InMemoryStorageProvider = class {
4472
4761
  if (nextFrontier.size === 0) break;
4473
4762
  }
4474
4763
  return {
4475
- centreId: entityId,
4764
+ centerId: entityId,
4476
4765
  layers
4477
4766
  };
4478
4767
  }
@@ -4631,7 +4920,7 @@ var InMemoryStorageProvider = class {
4631
4920
  yield { type: "relationships", data: [], sequence: 0, isLast: true };
4632
4921
  }
4633
4922
  }
4634
- async importBulk(repositoryId, data) {
4923
+ async importBulk(repositoryId, data, _options) {
4635
4924
  const store = this.getStore(repositoryId);
4636
4925
  let entitiesImported = 0;
4637
4926
  let relationshipsImported = 0;