@utaba/deep-memory-storage-cosmosdb 0.15.0 → 0.17.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
@@ -36,13 +36,16 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/CosmosDbProvider.ts
39
- var import_node_crypto = __toESM(require("crypto"), 1);
40
- var import_deep_memory6 = require("@utaba/deep-memory");
39
+ var import_deep_memory5 = require("@utaba/deep-memory");
41
40
 
42
41
  // src/CosmosDbConnection.ts
43
42
  var import_gremlin = __toESM(require("gremlin"), 1);
43
+
44
+ // src/usage.ts
44
45
  var import_node_async_hooks = require("async_hooks");
45
46
  var usageScope = new import_node_async_hooks.AsyncLocalStorage();
47
+
48
+ // src/CosmosDbConnection.ts
46
49
  var CosmosDbConnection = class {
47
50
  client = null;
48
51
  config;
@@ -138,14 +141,220 @@ function getRetryAfterMs(err, attempt) {
138
141
  function extractRequestCharge(resultSet) {
139
142
  const attrs = resultSet.attributes;
140
143
  if (!attrs) return void 0;
141
- const charge = attrs["x-ms-request-charge"] ?? attrs["x-ms-total-request-charge"];
142
- return typeof charge === "number" ? charge : void 0;
144
+ const total = attrs["x-ms-total-request-charge"];
145
+ if (typeof total === "number") return total;
146
+ const single = attrs["x-ms-request-charge"];
147
+ if (typeof single === "number") return single;
148
+ return void 0;
143
149
  }
144
150
  function sleep(ms) {
145
151
  return new Promise((resolve) => setTimeout(resolve, ms));
146
152
  }
147
153
 
154
+ // src/cosmos-rest-auth.ts
155
+ var import_node_crypto = __toESM(require("crypto"), 1);
156
+ function cosmosAuthToken(verb, resourceType, resourceLink, date, key) {
157
+ const payload = `${verb.toLowerCase()}
158
+ ${resourceType.toLowerCase()}
159
+ ${resourceLink}
160
+ ${date.toLowerCase()}
161
+
162
+ `;
163
+ const keyBuffer = Buffer.from(key, "base64");
164
+ const hmac = import_node_crypto.default.createHmac("sha256", keyBuffer);
165
+ hmac.update(payload);
166
+ const signature = hmac.digest("base64");
167
+ return encodeURIComponent(`type=master&ver=1.0&sig=${signature}`);
168
+ }
169
+ async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType, body, rejectUnauthorized) {
170
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
171
+ const token = cosmosAuthToken("post", resourceType, resourceLink, date, key);
172
+ const url = `${restBase}/${urlPath}`;
173
+ const options = {
174
+ method: "POST",
175
+ headers: {
176
+ "Authorization": token,
177
+ "x-ms-version": "2018-12-31",
178
+ "x-ms-date": date,
179
+ "Content-Type": "application/json"
180
+ },
181
+ body: JSON.stringify(body)
182
+ };
183
+ if (!rejectUnauthorized) {
184
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
185
+ }
186
+ const response = await fetch(url, options);
187
+ if (response.status === 201) return true;
188
+ if (response.status === 409) return false;
189
+ const text = await response.text();
190
+ throw new Error(`CosmosDB REST ${response.status}: ${text}`);
191
+ }
192
+
193
+ // src/CosmosDocumentClient.ts
194
+ var CosmosDocumentClient = class {
195
+ config;
196
+ fetchImpl;
197
+ /**
198
+ * `fetchImpl` is for tests only — production code should pass nothing and
199
+ * inherit `globalThis.fetch`. Keeping it on the constructor (rather than
200
+ * stubbing globals) means each test instance is hermetic.
201
+ */
202
+ constructor(config, fetchImpl) {
203
+ this.config = {
204
+ ...config,
205
+ maxRetries: config.maxRetries ?? 3,
206
+ defaultTimeoutMs: config.defaultTimeoutMs ?? 3e4
207
+ };
208
+ this.fetchImpl = fetchImpl ?? fetch;
209
+ }
210
+ /**
211
+ * Execute a parameterised Cosmos SQL query against the container's `docs`
212
+ * resource. Retries on 429/503 with the response's `x-ms-retry-after-ms`
213
+ * when present, otherwise exponential backoff. RU is accumulated into the
214
+ * active {@link usageScope}.
215
+ */
216
+ async query(sql, parameters, options) {
217
+ const resourceLink = `dbs/${this.config.database}/colls/${this.config.container}`;
218
+ const url = `${this.restBase()}/${resourceLink}/docs`;
219
+ if (!this.config.rejectUnauthorized) {
220
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
221
+ }
222
+ const body = JSON.stringify({ query: sql, parameters });
223
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
224
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
225
+ const token = cosmosAuthToken("post", "docs", resourceLink, date, this.config.key);
226
+ const headers = {
227
+ "Authorization": token,
228
+ "x-ms-version": "2018-12-31",
229
+ "x-ms-date": date,
230
+ "Content-Type": "application/query+json",
231
+ "x-ms-documentdb-isquery": "true"
232
+ };
233
+ if (options.partitionKey != null) {
234
+ headers["x-ms-documentdb-partitionkey"] = JSON.stringify([options.partitionKey]);
235
+ } else {
236
+ headers["x-ms-documentdb-query-enablecrosspartition"] = "true";
237
+ }
238
+ if (options.populateMetrics) {
239
+ headers["x-ms-documentdb-populatequerymetrics"] = "true";
240
+ }
241
+ if (options.continuationToken) {
242
+ headers["x-ms-continuation"] = options.continuationToken;
243
+ }
244
+ const response = await this.fetchImpl(url, { method: "POST", headers, body });
245
+ if (response.status === 429 || response.status === 503) {
246
+ if (attempt < this.config.maxRetries) {
247
+ const waitMs = parseRetryAfterMs(response, attempt);
248
+ const acc2 = usageScope.getStore();
249
+ if (acc2) acc2.retries++;
250
+ await sleep2(waitMs);
251
+ continue;
252
+ }
253
+ const text = await response.text();
254
+ throw new Error(`CosmosDB Document query ${response.status}: ${text}`);
255
+ }
256
+ if (!response.ok) {
257
+ const text = await response.text();
258
+ throw new Error(`CosmosDB Document query ${response.status}: ${text}`);
259
+ }
260
+ const json = await response.json();
261
+ const requestCharge = Number(response.headers.get("x-ms-request-charge") ?? "0") || 0;
262
+ const queryMetrics = response.headers.get("x-ms-documentdb-query-metrics");
263
+ const continuationToken = response.headers.get("x-ms-continuation");
264
+ const acc = usageScope.getStore();
265
+ if (acc) {
266
+ acc.calls++;
267
+ acc.ru += requestCharge;
268
+ }
269
+ return {
270
+ documents: json.Documents ?? [],
271
+ requestCharge,
272
+ queryMetrics,
273
+ continuationToken
274
+ };
275
+ }
276
+ throw new Error("CosmosDocumentClient.query: retry loop exhausted without resolution");
277
+ }
278
+ /**
279
+ * Read container metadata (id, partition key, indexing policy). Used by
280
+ * `ensureSchema()` to warn when externally-provisioned containers have
281
+ * excluded paths the findEntities SQL needs.
282
+ */
283
+ async getContainerProperties() {
284
+ const resourceLink = `dbs/${this.config.database}/colls/${this.config.container}`;
285
+ const url = `${this.restBase()}/${resourceLink}`;
286
+ if (!this.config.rejectUnauthorized) {
287
+ process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
288
+ }
289
+ const date = (/* @__PURE__ */ new Date()).toUTCString();
290
+ const token = cosmosAuthToken("get", "colls", resourceLink, date, this.config.key);
291
+ const response = await this.fetchImpl(url, {
292
+ method: "GET",
293
+ headers: {
294
+ "Authorization": token,
295
+ "x-ms-version": "2018-12-31",
296
+ "x-ms-date": date
297
+ }
298
+ });
299
+ if (!response.ok) {
300
+ const text = await response.text();
301
+ throw new Error(`CosmosDB Document getContainerProperties ${response.status}: ${text}`);
302
+ }
303
+ return await response.json();
304
+ }
305
+ restBase() {
306
+ return this.config.restEndpoint.replace(/\/+$/, "");
307
+ }
308
+ };
309
+ function parseRetryAfterMs(response, attempt) {
310
+ const header = response.headers.get("x-ms-retry-after-ms");
311
+ if (header) {
312
+ const n = Number(header);
313
+ if (Number.isFinite(n) && n > 0) return n;
314
+ }
315
+ return Math.min(500 * Math.pow(2, attempt), 1e4);
316
+ }
317
+ function sleep2(ms) {
318
+ return new Promise((resolve) => setTimeout(resolve, ms));
319
+ }
320
+
148
321
  // src/mapping.ts
322
+ var STORED_ENTITY_FIELDS = [
323
+ "id",
324
+ "entityType",
325
+ "entityLabel",
326
+ "slug",
327
+ "summary",
328
+ "properties",
329
+ "data",
330
+ "dataFormat",
331
+ "createdBy",
332
+ "createdByType",
333
+ "createdAt",
334
+ "createdInConversation",
335
+ "createdFromMessage",
336
+ "modifiedBy",
337
+ "modifiedByType",
338
+ "modifiedAt",
339
+ "modifiedInConversation",
340
+ "modifiedFromMessage"
341
+ ];
342
+ function buildRepositoryProjectChain() {
343
+ return [
344
+ `project('id','repositoryId','repoLabel','description','type','legal','owner','governanceConfig','metadata','createdAt','createdBy')`,
345
+ `.by(id)`,
346
+ `.by('repositoryId')`,
347
+ `.by('repoLabel')`,
348
+ `.by(coalesce(values('description'), constant('')))`,
349
+ `.by(coalesce(values('type'), constant('')))`,
350
+ `.by(coalesce(values('legal'), constant('')))`,
351
+ `.by(coalesce(values('owner'), constant('')))`,
352
+ `.by('governanceConfig')`,
353
+ `.by(coalesce(values('metadata'), constant('')))`,
354
+ `.by('createdAt')`,
355
+ `.by('createdBy')`
356
+ ].join("");
357
+ }
149
358
  function unwrap(val) {
150
359
  if (Array.isArray(val) && val.length > 0) return val[0];
151
360
  return val;
@@ -197,6 +406,54 @@ function entityFromGremlin(props) {
197
406
  embedding: embeddingStr ? safeParseJson(embeddingStr, void 0) : void 0
198
407
  };
199
408
  }
409
+ function pluckDocValue(doc, key) {
410
+ const arr = doc[key];
411
+ if (Array.isArray(arr) && arr.length > 0) {
412
+ const entry = arr[0];
413
+ if (entry && typeof entry === "object") {
414
+ return entry["_value"];
415
+ }
416
+ }
417
+ return void 0;
418
+ }
419
+ function pluckDocStr(doc, key) {
420
+ const v = pluckDocValue(doc, key);
421
+ return typeof v === "string" ? v : String(v ?? "");
422
+ }
423
+ function pluckDocOptStr(doc, key) {
424
+ const v = pluckDocValue(doc, key);
425
+ return v != null && v !== "" ? String(v) : void 0;
426
+ }
427
+ function provenanceFromDocument(doc) {
428
+ return {
429
+ createdBy: pluckDocStr(doc, "createdBy"),
430
+ createdByType: pluckDocStr(doc, "createdByType") || "agent",
431
+ createdAt: pluckDocStr(doc, "createdAt"),
432
+ createdInConversation: pluckDocOptStr(doc, "createdInConversation"),
433
+ createdFromMessage: pluckDocOptStr(doc, "createdFromMessage"),
434
+ modifiedBy: pluckDocStr(doc, "modifiedBy"),
435
+ modifiedByType: pluckDocStr(doc, "modifiedByType") || "agent",
436
+ modifiedAt: pluckDocStr(doc, "modifiedAt"),
437
+ modifiedInConversation: pluckDocOptStr(doc, "modifiedInConversation"),
438
+ modifiedFromMessage: pluckDocOptStr(doc, "modifiedFromMessage")
439
+ };
440
+ }
441
+ function entityFromDocument(doc) {
442
+ const id = typeof doc["id"] === "string" ? doc["id"] : String(doc["id"] ?? "");
443
+ const embeddingStr = pluckDocOptStr(doc, "embedding");
444
+ return {
445
+ id,
446
+ slug: pluckDocStr(doc, "slug"),
447
+ entityType: pluckDocStr(doc, "entityType"),
448
+ label: pluckDocStr(doc, "entityLabel"),
449
+ summary: pluckDocOptStr(doc, "summary"),
450
+ properties: safeParseJson(pluckDocValue(doc, "properties"), {}),
451
+ data: pluckDocOptStr(doc, "data"),
452
+ dataFormat: pluckDocOptStr(doc, "dataFormat"),
453
+ provenance: provenanceFromDocument(doc),
454
+ embedding: embeddingStr ? safeParseJson(embeddingStr, void 0) : void 0
455
+ };
456
+ }
200
457
  function relationshipFromGremlin(props) {
201
458
  const bidir = unwrap(props["bidirectional"]);
202
459
  return {
@@ -223,24 +480,6 @@ function repositoryFromGremlin(props) {
223
480
  createdBy: unwrapStr(props["createdBy"])
224
481
  };
225
482
  }
226
- function repositorySummaryFromGremlin(props) {
227
- return {
228
- repositoryId: unwrapStr(props["repositoryId"]),
229
- type: unwrapOptStr(props["type"]),
230
- label: unwrapStr(props["repoLabel"]),
231
- description: unwrapOptStr(props["description"]),
232
- governanceConfig: safeParseJson(unwrap(props["governanceConfig"]), { mode: "open" })
233
- };
234
- }
235
- function vocabularyFromGremlin(props) {
236
- return safeParseJson(unwrap(props["vocabulary"]), {
237
- version: "0.0.0",
238
- lastModified: (/* @__PURE__ */ new Date()).toISOString(),
239
- modifiedBy: "system",
240
- entityTypes: [],
241
- relationshipTypes: []
242
- });
243
- }
244
483
  function changeRecordFromGremlin(props) {
245
484
  return {
246
485
  changeId: unwrapStr(props["changeId"]),
@@ -255,9 +494,97 @@ function changeRecordFromGremlin(props) {
255
494
  reason: unwrapStr(props["reason"])
256
495
  };
257
496
  }
258
- function entityToGremlinProps(repositoryId, entity) {
259
- const props = {
260
- repositoryId,
497
+ var ABSENT_STRING_SENTINEL = "";
498
+ var SENTINEL_BINDING = "absentSentinel";
499
+ var ENTITY_REQUIRED_SLOTS = [
500
+ "entityType",
501
+ "entityLabel",
502
+ "slug",
503
+ "properties",
504
+ "createdBy",
505
+ "createdByType",
506
+ "createdAt",
507
+ "modifiedBy",
508
+ "modifiedByType",
509
+ "modifiedAt"
510
+ ];
511
+ var ENTITY_OPTIONAL_SLOTS = [
512
+ "summary",
513
+ "data",
514
+ "dataFormat",
515
+ "embedding",
516
+ "createdInConversation",
517
+ "createdFromMessage",
518
+ "modifiedInConversation",
519
+ "modifiedFromMessage"
520
+ ];
521
+ var RELATIONSHIP_REQUIRED_SLOTS = [
522
+ "relationshipType",
523
+ "sourceEntityId",
524
+ "targetEntityId",
525
+ "bidirectional",
526
+ "properties",
527
+ "createdBy",
528
+ "createdByType",
529
+ "createdAt",
530
+ "modifiedBy",
531
+ "modifiedByType",
532
+ "modifiedAt"
533
+ ];
534
+ var RELATIONSHIP_OPTIONAL_SLOTS = [
535
+ "createdInConversation",
536
+ "createdFromMessage",
537
+ "modifiedInConversation",
538
+ "modifiedFromMessage"
539
+ ];
540
+ var REPOSITORY_REQUIRED_SLOTS = [
541
+ "repoLabel",
542
+ "governanceConfig",
543
+ "createdAt",
544
+ "createdBy"
545
+ ];
546
+ var REPOSITORY_OPTIONAL_SLOTS = [
547
+ "description",
548
+ "type",
549
+ "legal",
550
+ "owner",
551
+ "metadata"
552
+ ];
553
+ function buildLadder(required, optional, paramPrefix) {
554
+ const parts = [];
555
+ let i = 0;
556
+ for (const slot of required) {
557
+ parts.push(`.property('${slot}', ${paramPrefix}${i++})`);
558
+ }
559
+ for (const slot of optional) {
560
+ parts.push(
561
+ `.choose(__.constant(${paramPrefix}${i}).is(neq(${SENTINEL_BINDING})), __.property('${slot}', ${paramPrefix}${i}), __.identity())`
562
+ );
563
+ i++;
564
+ }
565
+ return parts.join("");
566
+ }
567
+ function buildLadderBindings(required, optional, paramPrefix, values) {
568
+ const bindings = {};
569
+ let i = 0;
570
+ for (const slot of required) {
571
+ const v = values[slot];
572
+ if (v == null) {
573
+ throw new Error(`Fixed-shape ladder: required slot '${slot}' is null/undefined`);
574
+ }
575
+ bindings[`${paramPrefix}${i++}`] = v;
576
+ }
577
+ for (const slot of optional) {
578
+ const v = values[slot];
579
+ bindings[`${paramPrefix}${i++}`] = v ?? ABSENT_STRING_SENTINEL;
580
+ }
581
+ return bindings;
582
+ }
583
+ function buildEntityPropertyLadder() {
584
+ return buildLadder(ENTITY_REQUIRED_SLOTS, ENTITY_OPTIONAL_SLOTS, "p");
585
+ }
586
+ function entityToLadderBindings(entity) {
587
+ const bindings = buildLadderBindings(ENTITY_REQUIRED_SLOTS, ENTITY_OPTIONAL_SLOTS, "p", {
261
588
  entityType: entity.entityType,
262
589
  entityLabel: entity.label,
263
590
  slug: entity.slug,
@@ -267,46 +594,120 @@ function entityToGremlinProps(repositoryId, entity) {
267
594
  createdAt: entity.provenance.createdAt,
268
595
  modifiedBy: entity.provenance.modifiedBy,
269
596
  modifiedByType: entity.provenance.modifiedByType,
270
- modifiedAt: entity.provenance.modifiedAt
271
- };
272
- if (entity.summary != null) props["summary"] = entity.summary;
273
- if (entity.data != null) props["data"] = entity.data;
274
- if (entity.dataFormat != null) props["dataFormat"] = entity.dataFormat;
275
- if (entity.provenance.createdInConversation != null) props["createdInConversation"] = entity.provenance.createdInConversation;
276
- if (entity.provenance.createdFromMessage != null) props["createdFromMessage"] = entity.provenance.createdFromMessage;
277
- if (entity.provenance.modifiedInConversation != null) props["modifiedInConversation"] = entity.provenance.modifiedInConversation;
278
- if (entity.provenance.modifiedFromMessage != null) props["modifiedFromMessage"] = entity.provenance.modifiedFromMessage;
279
- if (entity.embedding != null) props["embedding"] = JSON.stringify(entity.embedding);
280
- return props;
281
- }
282
- function relationshipToGremlinProps(repositoryId, rel) {
283
- const props = {
284
- repositoryId,
285
- relationshipType: rel.relationshipType,
286
- sourceEntityId: rel.sourceEntityId,
287
- targetEntityId: rel.targetEntityId,
288
- bidirectional: rel.bidirectional,
289
- properties: JSON.stringify(rel.properties ?? {}),
290
- createdBy: rel.provenance.createdBy,
291
- createdByType: rel.provenance.createdByType,
292
- createdAt: rel.provenance.createdAt,
293
- modifiedBy: rel.provenance.modifiedBy,
294
- modifiedByType: rel.provenance.modifiedByType,
295
- modifiedAt: rel.provenance.modifiedAt
296
- };
297
- if (rel.provenance.createdInConversation != null) props["createdInConversation"] = rel.provenance.createdInConversation;
298
- if (rel.provenance.createdFromMessage != null) props["createdFromMessage"] = rel.provenance.createdFromMessage;
299
- if (rel.provenance.modifiedInConversation != null) props["modifiedInConversation"] = rel.provenance.modifiedInConversation;
300
- if (rel.provenance.modifiedFromMessage != null) props["modifiedFromMessage"] = rel.provenance.modifiedFromMessage;
301
- return props;
597
+ modifiedAt: entity.provenance.modifiedAt,
598
+ summary: entity.summary,
599
+ data: entity.data,
600
+ dataFormat: entity.dataFormat,
601
+ embedding: entity.embedding != null ? JSON.stringify(entity.embedding) : void 0,
602
+ createdInConversation: entity.provenance.createdInConversation,
603
+ createdFromMessage: entity.provenance.createdFromMessage,
604
+ modifiedInConversation: entity.provenance.modifiedInConversation,
605
+ modifiedFromMessage: entity.provenance.modifiedFromMessage
606
+ });
607
+ bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
608
+ return bindings;
609
+ }
610
+ function buildRelationshipPropertyLadder() {
611
+ return buildLadder(RELATIONSHIP_REQUIRED_SLOTS, RELATIONSHIP_OPTIONAL_SLOTS, "p");
612
+ }
613
+ function relationshipToLadderBindings(rel) {
614
+ const bindings = buildLadderBindings(
615
+ RELATIONSHIP_REQUIRED_SLOTS,
616
+ RELATIONSHIP_OPTIONAL_SLOTS,
617
+ "p",
618
+ {
619
+ relationshipType: rel.relationshipType,
620
+ sourceEntityId: rel.sourceEntityId,
621
+ targetEntityId: rel.targetEntityId,
622
+ bidirectional: rel.bidirectional,
623
+ properties: JSON.stringify(rel.properties ?? {}),
624
+ createdBy: rel.provenance.createdBy,
625
+ createdByType: rel.provenance.createdByType,
626
+ createdAt: rel.provenance.createdAt,
627
+ modifiedBy: rel.provenance.modifiedBy,
628
+ modifiedByType: rel.provenance.modifiedByType,
629
+ modifiedAt: rel.provenance.modifiedAt,
630
+ createdInConversation: rel.provenance.createdInConversation,
631
+ createdFromMessage: rel.provenance.createdFromMessage,
632
+ modifiedInConversation: rel.provenance.modifiedInConversation,
633
+ modifiedFromMessage: rel.provenance.modifiedFromMessage
634
+ }
635
+ );
636
+ bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
637
+ return bindings;
638
+ }
639
+ function buildRepositoryPropertyLadder() {
640
+ return buildLadder(REPOSITORY_REQUIRED_SLOTS, REPOSITORY_OPTIONAL_SLOTS, "p");
641
+ }
642
+ function repositoryConfigToLadderBindings(config) {
643
+ const bindings = buildLadderBindings(
644
+ REPOSITORY_REQUIRED_SLOTS,
645
+ REPOSITORY_OPTIONAL_SLOTS,
646
+ "p",
647
+ {
648
+ repoLabel: config.label,
649
+ governanceConfig: JSON.stringify(config.governanceConfig),
650
+ createdAt: config.createdAt,
651
+ createdBy: config.createdBy,
652
+ description: config.description,
653
+ type: config.type,
654
+ legal: config.legal,
655
+ owner: config.owner,
656
+ metadata: config.metadata != null ? JSON.stringify(config.metadata) : void 0
657
+ }
658
+ );
659
+ bindings[SENTINEL_BINDING] = ABSENT_STRING_SENTINEL;
660
+ return bindings;
302
661
  }
303
662
 
304
663
  // src/queries/repository.ts
305
664
  var import_deep_memory = require("@utaba/deep-memory");
306
665
  var REPO_LABEL = "_repository";
666
+ var REPOSITORY_INDEX_VERTEX_ID = "_repository_index";
667
+ var REPOSITORY_INDEX_PARTITION = "_index";
668
+ var REPOSITORY_INDEX_LABEL = "_repository_index";
307
669
  function repoVertexId(repositoryId) {
308
670
  return `repo:${repositoryId}`;
309
671
  }
672
+ async function ensureRepositoryIndex(conn) {
673
+ const existing = await conn.submit(
674
+ "g.V().has('repositoryId', pk).hasId(sid).count()",
675
+ { pk: REPOSITORY_INDEX_PARTITION, sid: REPOSITORY_INDEX_VERTEX_ID }
676
+ );
677
+ if (Number(existing.items[0] ?? 0) > 0) {
678
+ return null;
679
+ }
680
+ const scan = await conn.submit(
681
+ "g.V().hasLabel('_repository').values('repositoryId')",
682
+ {}
683
+ );
684
+ const ids = scan.items.map((item) => typeof item === "string" ? item : String(item ?? "")).filter((id) => id.length > 0);
685
+ await conn.submit(
686
+ "g.addV('" + REPOSITORY_INDEX_LABEL + "').property('id', sid).property('repositoryId', pk).property('repositoryIds', initial)",
687
+ {
688
+ pk: REPOSITORY_INDEX_PARTITION,
689
+ sid: REPOSITORY_INDEX_VERTEX_ID,
690
+ initial: JSON.stringify(ids)
691
+ }
692
+ );
693
+ return ids.length;
694
+ }
695
+ async function readRepositoryIndex(conn) {
696
+ const result = await conn.submit(
697
+ "g.V().has('repositoryId', pk).hasId(sid).values('repositoryIds')",
698
+ { pk: REPOSITORY_INDEX_PARTITION, sid: REPOSITORY_INDEX_VERTEX_ID }
699
+ );
700
+ if (result.items.length === 0) return [];
701
+ const raw = result.items[0];
702
+ const json = typeof raw === "string" ? raw : String(raw ?? "");
703
+ if (!json) return [];
704
+ try {
705
+ const parsed = JSON.parse(json);
706
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
707
+ } catch {
708
+ return [];
709
+ }
710
+ }
310
711
  function propertyChain(bindings, props, startIndex) {
311
712
  const parts = [];
312
713
  let idx = startIndex;
@@ -318,33 +719,27 @@ function propertyChain(bindings, props, startIndex) {
318
719
  }
319
720
  return { chain: parts.join(""), nextIndex: idx };
320
721
  }
722
+ var REPOSITORY_CREATE_QUERY = `g.addV('${REPO_LABEL}').property('id', vid).property('repositoryId', rid)${buildRepositoryPropertyLadder()}.sideEffect(__.V().has('repositoryId', pk).hasId(sid).property('repositoryIds', updatedIndex))`;
321
723
  async function createRepository(conn, config) {
322
724
  const vertexId = repoVertexId(config.repositoryId);
323
725
  const existing = await conn.submit(
324
- "g.V().has('id', vid).has('label', lbl).count()",
325
- { vid: vertexId, lbl: REPO_LABEL }
726
+ "g.V().has('repositoryId', rid).hasId(vid).has('label', lbl).count()",
727
+ { vid: vertexId, rid: config.repositoryId, lbl: REPO_LABEL }
326
728
  );
327
729
  if (existing.items.length > 0 && Number(existing.items[0]) > 0) {
328
730
  throw new import_deep_memory.DuplicateRepositoryError(config.repositoryId);
329
731
  }
732
+ const currentIds = await readRepositoryIndex(conn);
733
+ const updatedIds = currentIds.includes(config.repositoryId) ? currentIds : [...currentIds, config.repositoryId];
330
734
  const bindings = {
331
735
  vid: vertexId,
332
- rid: config.repositoryId
736
+ rid: config.repositoryId,
737
+ pk: REPOSITORY_INDEX_PARTITION,
738
+ sid: REPOSITORY_INDEX_VERTEX_ID,
739
+ updatedIndex: JSON.stringify(updatedIds),
740
+ ...repositoryConfigToLadderBindings(config)
333
741
  };
334
- const props = {
335
- repoLabel: config.label,
336
- description: config.description,
337
- type: config.type,
338
- legal: config.legal,
339
- owner: config.owner,
340
- governanceConfig: JSON.stringify(config.governanceConfig),
341
- metadata: config.metadata ? JSON.stringify(config.metadata) : void 0,
342
- createdAt: config.createdAt,
343
- createdBy: config.createdBy
344
- };
345
- const { chain } = propertyChain(bindings, props, 0);
346
- const query = `g.addV('${REPO_LABEL}').property('id', vid).property('repositoryId', rid)${chain}`;
347
- await conn.submit(query, bindings);
742
+ await conn.submit(REPOSITORY_CREATE_QUERY, bindings);
348
743
  return {
349
744
  repositoryId: config.repositoryId,
350
745
  type: config.type,
@@ -359,9 +754,10 @@ async function createRepository(conn, config) {
359
754
  };
360
755
  }
361
756
  async function getRepository(conn, repositoryId) {
757
+ const projection = buildRepositoryProjectChain();
362
758
  const result = await conn.submit(
363
- "g.V().has('id', vid).hasLabel('_repository').valueMap(true)",
364
- { vid: repoVertexId(repositoryId) }
759
+ `g.V().has('repositoryId', rid).hasId(vid).hasLabel('_repository').${projection}`,
760
+ { vid: repoVertexId(repositoryId), rid: repositoryId }
365
761
  );
366
762
  if (result.items.length === 0) return null;
367
763
  return repositoryFromGremlin(result.items[0]);
@@ -369,27 +765,32 @@ async function getRepository(conn, repositoryId) {
369
765
  async function listRepositories(conn, filter) {
370
766
  const limit = filter?.limit ?? 20;
371
767
  const offset = filter?.offset ?? 0;
372
- let countQuery = "g.V().hasLabel('_repository')";
373
- let dataQuery = "g.V().hasLabel('_repository')";
374
- const bindings = {};
375
- if (filter?.type) {
376
- countQuery += ".has('type', filterType)";
377
- dataQuery += ".has('type', filterType)";
378
- bindings["filterType"] = filter.type;
768
+ const repositoryIds = await readRepositoryIndex(conn);
769
+ if (repositoryIds.length === 0) {
770
+ return { items: [], total: 0, hasMore: false, limit, offset };
379
771
  }
380
- const countResult = await conn.submit(`${countQuery}.count()`, bindings);
381
- const total = Number(countResult.items[0] ?? 0);
382
- bindings["rangeStart"] = offset;
383
- bindings["rangeEnd"] = offset + limit;
384
- const dataResult = await conn.submit(
385
- `${dataQuery}.range(rangeStart, rangeEnd).valueMap(true)`,
386
- bindings
772
+ const hydrated = await Promise.all(
773
+ repositoryIds.map((rid) => getRepository(conn, rid))
387
774
  );
388
- const items = dataResult.items.map(repositorySummaryFromGremlin);
775
+ let summaries = hydrated.filter((r) => r != null).map((r) => {
776
+ const summary = {
777
+ repositoryId: r.repositoryId,
778
+ label: r.label,
779
+ governanceConfig: r.governanceConfig
780
+ };
781
+ if (r.type !== void 0) summary.type = r.type;
782
+ if (r.description !== void 0) summary.description = r.description;
783
+ return summary;
784
+ });
785
+ if (filter?.type) {
786
+ summaries = summaries.filter((s) => s.type === filter.type);
787
+ }
788
+ const total = summaries.length;
789
+ const items = summaries.slice(offset, offset + limit);
389
790
  return {
390
791
  items,
391
792
  total,
392
- hasMore: offset + limit < total,
793
+ hasMore: offset + items.length < total,
393
794
  limit,
394
795
  offset
395
796
  };
@@ -398,7 +799,7 @@ async function updateRepository(conn, repositoryId, updates) {
398
799
  const vertexId = repoVertexId(repositoryId);
399
800
  const existing = await getRepository(conn, repositoryId);
400
801
  if (!existing) throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
401
- const bindings = { vid: vertexId };
802
+ const bindings = { vid: vertexId, rid: repositoryId };
402
803
  const props = {};
403
804
  if (updates.label !== void 0) props["repoLabel"] = updates.label;
404
805
  if (updates.description !== void 0) props["description"] = updates.description;
@@ -412,7 +813,7 @@ async function updateRepository(conn, repositoryId, updates) {
412
813
  }
413
814
  if (Object.keys(props).length === 0) return existing;
414
815
  const { chain } = propertyChain(bindings, props, 0);
415
- const query = `g.V().has('id', vid).hasLabel('_repository')${chain}`;
816
+ const query = `g.V().has('repositoryId', rid).hasId(vid).hasLabel('_repository')${chain}`;
416
817
  await conn.submit(query, bindings);
417
818
  return await getRepository(conn, repositoryId);
418
819
  }
@@ -458,6 +859,18 @@ async function deleteRepository(conn, repositoryId, onProgress) {
458
859
  await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
459
860
  if (remainingCount === 0) break;
460
861
  }
862
+ const currentIds = await readRepositoryIndex(conn);
863
+ const updatedIds = currentIds.filter((id) => id !== repositoryId);
864
+ if (updatedIds.length !== currentIds.length) {
865
+ await conn.submit(
866
+ "g.V().has('repositoryId', pk).hasId(sid).property('repositoryIds', updatedIndex)",
867
+ {
868
+ pk: REPOSITORY_INDEX_PARTITION,
869
+ sid: REPOSITORY_INDEX_VERTEX_ID,
870
+ updatedIndex: JSON.stringify(updatedIds)
871
+ }
872
+ );
873
+ }
461
874
  }
462
875
  async function deleteAllContents(conn, repositoryId, onProgress) {
463
876
  const entityCountResult = await conn.submit(
@@ -554,33 +967,39 @@ async function getRepositoryStats(conn, repositoryId) {
554
967
  function vocabVertexId(repositoryId) {
555
968
  return `vocab:${repositoryId}`;
556
969
  }
970
+ var EMPTY_VOCABULARY = () => ({
971
+ version: "0.0.0",
972
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
973
+ modifiedBy: "system",
974
+ entityTypes: [],
975
+ relationshipTypes: []
976
+ });
557
977
  async function getVocabulary(conn, repositoryId) {
558
978
  const result = await conn.submit(
559
- "g.V().has('id', vid).hasLabel('_vocabulary').valueMap(true)",
560
- { vid: vocabVertexId(repositoryId) }
979
+ "g.V().has('repositoryId', rid).hasId(vid).hasLabel('_vocabulary').values('vocabulary').limit(1)",
980
+ { vid: vocabVertexId(repositoryId), rid: repositoryId }
561
981
  );
562
- if (result.items.length === 0) {
563
- return {
564
- version: "0.0.0",
565
- lastModified: (/* @__PURE__ */ new Date()).toISOString(),
566
- modifiedBy: "system",
567
- entityTypes: [],
568
- relationshipTypes: []
569
- };
982
+ if (result.items.length === 0) return EMPTY_VOCABULARY();
983
+ const raw = result.items[0];
984
+ const json = typeof raw === "string" ? raw : String(raw ?? "");
985
+ if (!json) return EMPTY_VOCABULARY();
986
+ try {
987
+ return JSON.parse(json);
988
+ } catch {
989
+ return EMPTY_VOCABULARY();
570
990
  }
571
- return vocabularyFromGremlin(result.items[0]);
572
991
  }
573
992
  async function saveVocabulary(conn, repositoryId, vocabulary) {
574
993
  const vid = vocabVertexId(repositoryId);
575
994
  const vocabJson = JSON.stringify(vocabulary);
576
995
  const existing = await conn.submit(
577
- "g.V().has('id', vid).hasLabel('_vocabulary').count()",
578
- { vid }
996
+ "g.V().has('repositoryId', rid).hasId(vid).hasLabel('_vocabulary').count()",
997
+ { vid, rid: repositoryId }
579
998
  );
580
999
  if (Number(existing.items[0] ?? 0) > 0) {
581
1000
  await conn.submit(
582
- "g.V().has('id', vid).hasLabel('_vocabulary').property('vocabulary', vocabJson)",
583
- { vid, vocabJson }
1001
+ "g.V().has('repositoryId', rid).hasId(vid).hasLabel('_vocabulary').property('vocabulary', vocabJson)",
1002
+ { vid, rid: repositoryId, vocabJson }
584
1003
  );
585
1004
  } else {
586
1005
  await conn.submit(
@@ -592,20 +1011,22 @@ async function saveVocabulary(conn, repositoryId, vocabulary) {
592
1011
  async function getVocabularyChangeLog(conn, repositoryId, options) {
593
1012
  const limit = options?.limit ?? 10;
594
1013
  const offset = options?.offset ?? 0;
595
- const countResult = await conn.submit(
596
- "g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').count()",
597
- { rid: repositoryId }
598
- );
1014
+ const [countResult, dataResult] = await Promise.all([
1015
+ conn.submit(
1016
+ "g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').count()",
1017
+ { rid: repositoryId }
1018
+ ),
1019
+ conn.submit(
1020
+ "g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').order().by('proposedAt', decr).range(rangeStart, rangeEnd).valueMap(true)",
1021
+ { rid: repositoryId, rangeStart: offset, rangeEnd: offset + limit }
1022
+ )
1023
+ ]);
599
1024
  const total = Number(countResult.items[0] ?? 0);
600
- const dataResult = await conn.submit(
601
- "g.V().has('repositoryId', rid).hasLabel('_vocabularyChangeLog').order().by('proposedAt', decr).range(rangeStart, rangeEnd).valueMap(true)",
602
- { rid: repositoryId, rangeStart: offset, rangeEnd: offset + limit }
603
- );
604
1025
  const items = dataResult.items.map(changeRecordFromGremlin);
605
1026
  return {
606
1027
  items,
607
1028
  total,
608
- hasMore: offset + limit < total,
1029
+ hasMore: offset + items.length < total,
609
1030
  limit,
610
1031
  offset
611
1032
  };
@@ -613,45 +1034,40 @@ async function getVocabularyChangeLog(conn, repositoryId, options) {
613
1034
 
614
1035
  // src/queries/entity.ts
615
1036
  var import_deep_memory2 = require("@utaba/deep-memory");
1037
+ var DUPLICATE_SENTINEL = "__duplicate";
1038
+ var ENTITY_CREATE_QUERY = `g.V().has('repositoryId', rid).hasId(vid).fold().coalesce(unfold().constant('${DUPLICATE_SENTINEL}'),addV(vertexLabel).property('id', vid).property('repositoryId', rid)${buildEntityPropertyLadder()})`;
616
1039
  async function createEntity(conn, repositoryId, entity) {
617
- const existing = await conn.submit(
618
- "g.V().has('repositoryId', rid).has('id', eid).count()",
619
- { rid: repositoryId, eid: entity.id }
620
- );
621
- if (Number(existing.items[0] ?? 0) > 0) {
1040
+ const bindings = {
1041
+ rid: repositoryId,
1042
+ vid: entity.id,
1043
+ vertexLabel: entity.entityType,
1044
+ ...entityToLadderBindings(entity)
1045
+ };
1046
+ const result = await conn.submit(ENTITY_CREATE_QUERY, bindings);
1047
+ if (result.items[0] === DUPLICATE_SENTINEL) {
622
1048
  throw new import_deep_memory2.DuplicateEntityError(entity.id);
623
1049
  }
624
- const props = entityToGremlinProps(repositoryId, entity);
625
- const bindings = { vid: entity.id };
626
- const propParts = [];
627
- let idx = 0;
628
- for (const [key, value] of Object.entries(props)) {
629
- const paramName = `p${idx++}`;
630
- bindings[paramName] = value;
631
- propParts.push(`.property('${key}', ${paramName})`);
632
- }
633
- bindings["vertexLabel"] = entity.entityType;
634
- const query = `g.addV(vertexLabel).property('id', vid)${propParts.join("")}`;
635
- await conn.submit(query, bindings);
636
1050
  return entity;
637
1051
  }
638
- async function getEntity(conn, repositoryId, entityId) {
1052
+ async function getEntity(conn, repositoryId, entityId, options) {
1053
+ const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
639
1054
  const result = await conn.submit(
640
- "g.V().has('repositoryId', rid).has('id', eid).has('entityType').valueMap(true)",
1055
+ `g.V().has('repositoryId', rid).hasId(eid).has('entityType').${projection}`,
641
1056
  { rid: repositoryId, eid: entityId }
642
1057
  );
643
1058
  if (result.items.length === 0) return null;
644
1059
  return entityFromGremlin(result.items[0]);
645
1060
  }
646
- async function getEntityBySlug(conn, repositoryId, slug) {
1061
+ async function getEntityBySlug(conn, repositoryId, slug, options) {
1062
+ const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
647
1063
  const result = await conn.submit(
648
- "g.V().has('repositoryId', rid).has('slug', slugVal).has('entityType').valueMap(true)",
1064
+ `g.V().has('repositoryId', rid).has('slug', slugVal).has('entityType').${projection}`,
649
1065
  { rid: repositoryId, slugVal: slug }
650
1066
  );
651
1067
  if (result.items.length === 0) return null;
652
1068
  return entityFromGremlin(result.items[0]);
653
1069
  }
654
- async function getEntities(conn, repositoryId, entityIds) {
1070
+ async function getEntities(conn, repositoryId, entityIds, options) {
655
1071
  if (entityIds.length === 0) return /* @__PURE__ */ new Map();
656
1072
  const bindings = { rid: repositoryId };
657
1073
  const idParams = [];
@@ -661,8 +1077,9 @@ async function getEntities(conn, repositoryId, entityIds) {
661
1077
  idParams.push(paramName);
662
1078
  });
663
1079
  const withinClause = `within(${idParams.join(", ")})`;
1080
+ const projection = (0, import_deep_memory2.buildVertexProjectChain)({ withEmbedding: options?.loadEmbeddings });
664
1081
  const result = await conn.submit(
665
- `g.V().has('repositoryId', rid).has('id', ${withinClause}).has('entityType').valueMap(true)`,
1082
+ `g.V().has('repositoryId', rid).hasId(${withinClause}).has('entityType').${projection}`,
666
1083
  bindings
667
1084
  );
668
1085
  const map = /* @__PURE__ */ new Map();
@@ -700,95 +1117,96 @@ async function updateEntity(conn, repositoryId, entityId, updates) {
700
1117
  addProp("modifiedAt", updates.provenance.modifiedAt);
701
1118
  if (updates.provenance.modifiedInConversation != null) addProp("modifiedInConversation", updates.provenance.modifiedInConversation);
702
1119
  if (updates.provenance.modifiedFromMessage != null) addProp("modifiedFromMessage", updates.provenance.modifiedFromMessage);
703
- const query = `g.V().has('repositoryId', rid).has('id', eid).has('entityType')${propParts.join("")}`;
704
- await conn.submit(query, bindings);
705
- return await getEntity(conn, repositoryId, entityId);
1120
+ const projection = (0, import_deep_memory2.buildVertexProjectChain)();
1121
+ const query = `g.V().has('repositoryId', rid).hasId(eid).has('entityType')${propParts.join("")}.${projection}`;
1122
+ const result = await conn.submit(query, bindings);
1123
+ if (result.items.length === 0) {
1124
+ throw new import_deep_memory2.EntityNotFoundError(entityId);
1125
+ }
1126
+ return entityFromGremlin(result.items[0]);
706
1127
  }
707
1128
  async function deleteEntity(conn, repositoryId, entityId) {
708
1129
  await conn.submit(
709
- "g.V().has('repositoryId', rid).has('id', eid).has('entityType').drop()",
1130
+ "g.V().has('repositoryId', rid).hasId(eid).has('entityType').drop()",
710
1131
  { rid: repositoryId, eid: entityId }
711
1132
  );
712
1133
  }
713
1134
  async function deleteEntitiesByType(conn, repositoryId, entityType) {
714
- const entityCountResult = await conn.submit(
715
- "g.V().has('repositoryId', rid).has('entityType', etype).count()",
716
- { rid: repositoryId, etype: entityType }
717
- );
718
- const deletedEntities = Number(entityCountResult.items[0] ?? 0);
719
- const relCountResult = await conn.submit(
720
- "g.V().has('repositoryId', rid).has('entityType', etype).bothE().dedup().count()",
1135
+ const result = await conn.submit(
1136
+ "g.V().has('repositoryId', rid).has('entityType', etype).aggregate('found').by('id').drop().cap('found')",
721
1137
  { rid: repositoryId, etype: entityType }
722
1138
  );
723
- const deletedRelationships = Number(relCountResult.items[0] ?? 0);
724
- if (deletedEntities > 0) {
725
- await conn.submit(
726
- "g.V().has('repositoryId', rid).has('entityType', etype).drop()",
727
- { rid: repositoryId, etype: entityType }
728
- );
729
- }
730
- return { deletedEntities, deletedRelationships };
1139
+ const bucket = result.items[0];
1140
+ const deletedEntities = Array.isArray(bucket) ? bucket.length : 0;
1141
+ return { deletedEntities, deletedRelationships: void 0 };
731
1142
  }
732
- async function findEntities(conn, repositoryId, query) {
733
- const bindings = { rid: repositoryId };
734
- let filterClause = ".has('repositoryId', rid).has('entityType')";
1143
+ function sqlPath(key) {
1144
+ return `c.${key}[0]._value`;
1145
+ }
1146
+ function buildWhereClause(query, repositoryId) {
1147
+ const params = [{ name: "@rid", value: repositoryId }];
1148
+ const predicates = ["c.repositoryId = @rid", "IS_DEFINED(c.entityType)"];
735
1149
  if (query.entityTypes && query.entityTypes.length > 0) {
736
- const typeParams = [];
1150
+ const typeParamNames = [];
737
1151
  query.entityTypes.forEach((t, i) => {
738
- const paramName = `etype${i}`;
739
- bindings[paramName] = t;
740
- typeParams.push(paramName);
1152
+ const name = `@etype${i}`;
1153
+ params.push({ name, value: t });
1154
+ typeParamNames.push(name);
741
1155
  });
742
- filterClause += `.has('entityType', within(${typeParams.join(", ")}))`;
743
- }
744
- const hasPropertyFilter = query.properties != null && Object.keys(query.properties).length > 0;
745
- const needsClientFilter = Boolean(query.searchTerm) || hasPropertyFilter;
746
- if (needsClientFilter) {
747
- const dataResult2 = await conn.submit(
748
- `g.V()${filterClause}.valueMap(true)`,
749
- bindings
1156
+ predicates.push(`${sqlPath("entityType")} IN (${typeParamNames.join(", ")})`);
1157
+ }
1158
+ if (query.searchTerm) {
1159
+ params.push({ name: "@term", value: query.searchTerm });
1160
+ predicates.push(
1161
+ `(CONTAINS(${sqlPath("entityLabel")}, @term, true) OR CONTAINS(${sqlPath("slug")}, @term, true) OR CONTAINS(${sqlPath("summary")}, @term, true))`
750
1162
  );
751
- let items2 = dataResult2.items.map(entityFromGremlin);
752
- if (query.searchTerm) {
753
- const term = query.searchTerm.toLowerCase();
754
- items2 = items2.filter(
755
- (e) => e.label.toLowerCase().includes(term) || e.slug.toLowerCase().includes(term) || e.summary != null && e.summary.toLowerCase().includes(term)
756
- );
757
- }
758
- if (hasPropertyFilter) {
759
- items2 = items2.filter((entity) => {
760
- for (const [key, value] of Object.entries(query.properties)) {
761
- if (entity.properties[key] !== value) return false;
762
- }
763
- return true;
764
- });
1163
+ }
1164
+ if (query.properties != null) {
1165
+ let i = 0;
1166
+ for (const [key, value] of Object.entries(query.properties)) {
1167
+ const fragment = JSON.stringify({ [key]: value }).slice(1, -1);
1168
+ const name = `@kv${i++}`;
1169
+ params.push({ name, value: fragment });
1170
+ predicates.push(`CONTAINS(${sqlPath("properties")}, ${name}, false)`);
765
1171
  }
766
- const total2 = items2.length;
767
- const paged = items2.slice(query.offset, query.offset + query.limit);
768
- return {
769
- items: paged,
770
- total: total2,
771
- hasMore: query.offset + query.limit < total2,
772
- limit: query.limit,
773
- offset: query.offset
774
- };
775
1172
  }
776
- const countResult = await conn.submit(
777
- `g.V()${filterClause}.count()`,
778
- bindings
779
- );
780
- const total = Number(countResult.items[0] ?? 0);
781
- bindings["rangeStart"] = query.offset;
782
- bindings["rangeEnd"] = query.offset + query.limit;
783
- const dataResult = await conn.submit(
784
- `g.V()${filterClause}.range(rangeStart, rangeEnd).valueMap(true)`,
785
- bindings
786
- );
787
- const items = dataResult.items.map(entityFromGremlin);
1173
+ return { sqlWhere: `WHERE ${predicates.join(" AND ")}`, params };
1174
+ }
1175
+ function buildSelectClause(loadEmbeddings) {
1176
+ const fields = ["c.id", ...STORED_ENTITY_FIELDS.filter((f) => f !== "id").map((f) => `c.${f}`)];
1177
+ if (loadEmbeddings) fields.push("c.embedding");
1178
+ return `SELECT ${fields.join(", ")}`;
1179
+ }
1180
+ async function findEntities(docClient, repositoryId, query, options) {
1181
+ const { sqlWhere, params } = buildWhereClause(query, repositoryId);
1182
+ const dataParams = [
1183
+ ...params,
1184
+ { name: "@off", value: query.offset },
1185
+ { name: "@lim", value: query.limit }
1186
+ ];
1187
+ const selectClause = buildSelectClause(options?.loadEmbeddings === true);
1188
+ const dataSql = `${selectClause} FROM c ${sqlWhere} ORDER BY c.id OFFSET @off LIMIT @lim`;
1189
+ const countSql = `SELECT VALUE COUNT(1) FROM c ${sqlWhere}`;
1190
+ const skipCount = query.properties != null && Object.keys(query.properties).length > 0;
1191
+ const [dataResult, countResult] = await Promise.all([
1192
+ docClient.query(dataSql, dataParams, {
1193
+ partitionKey: repositoryId
1194
+ }),
1195
+ skipCount ? Promise.resolve(null) : docClient.query(countSql, params, { partitionKey: repositoryId })
1196
+ ]);
1197
+ let items = dataResult.documents.map(entityFromDocument);
1198
+ if (query.properties != null && Object.keys(query.properties).length > 0) {
1199
+ const filters = Object.entries(query.properties).map(
1200
+ ([key, value]) => ({ key, operator: "eq", value })
1201
+ );
1202
+ items = items.filter((entity) => (0, import_deep_memory2.matchesPropertyFilters)(entity.properties, filters));
1203
+ }
1204
+ const total = countResult && countResult.documents.length > 0 ? Number(countResult.documents[0]) : void 0;
1205
+ const hasMore = total != null ? query.offset + items.length < total : items.length === query.limit;
788
1206
  return {
789
1207
  items,
790
1208
  total,
791
- hasMore: query.offset + query.limit < total,
1209
+ hasMore,
792
1210
  limit: query.limit,
793
1211
  offset: query.offset
794
1212
  };
@@ -796,36 +1214,27 @@ async function findEntities(conn, repositoryId, query) {
796
1214
 
797
1215
  // src/queries/relationship.ts
798
1216
  var import_deep_memory3 = require("@utaba/deep-memory");
1217
+ var DUPLICATE_SENTINEL2 = "__duplicate";
1218
+ var RELATIONSHIP_CREATE_QUERY = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold().constant('${DUPLICATE_SENTINEL2}'),g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${buildRelationshipPropertyLadder()})`;
799
1219
  async function createRelationship(conn, repositoryId, relationship) {
800
- const existing = await conn.submit(
801
- "g.E().has('repositoryId', rid).has('id', relId).count()",
802
- { rid: repositoryId, relId: relationship.id }
803
- );
804
- if (Number(existing.items[0] ?? 0) > 0) {
805
- throw new import_deep_memory3.DuplicateRelationshipError(relationship.id);
806
- }
807
- const props = relationshipToGremlinProps(repositoryId, relationship);
808
1220
  const bindings = {
1221
+ rid: repositoryId,
809
1222
  relId: relationship.id,
810
1223
  srcId: relationship.sourceEntityId,
811
1224
  tgtId: relationship.targetEntityId,
812
- rid: repositoryId,
813
- edgeLabel: relationship.relationshipType
1225
+ edgeLabel: relationship.relationshipType,
1226
+ ...relationshipToLadderBindings(relationship)
814
1227
  };
815
- const propParts = [];
816
- let idx = 0;
817
- for (const [key, value] of Object.entries(props)) {
818
- const paramName = `p${idx++}`;
819
- bindings[paramName] = value;
820
- propParts.push(`.property('${key}', ${paramName})`);
1228
+ const result = await conn.submit(RELATIONSHIP_CREATE_QUERY, bindings);
1229
+ if (result.items[0] === DUPLICATE_SENTINEL2) {
1230
+ throw new import_deep_memory3.DuplicateRelationshipError(relationship.id);
821
1231
  }
822
- const query = `g.V().has('repositoryId', rid).has('id', srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).has('id', tgtId).has('entityType')).property('id', relId)${propParts.join("")}`;
823
- await conn.submit(query, bindings);
824
1232
  return relationship;
825
1233
  }
826
1234
  async function getRelationship(conn, repositoryId, relationshipId) {
1235
+ const projection = (0, import_deep_memory3.buildEdgeProjectChain)();
827
1236
  const result = await conn.submit(
828
- "g.E().has('id', relId).has('repositoryId', rid).valueMap(true)",
1237
+ `g.E().hasId(relId).has('repositoryId', rid).${projection}`,
829
1238
  { relId: relationshipId, rid: repositoryId }
830
1239
  );
831
1240
  if (result.items.length === 0) return null;
@@ -835,240 +1244,86 @@ async function getEntityRelationships(conn, repositoryId, entityId, options) {
835
1244
  const limit = options?.limit ?? 50;
836
1245
  const offset = options?.offset ?? 0;
837
1246
  const direction = options?.direction ?? "both";
838
- const bindings = {
1247
+ const hasPropertyFilters = options?.propertyFilters != null && options.propertyFilters.length > 0;
1248
+ const baseBindings = {
839
1249
  rid: repositoryId,
840
1250
  eid: entityId
841
- };
842
- let edgeTraversal;
843
- switch (direction) {
844
- case "outbound":
845
- edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').outE()";
846
- break;
847
- case "inbound":
848
- edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').inE()";
849
- break;
850
- case "both":
851
- default:
852
- edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').bothE()";
853
- break;
854
- }
855
- let typeFilter = "";
856
- if (options?.relationshipTypes && options.relationshipTypes.length > 0) {
857
- const typeParams = [];
858
- options.relationshipTypes.forEach((t, i) => {
859
- const paramName = `rtype${i}`;
860
- bindings[paramName] = t;
861
- typeParams.push(paramName);
862
- });
863
- typeFilter = `.hasLabel(${typeParams.join(", ")})`;
864
- }
865
- let unionQuery = null;
866
- if (direction === "outbound") {
867
- unionQuery = `g.V().has('repositoryId', rid).has('id', eid).has('entityType').union(outE()${typeFilter}, inE()${typeFilter}.has('bidirectional', true))`;
868
- } else if (direction === "inbound") {
869
- unionQuery = `g.V().has('repositoryId', rid).has('id', eid).has('entityType').union(inE()${typeFilter}, outE()${typeFilter}.has('bidirectional', true))`;
870
- }
871
- const baseQuery = unionQuery ?? `${edgeTraversal}${typeFilter}`;
872
- const countResult = await conn.submit(`${baseQuery}.dedup().count()`, bindings);
873
- const total = Number(countResult.items[0] ?? 0);
874
- bindings["rangeStart"] = offset;
875
- bindings["rangeEnd"] = offset + limit;
876
- const dataResult = await conn.submit(
877
- `${baseQuery}.dedup().range(rangeStart, rangeEnd).valueMap(true)`,
878
- bindings
879
- );
880
- let items = dataResult.items.map(relationshipFromGremlin);
881
- if (options?.propertyFilters && options.propertyFilters.length > 0) {
882
- items = items.filter((rel) => (0, import_deep_memory3.matchesPropertyFilters)(rel.properties, options.propertyFilters));
883
- }
884
- return {
885
- items,
886
- total,
887
- hasMore: offset + limit < total,
888
- limit,
889
- offset
890
- };
891
- }
892
- async function deleteRelationship(conn, repositoryId, relationshipId) {
893
- await conn.submit(
894
- "g.E().has('id', relId).has('repositoryId', rid).drop()",
895
- { relId: relationshipId, rid: repositoryId }
896
- );
897
- }
898
- async function deleteRelationshipsByType(conn, repositoryId, relationshipType) {
899
- const countResult = await conn.submit(
900
- "g.E().has('repositoryId', rid).hasLabel(rtype).count()",
901
- { rid: repositoryId, rtype: relationshipType }
902
- );
903
- const deletedRelationships = Number(countResult.items[0] ?? 0);
904
- if (deletedRelationships > 0) {
905
- await conn.submit(
906
- "g.E().has('repositoryId', rid).hasLabel(rtype).drop()",
907
- { rid: repositoryId, rtype: relationshipType }
908
- );
909
- }
910
- return { deletedRelationships };
911
- }
912
-
913
- // src/queries/traversal.ts
914
- var import_deep_memory4 = require("@utaba/deep-memory");
915
- async function exploreNeighborhood(conn, repositoryId, entityId, options) {
916
- const layers = [];
917
- let frontier = /* @__PURE__ */ new Set([entityId]);
918
- const visited = /* @__PURE__ */ new Set([entityId]);
919
- for (let depth = 0; depth < options.depth; depth++) {
920
- const layer = {};
921
- const nextFrontier = /* @__PURE__ */ new Set();
922
- const layerCache = /* @__PURE__ */ new Map();
923
- for (const currentId of frontier) {
924
- const rels = await getRelationshipsForEntity(conn, repositoryId, currentId, options);
925
- for (const rel of rels) {
926
- const relType = rel.relationshipType;
927
- if (!layer[relType]) {
928
- layer[relType] = { total: 0, entities: [], relationships: [] };
929
- }
930
- let connectedId;
931
- if (rel.sourceEntityId === currentId) {
932
- connectedId = rel.targetEntityId;
933
- } else {
934
- connectedId = rel.sourceEntityId;
935
- }
936
- if (visited.has(connectedId)) continue;
937
- let entity = layerCache.get(connectedId);
938
- if (entity === void 0) {
939
- const loaded = await getEntityLight(conn, repositoryId, connectedId);
940
- if (loaded && (!options.entityTypes || options.entityTypes.length === 0 || options.entityTypes.includes(loaded.entityType))) {
941
- entity = loaded;
942
- } else {
943
- entity = null;
944
- }
945
- layerCache.set(connectedId, entity);
946
- if (entity) nextFrontier.add(connectedId);
947
- }
948
- if (!entity) continue;
949
- layer[relType].entities.push(entity);
950
- layer[relType].relationships.push(rel);
951
- layer[relType].total = layer[relType].entities.length;
952
- }
953
- }
954
- for (const relType of Object.keys(layer)) {
955
- const group = layer[relType];
956
- const start = options.offsetPerType;
957
- const end = start + options.limitPerType;
958
- group.entities = group.entities.slice(start, end);
959
- group.relationships = group.relationships.slice(start, end);
960
- }
961
- if (Object.keys(layer).length > 0) {
962
- layers.push(layer);
963
- }
964
- for (const id of nextFrontier) {
965
- visited.add(id);
966
- }
967
- frontier = nextFrontier;
968
- if (frontier.size === 0) break;
969
- }
970
- return { centerId: entityId, layers };
971
- }
972
- async function getRelationshipsForEntity(conn, repositoryId, entityId, options) {
973
- const bindings = { rid: repositoryId, eid: entityId };
1251
+ };
974
1252
  let edgeTraversal;
975
- switch (options.direction) {
1253
+ switch (direction) {
976
1254
  case "outbound":
977
- edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').union(outE(), inE().has('bidirectional', true))";
1255
+ edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').outE()";
978
1256
  break;
979
1257
  case "inbound":
980
- edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').union(inE(), outE().has('bidirectional', true))";
1258
+ edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').inE()";
981
1259
  break;
982
1260
  case "both":
983
1261
  default:
984
- edgeTraversal = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').bothE()";
1262
+ edgeTraversal = "g.V().has('repositoryId', rid).hasId(eid).has('entityType').bothE()";
985
1263
  break;
986
1264
  }
987
1265
  let typeFilter = "";
988
- if (options.relationshipTypes && options.relationshipTypes.length > 0) {
1266
+ if (options?.relationshipTypes && options.relationshipTypes.length > 0) {
989
1267
  const typeParams = [];
990
1268
  options.relationshipTypes.forEach((t, i) => {
991
1269
  const paramName = `rtype${i}`;
992
- bindings[paramName] = t;
1270
+ baseBindings[paramName] = t;
993
1271
  typeParams.push(paramName);
994
1272
  });
995
1273
  typeFilter = `.hasLabel(${typeParams.join(", ")})`;
996
1274
  }
997
- const result = await conn.submit(
998
- `${edgeTraversal}${typeFilter}.dedup().valueMap(true)`,
999
- bindings
1000
- );
1001
- let rels = result.items.map(relationshipFromGremlin);
1002
- if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
1003
- rels = rels.filter((rel) => (0, import_deep_memory4.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters));
1275
+ let unionQuery = null;
1276
+ if (direction === "outbound") {
1277
+ unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(outE()${typeFilter}, inE()${typeFilter}.has('bidirectional', true))`;
1278
+ } else if (direction === "inbound") {
1279
+ unionQuery = `g.V().has('repositoryId', rid).hasId(eid).has('entityType').union(inE()${typeFilter}, outE()${typeFilter}.has('bidirectional', true))`;
1004
1280
  }
1005
- return rels;
1006
- }
1007
- async function getEntityLight(conn, repositoryId, entityId) {
1008
- const result = await conn.submit(
1009
- "g.V().has('repositoryId', rid).has('id', eid).has('entityType').valueMap(true)",
1010
- { rid: repositoryId, eid: entityId }
1011
- );
1012
- if (result.items.length === 0) return null;
1013
- return entityFromGremlin(result.items[0]);
1014
- }
1015
- async function findPaths(conn, repositoryId, sourceId, targetId, options) {
1016
- const paths = [];
1017
- const maxDepth = options.maxDepth;
1018
- const limit = options.limit;
1019
- let queue = [{ entityId: sourceId, path: [sourceId], relationshipIds: [] }];
1020
- for (let depth = 0; depth < maxDepth && paths.length < limit; depth++) {
1021
- const nextQueue = [];
1022
- for (const state of queue) {
1023
- if (paths.length >= limit) break;
1024
- const bindings = { rid: repositoryId, eid: state.entityId };
1025
- let edgeQuery = "g.V().has('repositoryId', rid).has('id', eid).has('entityType').bothE()";
1026
- if (options.relationshipTypes && options.relationshipTypes.length > 0) {
1027
- const typeParams = [];
1028
- options.relationshipTypes.forEach((t, i) => {
1029
- const paramName = `rtype${i}`;
1030
- bindings[paramName] = t;
1031
- typeParams.push(paramName);
1032
- });
1033
- edgeQuery += `.hasLabel(${typeParams.join(", ")})`;
1034
- }
1035
- const relResult = await conn.submit(`${edgeQuery}.valueMap(true)`, bindings);
1036
- const rels = relResult.items.map(relationshipFromGremlin);
1037
- let filteredRels = rels;
1038
- if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
1039
- filteredRels = rels.filter((r) => (0, import_deep_memory4.matchesPropertyFilters)(r.properties, options.relationshipPropertyFilters));
1040
- }
1041
- for (const rel of filteredRels) {
1042
- const nextId = rel.sourceEntityId === state.entityId ? rel.targetEntityId : rel.sourceEntityId;
1043
- if (state.path.includes(nextId)) continue;
1044
- if (options.entityTypes && options.entityTypes.length > 0 && nextId !== targetId) {
1045
- const entity = await getEntityLight(conn, repositoryId, nextId);
1046
- if (!entity || !options.entityTypes.includes(entity.entityType)) continue;
1047
- }
1048
- const newPath = [...state.path, nextId];
1049
- const newRelIds = [...state.relationshipIds, rel.id];
1050
- if (nextId === targetId) {
1051
- paths.push({ entityIds: newPath, relationshipIds: newRelIds });
1052
- if (paths.length >= limit) break;
1053
- } else {
1054
- nextQueue.push({ entityId: nextId, path: newPath, relationshipIds: newRelIds });
1055
- }
1056
- }
1057
- }
1058
- queue = nextQueue;
1059
- if (queue.length === 0) break;
1281
+ const baseQuery = unionQuery ?? `${edgeTraversal}${typeFilter}`;
1282
+ const projection = (0, import_deep_memory3.buildEdgeProjectChain)();
1283
+ const dataBindings = { ...baseBindings, rangeStart: offset, rangeEnd: offset + limit };
1284
+ const [countResult, dataResult] = await Promise.all([
1285
+ hasPropertyFilters ? Promise.resolve(null) : conn.submit(`${baseQuery}.dedup().count()`, baseBindings),
1286
+ conn.submit(
1287
+ `${baseQuery}.dedup().range(rangeStart, rangeEnd).${projection}`,
1288
+ dataBindings
1289
+ )
1290
+ ]);
1291
+ const rawItems = dataResult.items;
1292
+ let items = rawItems.map(relationshipFromGremlin);
1293
+ if (hasPropertyFilters) {
1294
+ items = items.filter((rel) => (0, import_deep_memory3.matchesPropertyFilters)(rel.properties, options.propertyFilters));
1060
1295
  }
1296
+ const total = countResult ? Number(countResult.items[0] ?? 0) : void 0;
1297
+ const hasMore = total != null ? offset + rawItems.length < total : rawItems.length === limit;
1061
1298
  return {
1062
- paths,
1063
- totalPaths: paths.length
1299
+ items,
1300
+ total,
1301
+ hasMore,
1302
+ limit,
1303
+ offset
1064
1304
  };
1065
1305
  }
1306
+ async function deleteRelationship(conn, repositoryId, relationshipId) {
1307
+ await conn.submit(
1308
+ "g.E().hasId(relId).has('repositoryId', rid).drop()",
1309
+ { relId: relationshipId, rid: repositoryId }
1310
+ );
1311
+ }
1312
+ async function deleteRelationshipsByType(conn, repositoryId, relationshipType) {
1313
+ const result = await conn.submit(
1314
+ "g.E().has('repositoryId', rid).hasLabel(rtype).aggregate('found').by('id').drop().cap('found')",
1315
+ { rid: repositoryId, rtype: relationshipType }
1316
+ );
1317
+ const bucket = result.items[0];
1318
+ const deletedRelationships = Array.isArray(bucket) ? bucket.length : 0;
1319
+ return { deletedRelationships };
1320
+ }
1066
1321
 
1067
1322
  // src/queries/timeline.ts
1068
1323
  async function getTimeline(conn, repositoryId, entityId, options) {
1069
1324
  const events = [];
1070
1325
  const entityResult = await conn.submit(
1071
- "g.V().has('repositoryId', rid).has('id', eid).has('entityType').valueMap('createdAt', 'modifiedAt')",
1326
+ "g.V().has('repositoryId', rid).hasId(eid).has('entityType').valueMap('createdAt', 'modifiedAt')",
1072
1327
  { rid: repositoryId, eid: entityId }
1073
1328
  );
1074
1329
  if (entityResult.items.length > 0) {
@@ -1091,7 +1346,7 @@ async function getTimeline(conn, repositoryId, entityId, options) {
1091
1346
  }
1092
1347
  }
1093
1348
  const relResult = await conn.submit(
1094
- "g.V().has('repositoryId', rid).has('id', eid).has('entityType').bothE().valueMap('id', 'createdAt')",
1349
+ "g.V().has('repositoryId', rid).hasId(eid).has('entityType').bothE().valueMap('id', 'createdAt')",
1095
1350
  { rid: repositoryId, eid: entityId }
1096
1351
  );
1097
1352
  for (const item of relResult.items) {
@@ -1126,7 +1381,7 @@ function isInTimeRange(timestamp, timeRange) {
1126
1381
  }
1127
1382
 
1128
1383
  // src/queries/adaptive-import.ts
1129
- var import_deep_memory5 = require("@utaba/deep-memory");
1384
+ var import_deep_memory4 = require("@utaba/deep-memory");
1130
1385
  var DEFAULT_MIN = 1;
1131
1386
  var DEFAULT_START = 5;
1132
1387
  var DEFAULT_MAX = 32;
@@ -1320,7 +1575,7 @@ async function runTaskWithUsage(fn) {
1320
1575
  }
1321
1576
  }
1322
1577
  }
1323
- function sleep2(ms) {
1578
+ function sleep3(ms) {
1324
1579
  return new Promise((resolve) => setTimeout(resolve, ms));
1325
1580
  }
1326
1581
  async function runAdaptive(items, controller, fn) {
@@ -1345,7 +1600,7 @@ async function runAdaptive(items, controller, fn) {
1345
1600
  const slotsAvailable = inFlight < controller.getConcurrency();
1346
1601
  if (cooldownRemaining <= 0 && slotsAvailable) break;
1347
1602
  if (cooldownRemaining > 0) {
1348
- await Promise.race([sleep2(cooldownRemaining), gate.promise]);
1603
+ await Promise.race([sleep3(cooldownRemaining), gate.promise]);
1349
1604
  } else {
1350
1605
  await gate.promise;
1351
1606
  }
@@ -1377,7 +1632,7 @@ async function runAdaptive(items, controller, fn) {
1377
1632
  }
1378
1633
  await Promise.all(workers);
1379
1634
  if (aborted) {
1380
- throw new import_deep_memory5.ImportThrottleAbortError(
1635
+ throw new import_deep_memory4.ImportThrottleAbortError(
1381
1636
  controller.getConcurrency(),
1382
1637
  controller.getConsecutiveThrottlesAtMin(),
1383
1638
  controller.getCompleted(),
@@ -1519,91 +1774,82 @@ async function importBulk(conn, repositoryId, data, options) {
1519
1774
  }
1520
1775
  return { entitiesImported, relationshipsImported, errors };
1521
1776
  }
1777
+ var ENTITY_LADDER_CHAIN = buildEntityPropertyLadder();
1778
+ var RELATIONSHIP_LADDER_CHAIN = buildRelationshipPropertyLadder();
1779
+ var INSERT_ENTITY_QUERY = `g.addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN}`;
1780
+ var INSERT_RELATIONSHIP_QUERY = `g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${RELATIONSHIP_LADDER_CHAIN}`;
1781
+ var UPSERT_ENTITY_QUERY = `g.V().has('repositoryId', rid).hasId(vid).has('entityType').fold().coalesce(unfold()${ENTITY_LADDER_CHAIN}, addV(vertexLabel).property('id', vid).property('repositoryId', rid)${ENTITY_LADDER_CHAIN})`;
1782
+ var UPSERT_RELATIONSHIP_QUERY = `g.E().has('repositoryId', rid).hasId(relId).fold().coalesce(unfold()${RELATIONSHIP_LADDER_CHAIN}, g.V().has('repositoryId', rid).hasId(srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).hasId(tgtId).has('entityType')).property('id', relId).property('repositoryId', rid)${RELATIONSHIP_LADDER_CHAIN})`;
1522
1783
  async function insertEntity(conn, repositoryId, entity) {
1523
- const props = entityToGremlinProps(repositoryId, entity);
1524
1784
  const bindings = {
1785
+ rid: repositoryId,
1525
1786
  vid: entity.id,
1526
- vertexLabel: entity.entityType
1787
+ vertexLabel: entity.entityType,
1788
+ ...entityToLadderBindings(entity)
1527
1789
  };
1528
- const propParts = [];
1529
- let idx = 0;
1530
- for (const [key, value] of Object.entries(props)) {
1531
- const paramName = `p${idx++}`;
1532
- bindings[paramName] = value;
1533
- propParts.push(`.property('${key}', ${paramName})`);
1534
- }
1535
- const query = `g.addV(vertexLabel).property('id', vid)${propParts.join("")}`;
1536
- await conn.submit(query, bindings);
1790
+ await conn.submit(INSERT_ENTITY_QUERY, bindings);
1537
1791
  }
1538
1792
  async function insertRelationship(conn, repositoryId, rel) {
1539
- const props = relationshipToGremlinProps(repositoryId, rel);
1540
1793
  const bindings = {
1794
+ rid: repositoryId,
1541
1795
  relId: rel.id,
1542
1796
  srcId: rel.sourceEntityId,
1543
1797
  tgtId: rel.targetEntityId,
1544
- rid: repositoryId,
1545
- edgeLabel: rel.relationshipType
1798
+ edgeLabel: rel.relationshipType,
1799
+ ...relationshipToLadderBindings(rel)
1546
1800
  };
1547
- const propParts = [];
1548
- let idx = 0;
1549
- for (const [key, value] of Object.entries(props)) {
1550
- const paramName = `p${idx++}`;
1551
- bindings[paramName] = value;
1552
- propParts.push(`.property('${key}', ${paramName})`);
1553
- }
1554
- const query = `g.V().has('repositoryId', rid).has('id', srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).has('id', tgtId).has('entityType')).property('id', relId)${propParts.join("")}`;
1555
- await conn.submit(query, bindings);
1801
+ await conn.submit(INSERT_RELATIONSHIP_QUERY, bindings);
1556
1802
  }
1557
1803
  async function upsertEntity(conn, repositoryId, entity) {
1558
- const props = entityToGremlinProps(repositoryId, entity);
1559
1804
  const bindings = {
1560
- vid: entity.id,
1561
1805
  rid: repositoryId,
1562
- vertexLabel: entity.entityType
1806
+ vid: entity.id,
1807
+ vertexLabel: entity.entityType,
1808
+ ...entityToLadderBindings(entity)
1563
1809
  };
1564
- const propParts = [];
1565
- let idx = 0;
1566
- for (const [key, value] of Object.entries(props)) {
1567
- const paramName = `p${idx++}`;
1568
- bindings[paramName] = value;
1569
- propParts.push(`.property('${key}', ${paramName})`);
1570
- }
1571
- const query = `g.V().has('repositoryId', rid).has('id', vid).has('entityType').fold().coalesce(unfold()${propParts.join("")}, addV(vertexLabel).property('id', vid)${propParts.join("")})`;
1572
- await conn.submit(query, bindings);
1810
+ await conn.submit(UPSERT_ENTITY_QUERY, bindings);
1573
1811
  }
1574
1812
  async function upsertRelationship(conn, repositoryId, rel) {
1575
- const props = relationshipToGremlinProps(repositoryId, rel);
1576
1813
  const bindings = {
1814
+ rid: repositoryId,
1577
1815
  relId: rel.id,
1578
1816
  srcId: rel.sourceEntityId,
1579
1817
  tgtId: rel.targetEntityId,
1580
- rid: repositoryId,
1581
- edgeLabel: rel.relationshipType
1818
+ edgeLabel: rel.relationshipType,
1819
+ ...relationshipToLadderBindings(rel)
1582
1820
  };
1583
- const propParts = [];
1584
- let idx = 0;
1585
- for (const [key, value] of Object.entries(props)) {
1586
- const paramName = `p${idx++}`;
1587
- bindings[paramName] = value;
1588
- propParts.push(`.property('${key}', ${paramName})`);
1589
- }
1590
- const createEdge = `g.V().has('repositoryId', rid).has('id', srcId).has('entityType').addE(edgeLabel).to(g.V().has('repositoryId', rid).has('id', tgtId).has('entityType')).property('id', relId)${propParts.join("")}`;
1591
- const query = `g.E().has('repositoryId', rid).has('id', relId).fold().coalesce(unfold()${propParts.join("")}, ${createEdge})`;
1592
- await conn.submit(query, bindings);
1821
+ await conn.submit(UPSERT_RELATIONSHIP_QUERY, bindings);
1593
1822
  }
1594
1823
 
1595
1824
  // src/CosmosDbProvider.ts
1596
1825
  var PROVIDER_NAME = "cosmosdb";
1597
1826
  var SCHEMA_VERSION = 1;
1598
1827
  var META_VERTEX_ID = "_meta:schema";
1828
+ var GUARDED_INDEX_PATHS = [
1829
+ "/entityLabel",
1830
+ "/slug",
1831
+ "/summary",
1832
+ "/entityType",
1833
+ "/properties",
1834
+ "/repositoryId"
1835
+ ];
1836
+ var VOCABULARY_CACHE_TTL_MS = 6e4;
1599
1837
  var CosmosDbProvider = class {
1600
1838
  conn;
1839
+ docClient;
1601
1840
  config;
1602
- compiler = new import_deep_memory6.GremlinCompiler();
1841
+ compiler = new import_deep_memory5.GremlinCompiler();
1603
1842
  reportUsage;
1843
+ /**
1844
+ * Per-process vocabulary cache, keyed by repositoryId. Read lazily by
1845
+ * `traverseImpl` via `getVocabularyCached`; invalidated on `saveVocabulary`.
1846
+ * Each provider instance owns its own cache so isolated test providers do
1847
+ * not share state.
1848
+ */
1849
+ vocabularyCache = /* @__PURE__ */ new Map();
1604
1850
  constructor(config) {
1605
1851
  this.config = config;
1606
- this.reportUsage = (0, import_deep_memory6.createSafeSink)(config.reportUsage);
1852
+ this.reportUsage = (0, import_deep_memory5.createSafeSink)(config.reportUsage);
1607
1853
  this.conn = new CosmosDbConnection({
1608
1854
  endpoint: config.endpoint,
1609
1855
  key: config.key,
@@ -1613,6 +1859,15 @@ var CosmosDbProvider = class {
1613
1859
  defaultTimeoutMs: config.defaultTimeoutMs,
1614
1860
  rejectUnauthorized: config.rejectUnauthorized
1615
1861
  });
1862
+ this.docClient = new CosmosDocumentClient({
1863
+ restEndpoint: this.getRestEndpoint(),
1864
+ key: config.key,
1865
+ database: config.database,
1866
+ container: config.container,
1867
+ rejectUnauthorized: config.rejectUnauthorized ?? true,
1868
+ maxRetries: config.maxRetries,
1869
+ defaultTimeoutMs: config.defaultTimeoutMs
1870
+ });
1616
1871
  }
1617
1872
  /**
1618
1873
  * Run the given operation inside an async-local usage scope. While the
@@ -1657,8 +1912,8 @@ var CosmosDbProvider = class {
1657
1912
  * ever reaching query construction.
1658
1913
  */
1659
1914
  assertValidRepositoryId(repositoryId) {
1660
- if (!(0, import_deep_memory6.isValidUuid)(repositoryId)) {
1661
- throw new import_deep_memory6.InvalidInputError(
1915
+ if (!(0, import_deep_memory5.isValidUuid)(repositoryId)) {
1916
+ throw new import_deep_memory5.InvalidInputError(
1662
1917
  "repositoryId",
1663
1918
  `repositoryId must be a valid v4 UUID; got '${repositoryId}'`
1664
1919
  );
@@ -1706,24 +1961,18 @@ var CosmosDbProvider = class {
1706
1961
  await this.conn.close();
1707
1962
  await this.conn.connect();
1708
1963
  const existing = await this.conn.submit(
1709
- "g.V().has('id', metaId).hasLabel('_meta').valueMap(true)",
1964
+ "g.V().hasId(metaId).hasLabel('_meta').valueMap(true)",
1710
1965
  { metaId: META_VERTEX_ID }
1711
1966
  );
1712
1967
  if (existing.items.length > 0) {
1713
1968
  const props = existing.items[0];
1714
1969
  const version = Number(unwrapGremlinValue(props["schemaVersion"]) ?? 0);
1715
- if (version >= SCHEMA_VERSION && !databaseCreated && !schemaCreated) {
1716
- return {
1717
- databaseCreated: false,
1718
- schemaCreated: false,
1719
- alreadyUpToDate: true,
1720
- schemaVersion: SCHEMA_VERSION
1721
- };
1970
+ if (version < SCHEMA_VERSION || databaseCreated || schemaCreated) {
1971
+ await this.conn.submit(
1972
+ "g.V().hasId(metaId).hasLabel('_meta').property('schemaVersion', ver)",
1973
+ { metaId: META_VERTEX_ID, ver: SCHEMA_VERSION }
1974
+ );
1722
1975
  }
1723
- await this.conn.submit(
1724
- "g.V().has('id', metaId).hasLabel('_meta').property('schemaVersion', ver)",
1725
- { metaId: META_VERTEX_ID, ver: SCHEMA_VERSION }
1726
- );
1727
1976
  } else {
1728
1977
  await this.conn.submit(
1729
1978
  "g.addV('_meta').property('id', metaId).property('repositoryId', pk).property('schemaVersion', ver)",
@@ -1731,6 +1980,8 @@ var CosmosDbProvider = class {
1731
1980
  );
1732
1981
  schemaCreated = true;
1733
1982
  }
1983
+ await ensureRepositoryIndex(this.conn);
1984
+ await this.runIndexingPolicyDiagnostic();
1734
1985
  return {
1735
1986
  databaseCreated,
1736
1987
  schemaCreated,
@@ -1738,12 +1989,57 @@ var CosmosDbProvider = class {
1738
1989
  schemaVersion: SCHEMA_VERSION
1739
1990
  };
1740
1991
  } catch (err) {
1741
- throw new import_deep_memory6.ProviderError(
1992
+ throw new import_deep_memory5.ProviderError(
1742
1993
  `Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
1743
1994
  "Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
1744
1995
  );
1745
1996
  }
1746
1997
  }
1998
+ /**
1999
+ * Step E — verify the container's indexing policy covers every path that
2000
+ * the SQL `findEntities` rewrite hits. The probe (2026-05-26) confirmed
2001
+ * code-managed containers get the Cosmos default policy (everything
2002
+ * indexed). This guard is for operator-facing protection: externally
2003
+ * provisioned containers (ARM/Bicep) can have `excludedPaths` set, which
2004
+ * would force `findEntities` to scan rather than index-lookup.
2005
+ *
2006
+ * Single GET against the colls resource, minimal RU. Never fails
2007
+ * `ensureSchema` — a diagnostic must not break provisioning.
2008
+ */
2009
+ async runIndexingPolicyDiagnostic() {
2010
+ let policy;
2011
+ try {
2012
+ const props = await this.docClient.getContainerProperties();
2013
+ policy = props.indexingPolicy;
2014
+ } catch (err) {
2015
+ console.warn(
2016
+ `[CosmosDbProvider] could not verify indexing policy on container ${this.config.container}: ${err instanceof Error ? err.message : String(err)}`
2017
+ );
2018
+ return;
2019
+ }
2020
+ const offending = [];
2021
+ for (const entry of policy.excludedPaths) {
2022
+ const prefix = entry.path.replace(/\/[?*]$/, "");
2023
+ if (prefix === "" || prefix === "/") {
2024
+ for (const guard of GUARDED_INDEX_PATHS) {
2025
+ offending.push(`${guard} (covered by ${entry.path})`);
2026
+ }
2027
+ break;
2028
+ }
2029
+ for (const guard of GUARDED_INDEX_PATHS) {
2030
+ if (prefix === guard || guard.startsWith(prefix + "/")) {
2031
+ offending.push(`${guard} (excluded by ${entry.path})`);
2032
+ }
2033
+ }
2034
+ }
2035
+ if (offending.length > 0) {
2036
+ console.warn(
2037
+ `[CosmosDbProvider] Container ${this.config.container} has indexing-policy excludedPaths that cover paths used by findEntities. The query will fall back to scan and may exceed RU budgets:
2038
+ ${offending.join("\n ")}
2039
+ Verify the ARM/Bicep template that provisioned the container.`
2040
+ );
2041
+ }
2042
+ }
1747
2043
  /** Derive the REST endpoint from config — either explicit or from the Gremlin endpoint host. */
1748
2044
  getRestEndpoint() {
1749
2045
  if (this.config.restEndpoint) return this.config.restEndpoint.replace(/\/+$/, "");
@@ -1815,13 +2111,38 @@ var CosmosDbProvider = class {
1815
2111
  () => getVocabulary(this.conn, repositoryId)
1816
2112
  );
1817
2113
  }
2114
+ /**
2115
+ * Cached vocabulary read used by traversal compilation. The vocabulary is
2116
+ * compile-time context for the GremlinCompiler — it changes on the order of
2117
+ * once per session, but the traversal hot path pays one round-trip per call
2118
+ * to fetch it. The cache flips that to one round-trip per TTL window.
2119
+ *
2120
+ * Reads inside an active usage scope are still recorded if a fetch happens
2121
+ * (cache miss); cache hits emit no round-trip and therefore no usage entry.
2122
+ */
2123
+ async getVocabularyCached(repositoryId) {
2124
+ const now = Date.now();
2125
+ const cached = this.vocabularyCache.get(repositoryId);
2126
+ if (cached && cached.expiresAt > now) {
2127
+ return cached.vocab;
2128
+ }
2129
+ const vocab = await getVocabulary(this.conn, repositoryId);
2130
+ this.vocabularyCache.set(repositoryId, {
2131
+ vocab,
2132
+ expiresAt: now + VOCABULARY_CACHE_TTL_MS
2133
+ });
2134
+ return vocab;
2135
+ }
2136
+ /** Drop the cache entry for a repository — call after any vocabulary write. */
2137
+ invalidateVocabularyCache(repositoryId) {
2138
+ this.vocabularyCache.delete(repositoryId);
2139
+ }
1818
2140
  async saveVocabulary(repositoryId, vocabulary) {
1819
2141
  this.assertValidRepositoryId(repositoryId);
1820
- return this.track(
1821
- "saveVocabulary",
1822
- repositoryId,
1823
- () => saveVocabulary(this.conn, repositoryId, vocabulary)
1824
- );
2142
+ return this.track("saveVocabulary", repositoryId, async () => {
2143
+ await saveVocabulary(this.conn, repositoryId, vocabulary);
2144
+ this.invalidateVocabularyCache(repositoryId);
2145
+ });
1825
2146
  }
1826
2147
  async getVocabularyChangeLog(repositoryId, options) {
1827
2148
  this.assertValidRepositoryId(repositoryId);
@@ -1840,28 +2161,28 @@ var CosmosDbProvider = class {
1840
2161
  () => createEntity(this.conn, repositoryId, entity)
1841
2162
  );
1842
2163
  }
1843
- async getEntity(repositoryId, entityId) {
2164
+ async getEntity(repositoryId, entityId, options) {
1844
2165
  this.assertValidRepositoryId(repositoryId);
1845
2166
  return this.track(
1846
2167
  "getEntity",
1847
2168
  repositoryId,
1848
- () => getEntity(this.conn, repositoryId, entityId)
2169
+ () => getEntity(this.conn, repositoryId, entityId, options)
1849
2170
  );
1850
2171
  }
1851
- async getEntityBySlug(repositoryId, slug) {
2172
+ async getEntityBySlug(repositoryId, slug, options) {
1852
2173
  this.assertValidRepositoryId(repositoryId);
1853
2174
  return this.track(
1854
2175
  "getEntityBySlug",
1855
2176
  repositoryId,
1856
- () => getEntityBySlug(this.conn, repositoryId, slug)
2177
+ () => getEntityBySlug(this.conn, repositoryId, slug, options)
1857
2178
  );
1858
2179
  }
1859
- async getEntities(repositoryId, entityIds) {
2180
+ async getEntities(repositoryId, entityIds, options) {
1860
2181
  this.assertValidRepositoryId(repositoryId);
1861
2182
  return this.track(
1862
2183
  "getEntities",
1863
2184
  repositoryId,
1864
- () => getEntities(this.conn, repositoryId, entityIds)
2185
+ () => getEntities(this.conn, repositoryId, entityIds, options)
1865
2186
  );
1866
2187
  }
1867
2188
  async updateEntity(repositoryId, entityId, updates) {
@@ -1885,6 +2206,19 @@ var CosmosDbProvider = class {
1885
2206
  this.assertValidRepositoryId(repositoryId);
1886
2207
  return this.track("deleteEntities", repositoryId, () => this.deleteEntitiesImpl(repositoryId, ids));
1887
2208
  }
2209
+ /**
2210
+ * Single round-trip bulk-delete via the aggregate-side-effect pattern:
2211
+ * collapses the per-chunk existence-check + drop into one Gremlin call.
2212
+ *
2213
+ * g.V()...hasId(within(...)).has('entityType')
2214
+ * .aggregate('found').by('id') // collects the ids that match
2215
+ * .drop() // drops the vertices (and cascaded edges)
2216
+ * .cap('found') // emits the bucket as the single result
2217
+ *
2218
+ * The bucket is always emitted as a single list item — empty when nothing
2219
+ * matched (probe-verified 2026-05-25, local-tests/phase7-shape-probe.mjs).
2220
+ * `notFound` is derived client-side as the set difference.
2221
+ */
1888
2222
  async deleteEntitiesImpl(repositoryId, ids) {
1889
2223
  const deleted = [];
1890
2224
  const CHUNK = 100;
@@ -1897,26 +2231,14 @@ var CosmosDbProvider = class {
1897
2231
  bindings[p] = chunk[j];
1898
2232
  idParams.push(p);
1899
2233
  }
1900
- const withinExpr = `P.within(${idParams.join(", ")})`;
1901
- const existResult = await this.conn.submit(
1902
- `g.V().has('repositoryId', rid).has('id', ${withinExpr}).has('entityType').values('id')`,
2234
+ const withinExpr = `within(${idParams.join(", ")})`;
2235
+ const result = await this.conn.submit(
2236
+ `g.V().has('repositoryId', rid).hasId(${withinExpr}).has('entityType').aggregate('found').by('id').drop().cap('found')`,
1903
2237
  bindings
1904
2238
  );
1905
- const found = existResult.items;
1906
- if (found.length > 0) {
1907
- const dropBindings = { rid: repositoryId };
1908
- const dropIdParams = [];
1909
- for (let j = 0; j < found.length; j++) {
1910
- const p = `did${j}`;
1911
- dropBindings[p] = found[j];
1912
- dropIdParams.push(p);
1913
- }
1914
- const dropWithinExpr = `P.within(${dropIdParams.join(", ")})`;
1915
- await this.conn.submit(
1916
- `g.V().has('repositoryId', rid).has('id', ${dropWithinExpr}).has('entityType').drop()`,
1917
- dropBindings
1918
- );
1919
- deleted.push(...found);
2239
+ const bucket = result.items[0];
2240
+ if (Array.isArray(bucket)) {
2241
+ deleted.push(...bucket);
1920
2242
  }
1921
2243
  }
1922
2244
  const deletedSet = new Set(deleted);
@@ -1930,12 +2252,12 @@ var CosmosDbProvider = class {
1930
2252
  () => deleteEntitiesByType(this.conn, repositoryId, entityType)
1931
2253
  );
1932
2254
  }
1933
- async findEntities(repositoryId, query) {
2255
+ async findEntities(repositoryId, query, options) {
1934
2256
  this.assertValidRepositoryId(repositoryId);
1935
2257
  return this.track(
1936
2258
  "findEntities",
1937
2259
  repositoryId,
1938
- () => findEntities(this.conn, repositoryId, query)
2260
+ () => findEntities(this.docClient, repositoryId, query, options)
1939
2261
  );
1940
2262
  }
1941
2263
  // ─── Relationships ─────────────────────────────────────────────────
@@ -1976,6 +2298,20 @@ var CosmosDbProvider = class {
1976
2298
  this.assertValidRepositoryId(repositoryId);
1977
2299
  return this.track("deleteRelationships", repositoryId, () => this.deleteRelationshipsImpl(repositoryId, ids));
1978
2300
  }
2301
+ /**
2302
+ * Single round-trip bulk relationship delete via the aggregate-side-effect
2303
+ * pattern: collapses the per-chunk existence-check + drop into one Gremlin
2304
+ * call. Gremlin drop on edges is routed by the engine and the bucket gives
2305
+ * back the exact ids that were actually dropped, so `notFound` can be
2306
+ * derived client-side.
2307
+ *
2308
+ * Source-id partition routing is not exposed on this method (the public
2309
+ * surface accepts only edge ids), so the lookup may fan out across
2310
+ * partitions — see [docs/cosmosdb-gremlin-compatibility.md §`g.E().has`
2311
+ * doesn't always push partition down]. Callers that already hold a
2312
+ * StoredRelationship and want partition-scoped routing should add a
2313
+ * dedicated method when the need is concrete.
2314
+ */
1979
2315
  async deleteRelationshipsImpl(repositoryId, ids) {
1980
2316
  const deleted = [];
1981
2317
  const CHUNK = 100;
@@ -1988,23 +2324,14 @@ var CosmosDbProvider = class {
1988
2324
  bindings[p] = chunk[j];
1989
2325
  idParams.push(p);
1990
2326
  }
1991
- const withinExpr = `P.within(${idParams.join(", ")})`;
1992
- const existResult = await this.conn.submit(
1993
- `g.E().has('repositoryId', rid).has('id', ${withinExpr}).values('id')`,
2327
+ const withinExpr = `within(${idParams.join(", ")})`;
2328
+ const result = await this.conn.submit(
2329
+ `g.E().has('repositoryId', rid).hasId(${withinExpr}).aggregate('found').by('id').drop().cap('found')`,
1994
2330
  bindings
1995
2331
  );
1996
- const found = existResult.items;
1997
- if (found.length > 0) {
1998
- await cosmosRestBatchDelete(
1999
- this.getRestEndpoint(),
2000
- this.config.key,
2001
- this.config.database,
2002
- this.config.container,
2003
- repositoryId,
2004
- found,
2005
- this.config.rejectUnauthorized ?? true
2006
- );
2007
- deleted.push(...found);
2332
+ const bucket = result.items[0];
2333
+ if (Array.isArray(bucket)) {
2334
+ deleted.push(...bucket);
2008
2335
  }
2009
2336
  }
2010
2337
  const deletedSet = new Set(deleted);
@@ -2024,17 +2351,213 @@ var CosmosDbProvider = class {
2024
2351
  return this.track(
2025
2352
  "exploreNeighborhood",
2026
2353
  repositoryId,
2027
- () => exploreNeighborhood(this.conn, repositoryId, entityId, options)
2354
+ () => this.exploreNeighborhoodImpl(repositoryId, entityId, options)
2028
2355
  );
2029
2356
  }
2357
+ /**
2358
+ * Compiler-model implementation of exploreNeighborhood.
2359
+ *
2360
+ * Strategy: for each depth `d` from 1 to `options.depth`, build a
2361
+ * cumulative `TraversalSpec` with `d` steps and `returnMode: 'all'`, run
2362
+ * it through {@link executeTraversal}, then walk one BFS layer client-side
2363
+ * from the previous frontier using the returned edges.
2364
+ *
2365
+ * The server-side step direction is fixed to `'both'` (catches every edge
2366
+ * in either direction). The directional + bidirectional filter is applied
2367
+ * client-side per hop — i.e. `direction: 'outbound'` includes inbound edges
2368
+ * where `bidirectional` is true. The CosmosDB Gremlin compiler does not
2369
+ * natively express the `union(outE, inE.has(bidirectional))` shape that
2370
+ * would push this filter server-side, so it stays client-side; the
2371
+ * observable contract (which edges count toward "outbound" given
2372
+ * bidirectionality) is preserved.
2373
+ *
2374
+ * Round-trips per call: `options.depth` (one per layer). The previous BFS
2375
+ * was `1 + fanout + fanout² + …` round-trips for the same depth.
2376
+ */
2377
+ async exploreNeighborhoodImpl(repositoryId, entityId, options) {
2378
+ const layers = [];
2379
+ const visited = /* @__PURE__ */ new Set([entityId]);
2380
+ let frontier = /* @__PURE__ */ new Set([entityId]);
2381
+ for (let d = 1; d <= options.depth; d++) {
2382
+ if (frontier.size === 0) break;
2383
+ const spec = {
2384
+ start: { entityId },
2385
+ steps: buildExploreSteps(d, options),
2386
+ returnMode: "all",
2387
+ // Each round-trip fetches the cumulative subgraph at depth `d` and
2388
+ // we reconstruct layers client-side. The result can include every
2389
+ // edge and vertex reachable in ≤d hops in either direction; size
2390
+ // the limit generously.
2391
+ limit: 1e4,
2392
+ // Use 'full' + includeProvenance so executeTraversal returns full
2393
+ // StoredEntity rows (StorageNeighborhood embeds StoredEntity).
2394
+ detailLevel: "full",
2395
+ includeProvenance: true
2396
+ };
2397
+ const raw = await this.executeTraversal(repositoryId, spec);
2398
+ const edgesByVertex = /* @__PURE__ */ new Map();
2399
+ for (const rel of raw.allRelationships) {
2400
+ const a = edgesByVertex.get(rel.sourceEntityId);
2401
+ if (a) a.push(rel);
2402
+ else edgesByVertex.set(rel.sourceEntityId, [rel]);
2403
+ const b = edgesByVertex.get(rel.targetEntityId);
2404
+ if (b) b.push(rel);
2405
+ else edgesByVertex.set(rel.targetEntityId, [rel]);
2406
+ }
2407
+ const layer = {};
2408
+ const nextFrontier = /* @__PURE__ */ new Set();
2409
+ const layerEdgeSeen = /* @__PURE__ */ new Set();
2410
+ for (const fv of frontier) {
2411
+ const incident = edgesByVertex.get(fv) ?? [];
2412
+ for (const rel of incident) {
2413
+ const isSource = rel.sourceEntityId === fv;
2414
+ const isTarget = rel.targetEntityId === fv;
2415
+ let matchesDirection = false;
2416
+ let connectedId;
2417
+ if (isSource && (options.direction === "outbound" || options.direction === "both")) {
2418
+ matchesDirection = true;
2419
+ connectedId = rel.targetEntityId;
2420
+ } else if (isTarget && (options.direction === "inbound" || options.direction === "both")) {
2421
+ matchesDirection = true;
2422
+ connectedId = rel.sourceEntityId;
2423
+ } else if (rel.bidirectional) {
2424
+ if (isSource && options.direction === "inbound") {
2425
+ matchesDirection = true;
2426
+ connectedId = rel.targetEntityId;
2427
+ } else if (isTarget && options.direction === "outbound") {
2428
+ matchesDirection = true;
2429
+ connectedId = rel.sourceEntityId;
2430
+ }
2431
+ }
2432
+ if (!matchesDirection || !connectedId) continue;
2433
+ if (visited.has(connectedId)) continue;
2434
+ if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
2435
+ if (!(0, import_deep_memory5.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters)) continue;
2436
+ }
2437
+ const connectedEntity = raw.entityMap.get(connectedId);
2438
+ if (!connectedEntity) continue;
2439
+ if (options.entityTypes && options.entityTypes.length > 0 && !options.entityTypes.includes(connectedEntity.entityType)) {
2440
+ continue;
2441
+ }
2442
+ const edgeKey = `${rel.id}|${fv}->${connectedId}`;
2443
+ if (layerEdgeSeen.has(edgeKey)) continue;
2444
+ layerEdgeSeen.add(edgeKey);
2445
+ const relType = rel.relationshipType;
2446
+ if (!layer[relType]) {
2447
+ layer[relType] = { total: 0, entities: [], relationships: [] };
2448
+ }
2449
+ layer[relType].entities.push(connectedEntity);
2450
+ layer[relType].relationships.push(rel);
2451
+ layer[relType].total = layer[relType].entities.length;
2452
+ nextFrontier.add(connectedId);
2453
+ }
2454
+ }
2455
+ for (const relType of Object.keys(layer)) {
2456
+ const group = layer[relType];
2457
+ const start = options.offsetPerType;
2458
+ const end = start + options.limitPerType;
2459
+ group.entities = group.entities.slice(start, end);
2460
+ group.relationships = group.relationships.slice(start, end);
2461
+ }
2462
+ if (Object.keys(layer).length > 0) {
2463
+ layers.push(layer);
2464
+ }
2465
+ for (const id of nextFrontier) {
2466
+ visited.add(id);
2467
+ }
2468
+ frontier = nextFrontier;
2469
+ }
2470
+ return { centerId: entityId, layers };
2471
+ }
2030
2472
  async findPaths(repositoryId, sourceId, targetId, options) {
2031
2473
  this.assertValidRepositoryId(repositoryId);
2032
2474
  return this.track(
2033
2475
  "findPaths",
2034
2476
  repositoryId,
2035
- () => findPaths(this.conn, repositoryId, sourceId, targetId, options)
2477
+ () => this.findPathsImpl(repositoryId, sourceId, targetId, options)
2036
2478
  );
2037
2479
  }
2480
+ /**
2481
+ * Compiler-model implementation of findPaths.
2482
+ *
2483
+ * Strategy: build a `TraversalSpec` with a single `'both'` direction step
2484
+ * in `repeat()` mode with `emitIntermediates: true` and `returnMode:
2485
+ * 'path'`, run it once. The compiler always emits `.simplePath()` in path
2486
+ * mode for cycle prevention, producing
2487
+ * `.emit().repeat(bothE().otherV()).times(maxDepth).simplePath()
2488
+ * .range(...).path().by(<v>).by(<e>)`, which yields paths of every length
2489
+ * from 0 (the start vertex alone) to `maxDepth`. Live-probed against the
2490
+ * Cosmos emulator 2026-05-25 — see local-tests/phase4-repeat-emit-probe.mjs.
2491
+ *
2492
+ * The pre-Phase-4 Cosmos BFS in (now-deleted) `packages/storage-cosmosdb/
2493
+ * src/queries/traversal.ts` traversed edges with unconditional `bothE()`
2494
+ * regardless of the `bidirectional` flag (path discovery is reachability,
2495
+ * not semantic direction). Mirror that here by using step direction
2496
+ * `'both'` and not applying any direction filter. The plan's §6
2497
+ * observable-outputs rule requires preserving this.
2498
+ *
2499
+ * One round-trip total. The previous BFS was up to `1 + fanout + fanout² + …`.
2500
+ */
2501
+ async findPathsImpl(repositoryId, sourceId, targetId, options) {
2502
+ if (sourceId === targetId) {
2503
+ return { paths: [{ entityIds: [sourceId], relationshipIds: [] }], totalPaths: 1 };
2504
+ }
2505
+ const step = {
2506
+ direction: "both",
2507
+ repeat: { maxDepth: options.maxDepth, emitIntermediates: true }
2508
+ };
2509
+ if (options.relationshipTypes && options.relationshipTypes.length > 0) {
2510
+ step.relationshipTypes = options.relationshipTypes;
2511
+ }
2512
+ if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
2513
+ step.relationshipFilter = options.relationshipPropertyFilters;
2514
+ }
2515
+ const spec = {
2516
+ start: { entityId: sourceId },
2517
+ steps: [step],
2518
+ returnMode: "path",
2519
+ // Cycle prevention comes from the compiler unconditionally emitting
2520
+ // .simplePath() in path mode — replaces the explicit
2521
+ // `state.path.includes(nextId)` guard from the old BFS.
2522
+ // Pull the full pool of paths so we can post-filter to those ending at
2523
+ // targetId, then paginate. The emulator returns paths of every length
2524
+ // 0..maxDepth in one round-trip with the repeat+emit shape; cap
2525
+ // generously to ensure all candidates are inspected.
2526
+ limit: Math.max(options.limit + options.offset, options.limit) * 10,
2527
+ detailLevel: "full",
2528
+ includeProvenance: true
2529
+ };
2530
+ const raw = await this.executeTraversal(repositoryId, spec);
2531
+ const matchingPaths = [];
2532
+ for (const row of raw.pathRows) {
2533
+ const last = row.entityIds[row.entityIds.length - 1];
2534
+ if (last !== targetId) continue;
2535
+ if (options.entityTypes && options.entityTypes.length > 0) {
2536
+ let rejected = false;
2537
+ for (let i = 1; i < row.entityIds.length - 1; i++) {
2538
+ const intermediate = raw.entityMap.get(row.entityIds[i]);
2539
+ if (!intermediate) {
2540
+ rejected = true;
2541
+ break;
2542
+ }
2543
+ if (!options.entityTypes.includes(intermediate.entityType)) {
2544
+ rejected = true;
2545
+ break;
2546
+ }
2547
+ }
2548
+ if (rejected) continue;
2549
+ }
2550
+ matchingPaths.push({
2551
+ entityIds: [...row.entityIds],
2552
+ relationshipIds: [...row.relationshipIds]
2553
+ });
2554
+ }
2555
+ const paginated = matchingPaths.slice(options.offset, options.offset + options.limit);
2556
+ return {
2557
+ paths: paginated,
2558
+ totalPaths: matchingPaths.length
2559
+ };
2560
+ }
2038
2561
  // ─── Timeline ──────────────────────────────────────────────────────
2039
2562
  async getTimeline(repositoryId, entityId, options) {
2040
2563
  this.assertValidRepositoryId(repositoryId);
@@ -2122,79 +2645,205 @@ var CosmosDbProvider = class {
2122
2645
  }
2123
2646
  async traverse(repositoryId, spec) {
2124
2647
  this.assertValidRepositoryId(repositoryId);
2125
- return this.track("traverse", repositoryId, () => this.traverseImpl(repositoryId, spec));
2648
+ return this.track("traverse", repositoryId, () => this.traverseInternal(repositoryId, spec));
2649
+ }
2650
+ /**
2651
+ * Compile + execute a TraversalSpec against this repository's partition.
2652
+ *
2653
+ * Internal entrypoint shared by `traverse` (the public surface) and the
2654
+ * compiler-model rewrites of `exploreNeighborhood` / `findPaths`. Does NOT
2655
+ * wrap in `track()` — the outer public method owns its own usage scope, and
2656
+ * inner submits accumulate into that scope (the nested-scope guard in
2657
+ * `track()` keeps nested public calls from emitting duplicate records).
2658
+ */
2659
+ async traverseInternal(repositoryId, spec) {
2660
+ const raw = await this.executeTraversal(repositoryId, spec);
2661
+ const detailLevel = spec.detailLevel ?? "summary";
2662
+ const projectStoredEntity = (stored) => {
2663
+ const projected = (0, import_deep_memory5.projectEntity)(stored, detailLevel);
2664
+ if (!spec.includeProvenance) {
2665
+ delete projected["provenance"];
2666
+ }
2667
+ return projected;
2668
+ };
2669
+ const projectStoredRelationship = (rel, direction = "outbound") => ({
2670
+ id: rel.id,
2671
+ type: rel.relationshipType,
2672
+ sourceEntityId: rel.sourceEntityId,
2673
+ targetEntityId: rel.targetEntityId,
2674
+ direction,
2675
+ properties: rel.properties
2676
+ });
2677
+ let entities = [];
2678
+ let relationships;
2679
+ let paths;
2680
+ if (spec.returnMode === "terminal") {
2681
+ entities = raw.terminalEntities.map(projectStoredEntity);
2682
+ relationships = void 0;
2683
+ } else if (spec.returnMode === "all") {
2684
+ entities = raw.allEntities.map(projectStoredEntity);
2685
+ relationships = raw.allRelationships.map((r) => projectStoredRelationship(r));
2686
+ } else {
2687
+ paths = raw.pathRows.map((row) => ({
2688
+ length: Math.max(row.entityIds.length - 1, 0),
2689
+ entities: row.entityIds.map((id) => {
2690
+ const stored = raw.entityMap.get(id);
2691
+ if (!stored) {
2692
+ throw new import_deep_memory5.ProviderError(
2693
+ "Unpacking Gremlin path: entity referenced by path is missing from the result",
2694
+ "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2695
+ );
2696
+ }
2697
+ return projectStoredEntity(stored);
2698
+ }),
2699
+ relationships: row.relationshipIds.map((id, i) => {
2700
+ const stored = raw.relationshipMap.get(id);
2701
+ if (!stored) {
2702
+ throw new import_deep_memory5.ProviderError(
2703
+ "Unpacking Gremlin path: relationship referenced by path is missing from the result",
2704
+ "This indicates a Gremlin response shape mismatch \u2014 inspect compiledQuery."
2705
+ );
2706
+ }
2707
+ return projectStoredRelationship(stored, row.relationshipDirections[i]);
2708
+ })
2709
+ }));
2710
+ relationships = Array.from(raw.relationshipMap.values()).map(
2711
+ (rel) => projectStoredRelationship(rel, raw.pathRelFirstDirection.get(rel.id) ?? "outbound")
2712
+ );
2713
+ }
2714
+ const limit = spec.limit ?? 50;
2715
+ let total;
2716
+ if (spec.returnMode === "path") {
2717
+ total = paths?.length ?? 0;
2718
+ } else if (spec.returnMode === "all") {
2719
+ total = entities.length + (relationships?.length ?? 0);
2720
+ } else {
2721
+ total = entities.length;
2722
+ }
2723
+ const queryMetadata = {
2724
+ executionTimeMs: raw.executionTimeMs,
2725
+ resourceCost: raw.requestCharge != null ? { units: "RU", value: raw.requestCharge } : void 0,
2726
+ compiledQuery: raw.compiledQuery,
2727
+ compiledQueryLanguage: "gremlin",
2728
+ appliedLimits: {
2729
+ maxResults: limit,
2730
+ maxDepth: spec.steps?.length
2731
+ },
2732
+ truncated: total >= limit,
2733
+ truncationReason: total >= limit ? "result_limit" : void 0
2734
+ };
2735
+ return {
2736
+ entities,
2737
+ relationships,
2738
+ paths,
2739
+ total,
2740
+ returned: total,
2741
+ hasMore: total >= limit,
2742
+ queryMetadata
2743
+ };
2126
2744
  }
2127
- async traverseImpl(repositoryId, spec) {
2745
+ /**
2746
+ * Lower-level traversal helper: compiles a spec, submits to Gremlin, and
2747
+ * parses the rows into raw {@link StoredEntity} / {@link StoredRelationship}
2748
+ * objects (no detail-level projection, no provenance stripping). Used by
2749
+ * `traverseInternal` and by the storage-layer rewrites of
2750
+ * `exploreNeighborhood` / `findPaths` that need the full stored shape to
2751
+ * rebuild `StorageNeighborhood` / `StoragePathResult`.
2752
+ *
2753
+ * Returns a discriminated bag — only the fields relevant to `spec.returnMode`
2754
+ * are populated:
2755
+ * - `'terminal'`: `terminalEntities` (in row order, no dedup beyond what the
2756
+ * server emitted).
2757
+ * - `'all'`: `allEntities` and `allRelationships`, already server-deduped.
2758
+ * - `'path'`: `pathRows` plus the `entityMap` / `relationshipMap` lookup
2759
+ * tables and `pathRelFirstDirection` for the first-seen direction per
2760
+ * deduped edge id.
2761
+ */
2762
+ async executeTraversal(repositoryId, spec) {
2128
2763
  const startTime = Date.now();
2129
- const vocabulary = await this.getVocabulary(repositoryId);
2764
+ const vocabulary = await this.getVocabularyCached(repositoryId);
2130
2765
  const compiled = this.compiler.compile(spec, vocabulary);
2131
2766
  const scopedQuery = compiled.query.replace(
2132
2767
  "g.V()",
2133
2768
  "g.V().has('repositoryId', pRid)"
2134
2769
  );
2135
2770
  const scopedParams = { ...compiled.params, pRid: repositoryId };
2771
+ let result;
2136
2772
  try {
2137
- const result = await this.conn.submit(scopedQuery, scopedParams);
2138
- const executionTimeMs = Date.now() - startTime;
2139
- const detailLevel = spec.detailLevel ?? "summary";
2140
- const entities = [];
2141
- const relationships = [];
2142
- const paths = [];
2143
- if (spec.returnMode === "path") {
2144
- for (const item of result.items) {
2145
- const pathData = item;
2146
- if (pathData.objects) {
2147
- const pathEntities = [];
2148
- for (const obj of pathData.objects) {
2149
- const props = obj;
2150
- if (props["entityType"]) {
2151
- const stored = entityFromGremlin(props);
2152
- pathEntities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
2153
- }
2154
- }
2155
- if (pathEntities.length > 0) {
2156
- paths.push({
2157
- length: pathEntities.length - 1,
2158
- entities: pathEntities,
2159
- relationships: []
2160
- });
2161
- }
2162
- }
2163
- }
2164
- } else {
2165
- for (const item of result.items) {
2166
- const stored = entityFromGremlin(item);
2167
- entities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
2168
- }
2169
- }
2170
- const limit = spec.limit ?? 50;
2171
- const queryMetadata = {
2172
- executionTimeMs,
2173
- resourceCost: result.requestCharge != null ? { units: "RU", value: result.requestCharge } : void 0,
2174
- compiledQuery: scopedQuery,
2175
- compiledQueryLanguage: "gremlin",
2176
- appliedLimits: {
2177
- maxResults: limit,
2178
- maxDepth: spec.steps?.length
2179
- },
2180
- truncated: entities.length >= limit,
2181
- truncationReason: entities.length >= limit ? "result_limit" : void 0
2182
- };
2183
- return {
2184
- entities: spec.returnMode === "path" ? [] : entities,
2185
- relationships: spec.returnMode === "path" || spec.returnMode === "all" ? relationships : void 0,
2186
- paths: spec.returnMode === "path" ? paths : void 0,
2187
- total: entities.length + paths.length,
2188
- returned: entities.length + paths.length,
2189
- hasMore: entities.length >= limit || paths.length >= limit,
2190
- queryMetadata
2191
- };
2773
+ result = await this.conn.submit(scopedQuery, scopedParams);
2192
2774
  } catch (err) {
2193
- throw new import_deep_memory6.ProviderError(
2775
+ throw new import_deep_memory5.ProviderError(
2194
2776
  `Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
2195
2777
  "Check the traversal spec and ensure the CosmosDB connection is healthy."
2196
2778
  );
2197
2779
  }
2780
+ const executionTimeMs = Date.now() - startTime;
2781
+ const raw = {
2782
+ terminalEntities: [],
2783
+ allEntities: [],
2784
+ allRelationships: [],
2785
+ pathRows: [],
2786
+ entityMap: /* @__PURE__ */ new Map(),
2787
+ relationshipMap: /* @__PURE__ */ new Map(),
2788
+ pathRelFirstDirection: /* @__PURE__ */ new Map(),
2789
+ executionTimeMs,
2790
+ requestCharge: result.requestCharge,
2791
+ compiledQuery: scopedQuery
2792
+ };
2793
+ if (spec.returnMode === "terminal") {
2794
+ for (const item of result.items) {
2795
+ const stored = entityFromGremlin(item);
2796
+ raw.terminalEntities.push(stored);
2797
+ }
2798
+ } else if (spec.returnMode === "all") {
2799
+ for (const item of result.items) {
2800
+ const props = item;
2801
+ const kind = props["__kind"];
2802
+ if (kind === "v") {
2803
+ const stored = entityFromGremlin(props);
2804
+ raw.allEntities.push(stored);
2805
+ raw.entityMap.set(stored.id, stored);
2806
+ } else if (kind === "e") {
2807
+ const stored = relationshipFromGremlin(props);
2808
+ raw.allRelationships.push(stored);
2809
+ raw.relationshipMap.set(stored.id, stored);
2810
+ }
2811
+ }
2812
+ } else {
2813
+ for (const item of result.items) {
2814
+ const pathData = item;
2815
+ if (!pathData.objects) continue;
2816
+ const pathEntityIds = [];
2817
+ const pathRelIds = [];
2818
+ const pathRelDirections = [];
2819
+ let lastVertexId = null;
2820
+ for (const obj of pathData.objects) {
2821
+ const props = obj;
2822
+ const kind = props["__kind"];
2823
+ if (kind === "v") {
2824
+ const stored = entityFromGremlin(props);
2825
+ raw.entityMap.set(stored.id, stored);
2826
+ pathEntityIds.push(stored.id);
2827
+ lastVertexId = stored.id;
2828
+ } else if (kind === "e") {
2829
+ const stored = relationshipFromGremlin(props);
2830
+ raw.relationshipMap.set(stored.id, stored);
2831
+ pathRelIds.push(stored.id);
2832
+ const direction = lastVertexId === stored.sourceEntityId ? "outbound" : "inbound";
2833
+ pathRelDirections.push(direction);
2834
+ if (!raw.pathRelFirstDirection.has(stored.id)) {
2835
+ raw.pathRelFirstDirection.set(stored.id, direction);
2836
+ }
2837
+ }
2838
+ }
2839
+ raw.pathRows.push({
2840
+ entityIds: pathEntityIds,
2841
+ relationshipIds: pathRelIds,
2842
+ relationshipDirections: pathRelDirections
2843
+ });
2844
+ }
2845
+ }
2846
+ return raw;
2198
2847
  }
2199
2848
  /**
2200
2849
  * Execute a raw Gremlin query with caller-supplied bindings.
@@ -2229,67 +2878,16 @@ function unwrapGremlinValue(val) {
2229
2878
  if (Array.isArray(val) && val.length > 0) return val[0];
2230
2879
  return val;
2231
2880
  }
2232
- function cosmosAuthToken(verb, resourceType, resourceLink, date, key) {
2233
- const payload = `${verb.toLowerCase()}
2234
- ${resourceType.toLowerCase()}
2235
- ${resourceLink}
2236
- ${date.toLowerCase()}
2237
-
2238
- `;
2239
- const keyBuffer = Buffer.from(key, "base64");
2240
- const hmac = import_node_crypto.default.createHmac("sha256", keyBuffer);
2241
- hmac.update(payload);
2242
- const signature = hmac.digest("base64");
2243
- return encodeURIComponent(`type=master&ver=1.0&sig=${signature}`);
2244
- }
2245
- async function cosmosRestPut(restBase, key, urlPath, resourceLink, resourceType, body, rejectUnauthorized) {
2246
- const date = (/* @__PURE__ */ new Date()).toUTCString();
2247
- const token = cosmosAuthToken("post", resourceType, resourceLink, date, key);
2248
- const url = `${restBase}/${urlPath}`;
2249
- const options = {
2250
- method: "POST",
2251
- headers: {
2252
- "Authorization": token,
2253
- "x-ms-version": "2018-12-31",
2254
- "x-ms-date": date,
2255
- "Content-Type": "application/json"
2256
- },
2257
- body: JSON.stringify(body)
2258
- };
2259
- if (!rejectUnauthorized) {
2260
- process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
2261
- }
2262
- const response = await fetch(url, options);
2263
- if (response.status === 201) return true;
2264
- if (response.status === 409) return false;
2265
- const text = await response.text();
2266
- throw new Error(`CosmosDB REST ${response.status}: ${text}`);
2267
- }
2268
- async function cosmosRestBatchDelete(restBase, key, database, container, partitionKeyValue, ids, rejectUnauthorized) {
2269
- const resourceLink = `dbs/${database}/colls/${container}`;
2270
- const date = (/* @__PURE__ */ new Date()).toUTCString();
2271
- const token = cosmosAuthToken("post", "docs", resourceLink, date, key);
2272
- if (!rejectUnauthorized) {
2273
- process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
2881
+ function buildExploreSteps(depth, options) {
2882
+ const base = { direction: "both" };
2883
+ if (options.relationshipTypes && options.relationshipTypes.length > 0) {
2884
+ base.relationshipTypes = options.relationshipTypes;
2274
2885
  }
2275
- const ops = ids.map((id) => ({ operationType: "Delete", id }));
2276
- const response = await fetch(`${restBase}/${resourceLink}/docs`, {
2277
- method: "POST",
2278
- headers: {
2279
- "Authorization": token,
2280
- "x-ms-version": "2020-07-15",
2281
- "x-ms-date": date,
2282
- "Content-Type": "application/json",
2283
- "x-ms-documentdb-partitionkey": JSON.stringify([partitionKeyValue]),
2284
- "x-ms-cosmos-is-batch-request": "true",
2285
- "x-ms-cosmos-batch-atomic": "true"
2286
- },
2287
- body: JSON.stringify(ops)
2288
- });
2289
- if (response.status !== 200 && response.status !== 207) {
2290
- const text = await response.text();
2291
- throw new import_deep_memory6.ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
2886
+ const steps = [];
2887
+ for (let i = 0; i < depth; i++) {
2888
+ steps.push({ ...base });
2292
2889
  }
2890
+ return steps;
2293
2891
  }
2294
2892
  // Annotate the CommonJS export names for ESM import in node:
2295
2893
  0 && (module.exports = {