@sanity/workflow-engine 0.2.0 → 0.3.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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var groqJs = require("groq-js"), v = require("valibot");
3
+ var groqJs = require("groq-js"), schema = require("./_chunks-cjs/schema.cjs"), v = require("valibot");
4
4
  function _interopNamespaceCompat(e) {
5
5
  if (e && typeof e == "object" && "default" in e) return e;
6
6
  var n = /* @__PURE__ */ Object.create(null);
@@ -194,9 +194,94 @@ function stripStateForLake(value) {
194
194
  function bareId(id) {
195
195
  return isGdrUri(id) ? extractDocumentId(id) : id;
196
196
  }
197
- function randomKey(length = 12) {
198
- const bytes = new Uint8Array(length);
199
- return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
197
+ function getPath(value, path) {
198
+ let current = value;
199
+ for (const part of path.split(".")) {
200
+ if (current == null || typeof current != "object") return;
201
+ current = current[part];
202
+ }
203
+ return current;
204
+ }
205
+ function readStateSlot(instance, scope, slotId, stageId, taskId) {
206
+ if (scope === "workflow")
207
+ return slotValue$1(instance.state?.find((s) => s.id === slotId));
208
+ const stageEntry = instance.stages.find(
209
+ (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
210
+ );
211
+ if (stageEntry === void 0) return;
212
+ if (scope === "stage")
213
+ return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
214
+ const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
215
+ return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
216
+ }
217
+ function slotValue$1(slot) {
218
+ if (slot != null && typeof slot == "object")
219
+ return slot.value;
220
+ }
221
+ function resolveStaticSource(src, ctx) {
222
+ switch (src.source) {
223
+ case "literal":
224
+ return { handled: !0, value: src.value };
225
+ case "param":
226
+ return { handled: !0, value: ctx.params?.[src.paramId] };
227
+ case "actor":
228
+ return { handled: !0, value: ctx.actor };
229
+ case "now":
230
+ return { handled: !0, value: ctx.now };
231
+ default:
232
+ return { handled: !1 };
233
+ }
234
+ }
235
+ function resolveSource(src, ctx) {
236
+ const staticValue = resolveStaticSource(src, ctx);
237
+ if (staticValue.handled) return staticValue.value;
238
+ switch (src.source) {
239
+ case "self":
240
+ return ctx.instance._id;
241
+ case "stageId":
242
+ return ctx.stageId ?? ctx.instance.currentStageId;
243
+ case "stateRead":
244
+ return resolveStateRead(src, ctx);
245
+ case "effectOutput":
246
+ return resolveEffectOutput(src, ctx);
247
+ case "object":
248
+ return resolveObject(src, ctx);
249
+ case "row":
250
+ case "parentState":
251
+ return;
252
+ default:
253
+ return;
254
+ }
255
+ }
256
+ function resolveStateRead(src, ctx) {
257
+ const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
258
+ return src.path !== void 0 ? getPath(value, src.path) : value;
259
+ }
260
+ function resolveEffectOutput(src, ctx) {
261
+ const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
262
+ if (entry === void 0) return null;
263
+ const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
264
+ return value === void 0 ? null : value;
265
+ }
266
+ function resolveObject(src, ctx) {
267
+ const out = {};
268
+ for (const [field, fieldSrc] of Object.entries(src.fields))
269
+ out[field] = resolveSource(fieldSrc, ctx);
270
+ return out;
271
+ }
272
+ async function resolveBindings(args) {
273
+ const { bindings, staticInput, instance, now, params, actor } = args, resolved = {};
274
+ if (bindings) {
275
+ const ctx = {
276
+ instance,
277
+ now,
278
+ ...params !== void 0 ? { params } : {},
279
+ ...actor !== void 0 ? { actor } : {}
280
+ };
281
+ for (const [key, src] of Object.entries(bindings))
282
+ resolved[key] = resolveSource(src, ctx);
283
+ }
284
+ return { ...resolved, ...staticInput };
200
285
  }
201
286
  async function evaluateFilter(args) {
202
287
  const { filter, definition, snapshot, params } = args;
@@ -252,130 +337,333 @@ function matchesParamType(type, value) {
252
337
  function describeValue(value) {
253
338
  return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
254
339
  }
255
- function buildSnapshot(args) {
256
- const docs = [], knownIds = /* @__PURE__ */ new Set();
257
- for (const { doc, resource } of args.docs) {
258
- const uri = gdrFromResource(resource, doc._id), restamped = restampForSnapshot(doc, uri, resource);
259
- docs.push(restamped), knownIds.add(uri);
340
+ const GUARD_DOC_TYPE = "temp.system.guard";
341
+ class MutationGuardDeniedError extends Error {
342
+ denied;
343
+ documentId;
344
+ action;
345
+ constructor(args) {
346
+ const ids = args.denied.map((d) => d.guardId).join(", ");
347
+ super(`Mutation on "${args.documentId}" (${args.action}) denied by guard(s) [${ids}]`), this.name = "MutationGuardDeniedError", this.denied = args.denied, this.documentId = args.documentId, this.action = args.action;
260
348
  }
261
- return { docs, knownIds };
262
- }
263
- function restampForSnapshot(doc, gdrUri2, resource) {
264
- return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
265
349
  }
266
- function rewriteRefsRecursive(value, resource) {
267
- if (Array.isArray(value))
268
- return value.map((v2) => rewriteRefsRecursive(v2, resource));
269
- if (value === null || typeof value != "object") return value;
270
- const obj = value, out = {};
271
- for (const [k, v2] of Object.entries(obj))
272
- k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
273
- return out;
350
+ function lakeGuardId(args) {
351
+ return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
274
352
  }
275
- async function hydrateSnapshot(args) {
276
- const { client, clientForGdr, instance, definition } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri) => {
277
- if (visited.has(uri)) return;
278
- const fetched = await loadByGdr(client, clientForGdr, instance.workflowResource, uri);
279
- fetched && (loaded.push(fetched), visited.add(uri));
353
+ function compileGuard(args) {
354
+ return {
355
+ _id: args.id,
356
+ _type: GUARD_DOC_TYPE,
357
+ resourceType: args.resourceType,
358
+ resourceId: args.resourceId,
359
+ owner: args.owner,
360
+ sourceInstanceId: args.sourceInstanceId,
361
+ sourceDefinitionId: args.sourceDefinitionId,
362
+ sourceStageId: args.sourceStageId,
363
+ ...args.name !== void 0 ? { name: args.name } : {},
364
+ ...args.description !== void 0 ? { description: args.description } : {},
365
+ match: args.match,
366
+ predicate: args.predicate,
367
+ metadata: args.metadata
280
368
  };
281
- loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
282
- for (const ancestor of instance.ancestors)
283
- await loadInto(ancestor.id);
284
- for (const uri of collectSlotDocUris(instance.state))
285
- await loadInto(uri);
286
- if (definition) {
287
- const stageEntry = instance.stages.find(
288
- (s) => s.id === instance.currentStageId && s.exitedAt === void 0
289
- );
290
- for (const uri of collectSlotDocUris(stageEntry?.state))
291
- await loadInto(uri);
369
+ }
370
+ function toGroqJsPredicate(predicate) {
371
+ return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
372
+ }
373
+ function globMatch(pattern, value) {
374
+ if (!pattern.includes("*")) return pattern === value;
375
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
376
+ return new RegExp(`^${escaped}$`).test(value);
377
+ }
378
+ function guardMatches(guard, doc, action) {
379
+ const m = guard.match;
380
+ if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
381
+ return !1;
382
+ if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
383
+ const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
384
+ if (!byRef && !byPattern) return !1;
292
385
  }
293
- return buildSnapshot({ docs: loaded });
386
+ return !0;
294
387
  }
295
- async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri) {
296
- let parsed;
388
+ async function evaluateMutationGuard(args) {
389
+ const { guard, context } = args;
390
+ if (guard.predicate === "") return !1;
391
+ const params = { guard, mutation: { action: context.action } };
297
392
  try {
298
- parsed = parseGdr(uri);
393
+ const tree = groqJs.parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
394
+ return await (await groqJs.evaluate(tree, {
395
+ before: context.before,
396
+ after: context.after,
397
+ params,
398
+ ...context.identity !== void 0 ? { identity: context.identity } : {}
399
+ })).get() === !0;
299
400
  } catch {
300
- const doc2 = await defaultClient.getDocument(uri);
301
- return doc2 ? { doc: doc2, resource: defaultResource } : null;
401
+ return !1;
302
402
  }
303
- const doc = await clientForGdr(parsed).getDocument(parsed.documentId);
304
- return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
305
403
  }
306
- function resourceFromParsed(parsed) {
307
- return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
404
+ async function denyingGuards(args) {
405
+ const { guards, doc, context } = args, denied = [];
406
+ for (const guard of guards)
407
+ guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
408
+ return denied;
308
409
  }
309
- function collectSlotDocUris(resolvedStateSlots) {
310
- return Array.isArray(resolvedStateSlots) ? resolvedStateSlots.flatMap(slotDocUris) : [];
410
+ function resourceOf(p) {
411
+ return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
311
412
  }
312
- function slotDocUris(slot) {
313
- if (!slot || typeof slot != "object") return [];
314
- const s = slot;
315
- if (s._type === "workflow.state.doc.ref") {
316
- const v2 = s.value, id = refId(v2);
317
- return id !== void 0 ? [id] : [];
318
- }
319
- if (s._type === "workflow.state.doc.refs") {
320
- const v2 = s.value;
321
- return Array.isArray(v2) ? v2.map(refId).filter((id) => id !== void 0) : [];
413
+ function resolveIdRefTarget(src, ctx) {
414
+ const value = resolveSource(src, ctx);
415
+ if (typeof value == "string" && isGdrUri(value))
416
+ return { parsed: parseGdr(value) };
417
+ if (value && typeof value == "object") {
418
+ const v2 = value;
419
+ if (typeof v2.id == "string" && isGdrUri(v2.id))
420
+ return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
322
421
  }
323
- return [];
324
- }
325
- function refId(ref) {
326
- return ref && typeof ref.id == "string" ? ref.id : void 0;
422
+ return null;
327
423
  }
328
- function getPath(value, path) {
329
- let current = value;
330
- for (const part of path.split(".")) {
331
- if (current == null || typeof current != "object") return;
332
- current = current[part];
424
+ function resolveIdRefTargets(idRefs, ctx) {
425
+ const targets = [];
426
+ for (const src of idRefs ?? []) {
427
+ const target = resolveIdRefTarget(src, ctx);
428
+ if (target === null) return null;
429
+ targets.push(target);
333
430
  }
334
- return current;
431
+ return targets.length === 0 ? null : targets;
335
432
  }
336
- class SlotValueShapeError extends Error {
337
- slotType;
338
- slotId;
339
- issues;
340
- constructor(args) {
341
- const issueText = args.issues.join("; ");
342
- super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
433
+ function assertSingleResource(targets) {
434
+ const resource = resourceOf(targets[0].parsed);
435
+ for (const g of targets) {
436
+ const r = resourceOf(g.parsed);
437
+ if (r.type !== resource.type || r.id !== resource.id)
438
+ throw new Error(
439
+ `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
440
+ );
343
441
  }
442
+ return resource;
344
443
  }
345
- const GdrShape = v__namespace.looseObject({
346
- id: v__namespace.pipe(
347
- v__namespace.string(),
348
- v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
349
- ),
350
- type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
351
- }), ReleaseRefShape = v__namespace.looseObject({
352
- id: v__namespace.pipe(
353
- v__namespace.string(),
354
- v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
355
- ),
356
- type: v__namespace.literal("system.release"),
357
- releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
358
- }), ActorShape = v__namespace.looseObject({
359
- kind: v__namespace.picklist(["user", "ai", "system"]),
360
- id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
361
- roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
362
- onBehalfOf: v__namespace.optional(v__namespace.string())
363
- }), AssigneeShape = v__namespace.union([
364
- v__namespace.looseObject({ kind: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
365
- v__namespace.looseObject({ kind: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
366
- ]), ChecklistItemShape = v__namespace.looseObject({
367
- label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
368
- done: v__namespace.boolean(),
369
- doneBy: v__namespace.optional(v__namespace.string()),
370
- doneAt: v__namespace.optional(v__namespace.string()),
371
- _key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
372
- }), NoteItemShape = v__namespace.pipe(
373
- v__namespace.looseObject({
374
- _key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
375
- }),
376
- v__namespace.check(
377
- (val) => typeof val == "object" && val !== null && !Array.isArray(val),
378
- "must be an object"
444
+ function bareIdRefs(targets) {
445
+ return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
446
+ }
447
+ function resolveMatchTypes(targets, authorTypes) {
448
+ if (authorTypes !== void 0) return authorTypes;
449
+ const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
450
+ return inferred.length > 0 ? inferred : void 0;
451
+ }
452
+ function resolveMetadata(metadata, ctx) {
453
+ const out = {};
454
+ for (const [k, src] of Object.entries(metadata ?? {}))
455
+ out[k] = resolveSource(src, ctx);
456
+ return out;
457
+ }
458
+ const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
459
+ function resolveGuard(guard, index, instance, stageId, now) {
460
+ const ctx = { instance, stageId, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
461
+ if (targets === null) return null;
462
+ const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
463
+ return { doc: compileGuard({
464
+ id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
465
+ resourceType: resource.type,
466
+ resourceId: resource.id,
467
+ owner: GUARD_OWNER,
468
+ sourceInstanceId: instance._id,
469
+ sourceDefinitionId: instance.workflowId,
470
+ sourceStageId: stageId,
471
+ ...guard.name !== void 0 ? { name: guard.name } : {},
472
+ ...guard.description !== void 0 ? { description: guard.description } : {},
473
+ match: {
474
+ ...types !== void 0 ? { types } : {},
475
+ idRefs: bareIdRefs(targets),
476
+ ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
477
+ actions: guard.match.actions
478
+ },
479
+ predicate: guard.predicate ?? "",
480
+ metadata: resolveMetadata(guard.metadata, ctx)
481
+ }), routeGdr: targets[0].parsed };
482
+ }
483
+ async function upsertGuard(client, doc) {
484
+ if (!await client.getDocument(doc._id)) {
485
+ await client.create(doc);
486
+ return;
487
+ }
488
+ const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
489
+ await client.patch(doc._id).set(body).commit();
490
+ }
491
+ function resolvedStageGuards(args) {
492
+ const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
493
+ for (const [index, guard] of (stage?.guards ?? []).entries()) {
494
+ const resolved = resolveGuard(guard, index, args.instance, args.stageId, args.now);
495
+ resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
496
+ }
497
+ return out;
498
+ }
499
+ async function committedStageId(args) {
500
+ return (await args.client.getDocument(args.instance._id))?.currentStageId;
501
+ }
502
+ async function deployStageGuards(args) {
503
+ if (await committedStageId(args) === args.stageId)
504
+ for (const { client, doc } of resolvedStageGuards(args))
505
+ await upsertGuard(client, doc);
506
+ }
507
+ async function retractStageGuards(args) {
508
+ const live = await committedStageId(args);
509
+ if (!(live === void 0 || live === args.stageId))
510
+ for (const { client, doc } of resolvedStageGuards(args))
511
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
512
+ }
513
+ function randomKey(length = 12) {
514
+ const bytes = new Uint8Array(length);
515
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
516
+ }
517
+ function buildSnapshot(args) {
518
+ const docs = [], knownIds = /* @__PURE__ */ new Set();
519
+ for (const { doc, resource } of args.docs) {
520
+ const uri = gdrFromResource(resource, doc._id), restamped = restampForSnapshot(doc, uri, resource);
521
+ docs.push(restamped), knownIds.add(uri);
522
+ }
523
+ return { docs, knownIds };
524
+ }
525
+ function restampForSnapshot(doc, gdrUri2, resource) {
526
+ return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
527
+ }
528
+ function rewriteRefsRecursive(value, resource) {
529
+ if (Array.isArray(value))
530
+ return value.map((v2) => rewriteRefsRecursive(v2, resource));
531
+ if (value === null || typeof value != "object") return value;
532
+ const obj = value, out = {};
533
+ for (const [k, v2] of Object.entries(obj))
534
+ k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
535
+ return out;
536
+ }
537
+ function findOpenStageEntry(host) {
538
+ return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
539
+ }
540
+ function collectWatchRefs(instance) {
541
+ const stage = findOpenStageEntry(instance);
542
+ return [
543
+ gdrRef(instance.workflowResource, instance._id, instance._type),
544
+ ...instance.ancestors,
545
+ ...slotDocRefs(instance.state),
546
+ ...slotDocRefs(stage?.state),
547
+ ...slotReleaseRefs(instance.state),
548
+ ...slotReleaseRefs(stage?.state)
549
+ ];
550
+ }
551
+ function readsRaw(ref) {
552
+ return ref.type === "workflow.instance" || ref.type === "system.release";
553
+ }
554
+ function contentReleaseName(args) {
555
+ const { ref, perspective } = args;
556
+ if (!readsRaw(ref) && Array.isArray(perspective))
557
+ return perspective.find((entry) => entry !== "drafts" && entry !== "published" && entry !== "raw");
558
+ }
559
+ function subscriptionDocumentsForInstance(instance) {
560
+ const seen = /* @__PURE__ */ new Set(), documents = [];
561
+ for (const ref of collectWatchRefs(instance))
562
+ !isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push({ ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type }));
563
+ return {
564
+ documents,
565
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
566
+ };
567
+ }
568
+ function slotDocRefs(slots) {
569
+ return slotEntries(slots).flatMap((slot) => slot._type === "workflow.state.doc.ref" ? isGdr(slot.value) ? [slot.value] : [] : slot._type === "workflow.state.doc.refs" ? Array.isArray(slot.value) ? slot.value.filter(isGdr) : [] : []);
570
+ }
571
+ function slotReleaseRefs(slots) {
572
+ return slotEntries(slots).flatMap(
573
+ (slot) => slot._type === "workflow.state.release.ref" && isGdr(slot.value) ? [slot.value] : []
574
+ );
575
+ }
576
+ function slotEntries(slots) {
577
+ return Array.isArray(slots) ? slots.filter(
578
+ (slot) => !!slot && typeof slot == "object"
579
+ ) : [];
580
+ }
581
+ async function hydrateSnapshot(args) {
582
+ const { client, clientForGdr, instance, overlay } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri, perspective) => {
583
+ if (visited.has(uri)) return;
584
+ const held = overlay?.get(uri);
585
+ if (held !== void 0) {
586
+ loaded.push(held), visited.add(uri);
587
+ return;
588
+ }
589
+ const fetched = await loadByGdr(
590
+ client,
591
+ clientForGdr,
592
+ instance.workflowResource,
593
+ uri,
594
+ perspective
595
+ );
596
+ fetched && (loaded.push(fetched), visited.add(uri));
597
+ };
598
+ loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
599
+ for (const ref of collectWatchRefs(instance))
600
+ await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
601
+ return buildSnapshot({ docs: loaded });
602
+ }
603
+ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
604
+ let parsed;
605
+ try {
606
+ parsed = parseGdr(uri);
607
+ } catch {
608
+ const doc2 = await readDoc(defaultClient, uri, perspective);
609
+ return doc2 ? { doc: doc2, resource: defaultResource } : null;
610
+ }
611
+ const routed = clientForGdr(parsed), doc = await readDoc(routed, parsed.documentId, perspective);
612
+ return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
613
+ }
614
+ async function readDoc(client, id, perspective) {
615
+ return perspective === void 0 ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
616
+ }
617
+ function resourceFromParsed(parsed) {
618
+ return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
619
+ }
620
+ function collectSlotDocUris(resolvedStateSlots) {
621
+ return slotDocRefs(resolvedStateSlots).map((ref) => ref.id);
622
+ }
623
+ const WORKFLOW_INSTANCE_TYPE = "workflow.instance";
624
+ class SlotValueShapeError extends Error {
625
+ slotType;
626
+ slotId;
627
+ issues;
628
+ constructor(args) {
629
+ const issueText = args.issues.join("; ");
630
+ super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
631
+ }
632
+ }
633
+ const GdrShape = v__namespace.looseObject({
634
+ id: v__namespace.pipe(
635
+ v__namespace.string(),
636
+ v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
637
+ ),
638
+ type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
639
+ }), ReleaseRefShape = v__namespace.looseObject({
640
+ id: v__namespace.pipe(
641
+ v__namespace.string(),
642
+ v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
643
+ ),
644
+ type: v__namespace.literal("system.release"),
645
+ releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
646
+ }), ActorShape = v__namespace.looseObject({
647
+ kind: v__namespace.picklist(["user", "ai", "system"]),
648
+ id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
649
+ roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
650
+ onBehalfOf: v__namespace.optional(v__namespace.string())
651
+ }), AssigneeShape = v__namespace.union([
652
+ v__namespace.looseObject({ kind: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
653
+ v__namespace.looseObject({ kind: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
654
+ ]), ChecklistItemShape = v__namespace.looseObject({
655
+ label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
656
+ done: v__namespace.boolean(),
657
+ doneBy: v__namespace.optional(v__namespace.string()),
658
+ doneAt: v__namespace.optional(v__namespace.string()),
659
+ _key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
660
+ }), NoteItemShape = v__namespace.pipe(
661
+ v__namespace.looseObject({
662
+ _key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
663
+ }),
664
+ v__namespace.check(
665
+ (val) => typeof val == "object" && val !== null && !Array.isArray(val),
666
+ "must be an object"
379
667
  )
380
668
  ), NullableString = v__namespace.union([v__namespace.null(), v__namespace.string()]), NullableNumber = v__namespace.union([v__namespace.null(), v__namespace.number()]), NullableBoolean = v__namespace.union([v__namespace.null(), v__namespace.boolean()]), NullableDateTime = v__namespace.union([
381
669
  v__namespace.null(),
@@ -407,15 +695,15 @@ function isAppendable(slotType) {
407
695
  return slotType in itemSchemas;
408
696
  }
409
697
  function validateSlotValue(args) {
410
- const schema = valueSchemas[args.slotType];
411
- if (schema === void 0)
698
+ const schema2 = valueSchemas[args.slotType];
699
+ if (schema2 === void 0)
412
700
  throw new SlotValueShapeError({
413
701
  slotType: args.slotType,
414
702
  slotId: args.slotId,
415
703
  issues: [`unknown slot type ${args.slotType}`],
416
704
  mode: "value"
417
705
  });
418
- const result = v__namespace.safeParse(schema, args.value);
706
+ const result = v__namespace.safeParse(schema2, args.value);
419
707
  if (!result.success)
420
708
  throw new SlotValueShapeError({
421
709
  slotType: args.slotType,
@@ -432,7 +720,7 @@ function validateSlotAppendItem(args) {
432
720
  issues: [`slot type ${args.slotType} does not support append`],
433
721
  mode: "item"
434
722
  });
435
- const schema = itemSchemas[args.slotType], result = v__namespace.safeParse(schema, args.item);
723
+ const schema2 = itemSchemas[args.slotType], result = v__namespace.safeParse(schema2, args.item);
436
724
  if (!result.success)
437
725
  throw new SlotValueShapeError({
438
726
  slotType: args.slotType,
@@ -599,7 +887,12 @@ async function loadContext(client, instanceId, options) {
599
887
  const instance = await client.getDocument(instanceId);
600
888
  if (!instance)
601
889
  throw new Error(`Workflow instance ${instanceId} not found`);
602
- const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition });
890
+ const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
891
+ client,
892
+ clientForGdr,
893
+ instance,
894
+ ...options?.overlay !== void 0 ? { overlay: options.overlay } : {}
895
+ });
603
896
  return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
604
897
  }
605
898
  function ctxEvaluateFilter(ctx, filter) {
@@ -619,7 +912,7 @@ async function buildEngineContext(args) {
619
912
  now: clock(),
620
913
  instance,
621
914
  definition,
622
- snapshot: await hydrateSnapshot({ client, clientForGdr, instance, definition })
915
+ snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
623
916
  };
624
917
  }
625
918
  function findStage(definition, stageId) {
@@ -668,297 +961,281 @@ async function resolveTaskStateSlots(args) {
668
961
  randomKey
669
962
  });
670
963
  }
671
- function readStateSlot(instance, scope, slotId, stageId, taskId) {
672
- if (scope === "workflow")
673
- return slotValue$1(instance.state?.find((s) => s.id === slotId));
674
- const stageEntry = instance.stages.find(
675
- (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
676
- );
677
- if (stageEntry === void 0) return;
678
- if (scope === "stage")
679
- return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
680
- const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
681
- return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
682
- }
683
- function slotValue$1(slot) {
684
- if (slot != null && typeof slot == "object")
685
- return slot.value;
964
+ function effectsContextEntry(id, value) {
965
+ const _key = randomKey();
966
+ return typeof value == "string" ? { _key, _type: "effectsContext.string", id, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", id, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", id, value } : { _key, _type: "effectsContext.ref", id, value };
686
967
  }
687
- function resolveStaticSource(src, ctx) {
688
- switch (src.source) {
689
- case "literal":
690
- return { handled: !0, value: src.value };
691
- case "param":
692
- return { handled: !0, value: ctx.params?.[src.paramId] };
693
- case "actor":
694
- return { handled: !0, value: ctx.actor };
695
- case "now":
696
- return { handled: !0, value: ctx.now };
697
- default:
698
- return { handled: !1 };
699
- }
968
+ function buildInstanceBase(args) {
969
+ const { id, now, actor, perspective } = args;
970
+ return {
971
+ _id: id,
972
+ _type: WORKFLOW_INSTANCE_TYPE,
973
+ _rev: "",
974
+ _createdAt: now,
975
+ _updatedAt: now,
976
+ tags: args.tags,
977
+ workflowResource: args.workflowResource,
978
+ workflowId: args.workflowId,
979
+ pinnedVersion: args.pinnedVersion,
980
+ definitionSnapshot: JSON.stringify(args.definition),
981
+ state: args.state,
982
+ effectsContext: args.effectsContext,
983
+ ancestors: args.ancestors,
984
+ ...perspective !== void 0 ? { perspective } : {},
985
+ currentStageId: args.initialStageId,
986
+ stages: [],
987
+ pendingEffects: [],
988
+ effectHistory: [],
989
+ history: [
990
+ {
991
+ _key: randomKey(),
992
+ _type: "workflow.history.stageEntered",
993
+ at: now,
994
+ stageId: args.initialStageId,
995
+ ...actor !== void 0 ? { actor } : {}
996
+ }
997
+ ],
998
+ startedAt: now,
999
+ lastChangedAt: now
1000
+ };
700
1001
  }
701
- function resolveSource(src, ctx) {
702
- const staticValue = resolveStaticSource(src, ctx);
703
- if (staticValue.handled) return staticValue.value;
704
- switch (src.source) {
705
- case "self":
706
- return ctx.instance._id;
707
- case "stageId":
708
- return ctx.stageId ?? ctx.instance.currentStageId;
709
- case "stateRead":
710
- return resolveStateRead(src, ctx);
711
- case "effectOutput":
712
- return resolveEffectOutput(src, ctx);
713
- case "object":
714
- return resolveObject(src, ctx);
715
- case "row":
716
- case "parentState":
717
- return;
718
- default:
719
- return;
720
- }
721
- }
722
- function resolveStateRead(src, ctx) {
723
- const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
724
- return src.path !== void 0 ? getPath(value, src.path) : value;
725
- }
726
- function resolveEffectOutput(src, ctx) {
727
- const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
728
- if (entry === void 0) return null;
729
- const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
730
- return value === void 0 ? null : value;
731
- }
732
- function resolveObject(src, ctx) {
733
- const out = {};
734
- for (const [field, fieldSrc] of Object.entries(src.fields))
735
- out[field] = resolveSource(fieldSrc, ctx);
736
- return out;
737
- }
738
- async function resolveBindings(args) {
739
- const { bindings, staticInput, instance, now, params, actor } = args, resolved = {};
740
- if (bindings) {
741
- const ctx = {
742
- instance,
743
- now,
744
- ...params !== void 0 ? { params } : {},
745
- ...actor !== void 0 ? { actor } : {}
746
- };
747
- for (const [key, src] of Object.entries(bindings))
748
- resolved[key] = resolveSource(src, ctx);
749
- }
750
- return { ...resolved, ...staticInput };
751
- }
752
- function effectsContextEntry(id, value) {
753
- const _key = randomKey();
754
- return typeof value == "string" ? { _key, _type: "effectsContext.string", id, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", id, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", id, value } : { _key, _type: "effectsContext.ref", id, value };
1002
+ function startMutation(instance) {
1003
+ return {
1004
+ currentStageId: instance.currentStageId,
1005
+ // Deep-ish copy. Per-scope state slot arrays need their own copies
1006
+ // so ops can mutate without leaking into the source.
1007
+ state: (instance.state ?? []).map((s) => ({ ...s })),
1008
+ stages: instance.stages.map((s) => ({
1009
+ ...s,
1010
+ state: (s.state ?? []).map((slot) => ({ ...slot })),
1011
+ tasks: s.tasks.map((t) => ({
1012
+ ...t,
1013
+ ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1014
+ }))
1015
+ })),
1016
+ pendingEffects: [...instance.pendingEffects],
1017
+ effectHistory: [...instance.effectHistory],
1018
+ effectsContext: [...instance.effectsContext],
1019
+ history: [...instance.history],
1020
+ lastChangedAt: instance.lastChangedAt,
1021
+ ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1022
+ pendingCreates: []
1023
+ };
755
1024
  }
756
- function buildInstanceBase(args) {
757
- const { id, now, actor, perspective } = args;
1025
+ function taskStatusChangedEntry(args) {
758
1026
  return {
759
- _id: id,
760
- _type: "workflow.instance",
761
- _rev: "",
762
- _createdAt: now,
763
- _updatedAt: now,
764
- tags: args.tags,
765
- workflowResource: args.workflowResource,
766
- workflowId: args.workflowId,
767
- pinnedVersion: args.pinnedVersion,
768
- definitionSnapshot: JSON.stringify(args.definition),
769
- state: args.state,
770
- effectsContext: args.effectsContext,
771
- ancestors: args.ancestors,
772
- ...perspective !== void 0 ? { perspective } : {},
773
- currentStageId: args.initialStageId,
774
- stages: [],
775
- pendingEffects: [],
776
- effectHistory: [],
777
- history: [
778
- {
779
- _key: randomKey(),
780
- _type: "workflow.history.stageEntered",
781
- at: now,
782
- stageId: args.initialStageId,
783
- ...actor !== void 0 ? { actor } : {}
784
- }
785
- ],
786
- startedAt: now,
787
- lastChangedAt: now
1027
+ _key: randomKey(),
1028
+ _type: "workflow.history.taskStatusChanged",
1029
+ at: args.at,
1030
+ stageId: args.stageId,
1031
+ taskId: args.taskId,
1032
+ from: args.from,
1033
+ to: args.to,
1034
+ ...args.actor !== void 0 ? { actor: args.actor } : {}
788
1035
  };
789
1036
  }
790
- const GUARD_DOC_TYPE = "temp.system.guard";
791
- class MutationGuardDeniedError extends Error {
792
- denied;
793
- documentId;
794
- action;
795
- constructor(args) {
796
- const ids = args.denied.map((d) => d.guardId).join(", ");
797
- super(`Mutation on "${args.documentId}" (${args.action}) denied by guard(s) [${ids}]`), this.name = "MutationGuardDeniedError", this.denied = args.denied, this.documentId = args.documentId, this.action = args.action;
798
- }
1037
+ function stageTransitionHistory(args) {
1038
+ const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1039
+ at,
1040
+ ...transitionId !== void 0 ? { transitionId } : {},
1041
+ ...actor !== void 0 ? { actor } : {},
1042
+ ...via !== void 0 ? { via } : {},
1043
+ ...reason !== void 0 ? { reason } : {}
1044
+ };
1045
+ return [
1046
+ {
1047
+ _key: randomKey(),
1048
+ _type: "workflow.history.stageExited",
1049
+ stageId: fromStageId,
1050
+ toStageId,
1051
+ ...shared
1052
+ },
1053
+ {
1054
+ _key: randomKey(),
1055
+ _type: "workflow.history.transitionFired",
1056
+ fromStageId,
1057
+ toStageId,
1058
+ ...shared
1059
+ },
1060
+ {
1061
+ _key: randomKey(),
1062
+ _type: "workflow.history.stageEntered",
1063
+ stageId: toStageId,
1064
+ fromStageId,
1065
+ ...shared
1066
+ }
1067
+ ];
799
1068
  }
800
- function lakeGuardId(args) {
801
- return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
1069
+ function instanceStateFields(src) {
1070
+ const fields = {
1071
+ currentStageId: src.currentStageId,
1072
+ state: src.state,
1073
+ stages: src.stages,
1074
+ pendingEffects: src.pendingEffects,
1075
+ effectHistory: src.effectHistory,
1076
+ effectsContext: src.effectsContext,
1077
+ history: src.history
1078
+ };
1079
+ return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
802
1080
  }
803
- function compileGuard(args) {
1081
+ function materializeInstance(base, mutation) {
804
1082
  return {
805
- _id: args.id,
806
- _type: GUARD_DOC_TYPE,
807
- resourceType: args.resourceType,
808
- resourceId: args.resourceId,
809
- owner: args.owner,
810
- sourceInstanceId: args.sourceInstanceId,
811
- sourceDefinitionId: args.sourceDefinitionId,
812
- sourceStageId: args.sourceStageId,
813
- ...args.name !== void 0 ? { name: args.name } : {},
814
- ...args.description !== void 0 ? { description: args.description } : {},
815
- match: args.match,
816
- predicate: args.predicate,
817
- metadata: args.metadata
1083
+ ...base,
1084
+ currentStageId: mutation.currentStageId,
1085
+ state: mutation.state,
1086
+ stages: mutation.stages
818
1087
  };
819
1088
  }
820
- function toGroqJsPredicate(predicate) {
821
- return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
1089
+ async function persist(ctx, mutation) {
1090
+ const set = {
1091
+ ...instanceStateFields(mutation),
1092
+ lastChangedAt: ctx.now
1093
+ }, pendingCreates = mutation.pendingCreates;
1094
+ if (pendingCreates.length === 0)
1095
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1096
+ const tx = ctx.client.transaction();
1097
+ for (const body of pendingCreates) tx.create(body);
1098
+ tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1099
+ const actorForPriming = void 0;
1100
+ for (const body of pendingCreates)
1101
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1102
+ const reloaded = await ctx.client.getDocument(ctx.instance._id);
1103
+ if (!reloaded)
1104
+ throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1105
+ return reloaded;
822
1106
  }
823
- function globMatch(pattern, value) {
824
- if (!pattern.includes("*")) return pattern === value;
825
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
826
- return new RegExp(`^${escaped}$`).test(value);
1107
+ function currentStageEntry(mutation) {
1108
+ const entry = findOpenStageEntry(mutation);
1109
+ if (entry === void 0)
1110
+ throw new Error(
1111
+ `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
1112
+ );
1113
+ return entry;
827
1114
  }
828
- function guardMatches(guard, doc, action) {
829
- const m = guard.match;
830
- if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
831
- return !1;
832
- if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
833
- const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
834
- if (!byRef && !byPattern) return !1;
835
- }
836
- return !0;
1115
+ function currentTasks(mutation) {
1116
+ return currentStageEntry(mutation).tasks;
837
1117
  }
838
- async function evaluateMutationGuard(args) {
839
- const { guard, context } = args;
840
- if (guard.predicate === "") return !1;
841
- const params = { guard, mutation: { action: context.action } };
842
- try {
843
- const tree = groqJs.parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
844
- return await (await groqJs.evaluate(tree, {
845
- before: context.before,
846
- after: context.after,
847
- params,
848
- ...context.identity !== void 0 ? { identity: context.identity } : {}
849
- })).get() === !0;
850
- } catch {
851
- return !1;
852
- }
1118
+ function findTaskInCurrentStage(ctx, taskId) {
1119
+ const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1120
+ if (task === void 0)
1121
+ throw new Error(
1122
+ `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1123
+ );
1124
+ return { stage, task };
853
1125
  }
854
- async function denyingGuards(args) {
855
- const { guards, doc, context } = args, denied = [];
856
- for (const guard of guards)
857
- guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
858
- return denied;
1126
+ function requireMutationTaskEntry(mutation, taskId) {
1127
+ const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1128
+ if (mutEntry === void 0)
1129
+ throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1130
+ return mutEntry;
859
1131
  }
860
- function resourceOf(p) {
861
- return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
1132
+ function findCurrentStageEntry(instance) {
1133
+ return findOpenStageEntry(instance);
862
1134
  }
863
- function resolveIdRefTarget(src, ctx) {
864
- const value = resolveSource(src, ctx);
865
- if (typeof value == "string" && isGdrUri(value))
866
- return { parsed: parseGdr(value) };
867
- if (value && typeof value == "object") {
868
- const v2 = value;
869
- if (typeof v2.id == "string" && isGdrUri(v2.id))
870
- return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
871
- }
872
- return null;
873
- }
874
- function resolveIdRefTargets(idRefs, ctx) {
875
- const targets = [];
876
- for (const src of idRefs ?? []) {
877
- const target = resolveIdRefTarget(src, ctx);
878
- if (target === null) return null;
879
- targets.push(target);
880
- }
881
- return targets.length === 0 ? null : targets;
882
- }
883
- function assertSingleResource(targets) {
884
- const resource = resourceOf(targets[0].parsed);
885
- for (const g of targets) {
886
- const r = resourceOf(g.parsed);
887
- if (r.type !== resource.type || r.id !== resource.id)
888
- throw new Error(
889
- `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
890
- );
891
- }
892
- return resource;
893
- }
894
- function bareIdRefs(targets) {
895
- return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
896
- }
897
- function resolveMatchTypes(targets, authorTypes) {
898
- if (authorTypes !== void 0) return authorTypes;
899
- const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
900
- return inferred.length > 0 ? inferred : void 0;
901
- }
902
- function resolveMetadata(metadata, ctx) {
903
- const out = {};
904
- for (const [k, src] of Object.entries(metadata ?? {}))
905
- out[k] = resolveSource(src, ctx);
906
- return out;
1135
+ function findCurrentTasks(instance) {
1136
+ return findCurrentStageEntry(instance)?.tasks ?? [];
907
1137
  }
908
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
909
- function resolveGuard(guard, index, instance, stageId, now) {
910
- const ctx = { instance, stageId, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
911
- if (targets === null) return null;
912
- const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
913
- return { doc: compileGuard({
914
- id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
915
- resourceType: resource.type,
916
- resourceId: resource.id,
917
- owner: GUARD_OWNER,
918
- sourceInstanceId: instance._id,
919
- sourceDefinitionId: instance.workflowId,
920
- sourceStageId: stageId,
921
- ...guard.name !== void 0 ? { name: guard.name } : {},
922
- ...guard.description !== void 0 ? { description: guard.description } : {},
923
- match: {
924
- ...types !== void 0 ? { types } : {},
925
- idRefs: bareIdRefs(targets),
926
- ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
927
- actions: guard.match.actions
928
- },
929
- predicate: guard.predicate ?? "",
930
- metadata: resolveMetadata(guard.metadata, ctx)
931
- }), routeGdr: targets[0].parsed };
1138
+ async function completeEffect(args) {
1139
+ const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1140
+ ...options?.clock ? { clock: options.clock } : {}
1141
+ });
1142
+ return commitCompleteEffect(
1143
+ ctx,
1144
+ effectKey,
1145
+ status,
1146
+ outputs,
1147
+ detail,
1148
+ error,
1149
+ durationMs,
1150
+ options?.actor
1151
+ );
932
1152
  }
933
- async function upsertGuard(client, doc) {
934
- if (!await client.getDocument(doc._id)) {
935
- await client.create(doc);
936
- return;
937
- }
938
- const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
939
- await client.patch(doc._id).set(body).commit();
1153
+ function buildEffectHistoryEntry(pending, outcome) {
1154
+ const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
1155
+ return {
1156
+ _key: pending._key,
1157
+ effectId: pending.effectId,
1158
+ ...pending.title !== void 0 ? { title: pending.title } : {},
1159
+ ...pending.description !== void 0 ? { description: pending.description } : {},
1160
+ params: pending.params,
1161
+ origin: pending.origin,
1162
+ ...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
1163
+ ranAt,
1164
+ ...durationMs !== void 0 ? { durationMs } : {},
1165
+ status,
1166
+ ...detail !== void 0 ? { detail } : {},
1167
+ ...error !== void 0 ? { error } : {},
1168
+ ...outputs !== void 0 ? { outputs } : {}
1169
+ };
940
1170
  }
941
- function resolvedStageGuards(args) {
942
- const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
943
- for (const [index, guard] of (stage?.guards ?? []).entries()) {
944
- const resolved = resolveGuard(guard, index, args.instance, args.stageId, args.now);
945
- resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
1171
+ function upsertEffectsContext(mutation, outputs) {
1172
+ for (const [id, value] of Object.entries(outputs)) {
1173
+ const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1174
+ existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
946
1175
  }
947
- return out;
948
1176
  }
949
- async function committedStageId(args) {
950
- return (await args.client.getDocument(args.instance._id))?.currentStageId;
1177
+ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1178
+ const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1179
+ if (pending === void 0)
1180
+ throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1181
+ const mutation = startMutation(ctx.instance);
1182
+ mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1183
+ const ranAt = ctx.now;
1184
+ return mutation.effectHistory.push(
1185
+ buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1186
+ ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1187
+ _key: randomKey(),
1188
+ _type: "workflow.history.effectCompleted",
1189
+ at: ranAt,
1190
+ effectKey,
1191
+ effectId: pending.effectId,
1192
+ status,
1193
+ ...outputs !== void 0 ? { outputs } : {},
1194
+ ...detail !== void 0 ? { detail } : {},
1195
+ ...actor !== void 0 ? { actor } : {}
1196
+ }), await persist(ctx, mutation), { effectKey, status };
951
1197
  }
952
- async function deployStageGuards(args) {
953
- if (await committedStageId(args) === args.stageId)
954
- for (const { client, doc } of resolvedStageGuards(args))
955
- await upsertGuard(client, doc);
1198
+ function buildQueuedEffect(effect, origin, params, actor, now) {
1199
+ const key = randomKey(), pending = {
1200
+ _key: key,
1201
+ _type: "workflow.pendingEffect",
1202
+ effectId: effect.id,
1203
+ ...effect.title !== void 0 ? { title: effect.title } : {},
1204
+ ...effect.description !== void 0 ? { description: effect.description } : {},
1205
+ ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1206
+ params,
1207
+ origin,
1208
+ ...actor !== void 0 ? { actor } : {},
1209
+ queuedAt: now
1210
+ }, history = {
1211
+ _key: randomKey(),
1212
+ _type: "workflow.history.effectQueued",
1213
+ at: now,
1214
+ effectKey: key,
1215
+ effectId: effect.id,
1216
+ origin
1217
+ };
1218
+ return { pending, history };
956
1219
  }
957
- async function retractStageGuards(args) {
958
- const live = await committedStageId(args);
959
- if (!(live === void 0 || live === args.stageId))
960
- for (const { client, doc } of resolvedStageGuards(args))
961
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
1220
+ async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1221
+ if (!effects) return;
1222
+ const now = ctx.now, liveInstance = {
1223
+ ...ctx.instance,
1224
+ state: mutation.state,
1225
+ stages: mutation.stages,
1226
+ effectsContext: mutation.effectsContext
1227
+ };
1228
+ for (const effect of effects) {
1229
+ const params = await resolveBindings({
1230
+ bindings: effect.bindings,
1231
+ staticInput: effect.input,
1232
+ instance: liveInstance,
1233
+ now,
1234
+ ...callerParams !== void 0 ? { params: callerParams } : {},
1235
+ ...actor !== void 0 ? { actor } : {}
1236
+ }), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
1237
+ mutation.pendingEffects.push(pending), mutation.history.push(history);
1238
+ }
962
1239
  }
963
1240
  class ActionParamsInvalidError extends Error {
964
1241
  action;
@@ -1034,6 +1311,26 @@ async function deployOrRollback(args) {
1034
1311
  throw guardError;
1035
1312
  }
1036
1313
  }
1314
+ const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
1315
+ function validateTags(tags) {
1316
+ if (tags.length === 0)
1317
+ throw new Error("tags: must be a non-empty array");
1318
+ for (const tag of tags)
1319
+ if (!TAG_RE.test(tag))
1320
+ throw new Error(
1321
+ `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
1322
+ );
1323
+ }
1324
+ function canonicalTag(tags) {
1325
+ return tags[0];
1326
+ }
1327
+ function tagScopeFilter(param = "engineTags") {
1328
+ return `count(tags[@ in $${param}]) > 0`;
1329
+ }
1330
+ function definitionLookupGroq(explicit) {
1331
+ const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && workflowId == $workflowId && ${tagScopeFilter()}`;
1332
+ return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
1333
+ }
1037
1334
  async function discoverItems(args) {
1038
1335
  const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
1039
1336
  return client.fetch(groq, paramsForLake(params), fetchOptions);
@@ -1157,10 +1454,11 @@ function coerceSpawnGdr(raw, workflowResource) {
1157
1454
  return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1158
1455
  }
1159
1456
  async function resolveLogicalDefinition(client, ref, tags) {
1160
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
1161
- wantsExplicit && (params.version = ref.version);
1162
- const groq = wantsExplicit ? '*[_type == "workflow.definition" && workflowId == $workflowId && version == $version && count(tags[@ in $tags]) > 0][0]' : '*[_type == "workflow.definition" && workflowId == $workflowId && count(tags[@ in $tags]) > 0] | order(version desc)[0]';
1163
- return await client.fetch(groq, params) ?? null;
1457
+ const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
1458
+ return wantsExplicit && (params.version = ref.version), await client.fetch(
1459
+ definitionLookupGroq(wantsExplicit),
1460
+ params
1461
+ ) ?? null;
1164
1462
  }
1165
1463
  async function prepareChildInstance(args) {
1166
1464
  const { client, parent, task, spawn, initialState, effectsContext, actor, now } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
@@ -1170,11 +1468,11 @@ async function prepareChildInstance(args) {
1170
1468
  `Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
1171
1469
  );
1172
1470
  }
1173
- const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: "workflow.instance" }, ancestors = [
1471
+ const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
1174
1472
  ...parent.ancestors,
1175
1473
  {
1176
1474
  id: selfGdr(parent),
1177
- type: "workflow.instance"
1475
+ type: WORKFLOW_INSTANCE_TYPE
1178
1476
  }
1179
1477
  ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1180
1478
  slotDefs: definition.state,
@@ -1495,9 +1793,10 @@ async function pickTransition(ctx, stage, action) {
1495
1793
  if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
1496
1794
  }
1497
1795
  async function evaluateAutoTransitions(args) {
1498
- const { client, instanceId, options, clientForGdr, clock } = args, ctx = await loadContext(client, instanceId, {
1796
+ const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
1499
1797
  ...clientForGdr ? { clientForGdr } : {},
1500
- ...clock ? { clock } : {}
1798
+ ...clock ? { clock } : {},
1799
+ ...overlay ? { overlay } : {}
1501
1800
  });
1502
1801
  return commitTransition(ctx, void 0, options?.actor);
1503
1802
  }
@@ -1509,8 +1808,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1509
1808
  const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
1510
1809
  client,
1511
1810
  clientForGdr: clientForGdr ?? (() => client),
1512
- instance,
1513
- definition
1811
+ instance
1514
1812
  }), tasks = [];
1515
1813
  for (const task of stage.tasks ?? []) {
1516
1814
  const inScope = await evaluateFilter({
@@ -1603,10 +1901,11 @@ async function gatherAutoResolveCandidates(ctx, tasks) {
1603
1901
  }
1604
1902
  return candidates;
1605
1903
  }
1606
- async function resolveCompleteWhen(client, instanceId, clientForGdr, clock) {
1904
+ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
1607
1905
  const ctx = await loadContext(client, instanceId, {
1608
1906
  ...clientForGdr ? { clientForGdr } : {},
1609
- ...clock ? { clock } : {}
1907
+ ...clock ? { clock } : {},
1908
+ ...overlay ? { overlay } : {}
1610
1909
  }), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1611
1910
  (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
1612
1911
  );
@@ -1635,15 +1934,16 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock) {
1635
1934
  }
1636
1935
  return count > 0 && await persist(ctx, mutation), count;
1637
1936
  }
1638
- async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock) {
1937
+ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock, overlay) {
1639
1938
  let count = 0;
1640
1939
  for (; ; ) {
1641
- if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock), !(await evaluateAutoTransitions({
1940
+ if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay), !(await evaluateAutoTransitions({
1642
1941
  client,
1643
1942
  instanceId,
1644
1943
  ...actor !== void 0 ? { options: { actor } } : {},
1645
1944
  ...clientForGdr ? { clientForGdr } : {},
1646
- ...clock ? { clock } : {}
1945
+ ...clock ? { clock } : {},
1946
+ ...overlay ? { overlay } : {}
1647
1947
  })).fired) return count;
1648
1948
  if (count++, count >= CASCADE_LIMIT)
1649
1949
  throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
@@ -1653,7 +1953,7 @@ async function findSatisfiedParentSpawn(client, child) {
1653
1953
  const parentRef = child.ancestors.at(-1);
1654
1954
  if (parentRef === void 0) return;
1655
1955
  const parent = await client.getDocument(gdrToBareId(parentRef.id));
1656
- if (!parent || parent._type !== "workflow.instance" || typeof parent.definitionSnapshot != "string") return;
1956
+ if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
1657
1957
  const definition = JSON.parse(parent.definitionSnapshot), stage = definition.stages.find((s) => s.id === parent.currentStageId);
1658
1958
  if (stage === void 0) return;
1659
1959
  const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.spawns === void 0 ? !1 : parentCurrentTasks.find((e) => e.id === t.id)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
@@ -1687,361 +1987,519 @@ async function propagateToAncestors(client, instanceId, actor, clientForGdr, clo
1687
1987
  })
1688
1988
  ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock);
1689
1989
  }
1690
- function startMutation(instance) {
1691
- return {
1692
- currentStageId: instance.currentStageId,
1693
- // Deep-ish copy. Per-scope state slot arrays need their own copies
1694
- // so ops can mutate without leaking into the source.
1695
- state: (instance.state ?? []).map((s) => ({ ...s })),
1696
- stages: instance.stages.map((s) => ({
1697
- ...s,
1698
- state: (s.state ?? []).map((slot) => ({ ...slot })),
1699
- tasks: s.tasks.map((t) => ({
1700
- ...t,
1701
- ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1702
- }))
1703
- })),
1704
- pendingEffects: [...instance.pendingEffects],
1705
- effectHistory: [...instance.effectHistory],
1706
- effectsContext: [...instance.effectsContext],
1707
- history: [...instance.history],
1708
- lastChangedAt: instance.lastChangedAt,
1709
- ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1710
- pendingCreates: []
1711
- };
1712
- }
1713
- function taskStatusChangedEntry(args) {
1714
- return {
1715
- _key: randomKey(),
1716
- _type: "workflow.history.taskStatusChanged",
1717
- at: args.at,
1718
- stageId: args.stageId,
1719
- taskId: args.taskId,
1720
- from: args.from,
1721
- to: args.to,
1722
- ...args.actor !== void 0 ? { actor: args.actor } : {}
1723
- };
1990
+ const parsedFilters = /* @__PURE__ */ new Map();
1991
+ async function matchesFilter(args) {
1992
+ const { document, filter, userId } = args;
1993
+ parsedFilters.has(filter) || parsedFilters.set(filter, groqJs.parse(`*[${filter}]`));
1994
+ const ast = parsedFilters.get(filter), data = await (await groqJs.evaluate(ast, {
1995
+ dataset: [document],
1996
+ ...userId !== void 0 ? { identity: userId } : {}
1997
+ })).get();
1998
+ return Array.isArray(data) && data.length === 1;
1724
1999
  }
1725
- function stageTransitionHistory(args) {
1726
- const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1727
- at,
1728
- ...transitionId !== void 0 ? { transitionId } : {},
1729
- ...actor !== void 0 ? { actor } : {},
1730
- ...via !== void 0 ? { via } : {},
1731
- ...reason !== void 0 ? { reason } : {}
1732
- };
1733
- return [
1734
- {
1735
- _key: randomKey(),
1736
- _type: "workflow.history.stageExited",
1737
- stageId: fromStageId,
1738
- toStageId,
1739
- ...shared
1740
- },
1741
- {
1742
- _key: randomKey(),
1743
- _type: "workflow.history.transitionFired",
1744
- fromStageId,
1745
- toStageId,
1746
- ...shared
1747
- },
1748
- {
1749
- _key: randomKey(),
1750
- _type: "workflow.history.stageEntered",
1751
- stageId: toStageId,
1752
- fromStageId,
1753
- ...shared
1754
- }
1755
- ];
1756
- }
1757
- function instanceStateFields(src) {
1758
- const fields = {
1759
- currentStageId: src.currentStageId,
1760
- state: src.state,
1761
- stages: src.stages,
1762
- pendingEffects: src.pendingEffects,
1763
- effectHistory: src.effectHistory,
1764
- effectsContext: src.effectsContext,
1765
- history: src.history
1766
- };
1767
- return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
1768
- }
1769
- function materializeInstance(base, mutation) {
1770
- return {
1771
- ...base,
1772
- currentStageId: mutation.currentStageId,
1773
- state: mutation.state,
1774
- stages: mutation.stages
1775
- };
1776
- }
1777
- async function persist(ctx, mutation) {
1778
- const set = {
1779
- ...instanceStateFields(mutation),
1780
- lastChangedAt: ctx.now
1781
- }, pendingCreates = mutation.pendingCreates;
1782
- if (pendingCreates.length === 0)
1783
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1784
- const tx = ctx.client.transaction();
1785
- for (const body of pendingCreates) tx.create(body);
1786
- tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1787
- const actorForPriming = void 0;
1788
- for (const body of pendingCreates)
1789
- await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1790
- const reloaded = await ctx.client.getDocument(ctx.instance._id);
1791
- if (!reloaded)
1792
- throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1793
- return reloaded;
2000
+ async function grantsPermissionOn(args) {
2001
+ const { document, grants, permission, userId } = args;
2002
+ if (!document || grants.length === 0) return !1;
2003
+ for (const grant of grants)
2004
+ if (grant.permissions.includes(permission) && await matchesFilter({
2005
+ document,
2006
+ filter: grant.filter,
2007
+ ...userId !== void 0 ? { userId } : {}
2008
+ }))
2009
+ return !0;
2010
+ return !1;
1794
2011
  }
1795
- function findOpenStageEntry(host) {
1796
- return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
2012
+ async function fetchGrants(args) {
2013
+ const { client, resourcePath, signal } = args;
2014
+ return client.request({ url: resourcePath, ...signal ? { signal } : {} });
1797
2015
  }
1798
- function currentStageEntry(mutation) {
1799
- const entry = findOpenStageEntry(mutation);
1800
- if (entry === void 0)
2016
+ const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
2017
+ async function resolveAccess(client, args = {}) {
2018
+ const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2019
+ if (overrideActor !== void 0 && overrideGrants !== void 0)
2020
+ return { actor: overrideActor, grants: overrideGrants };
2021
+ const requestFn = client.request === void 0 ? void 0 : (opts) => client.request(opts);
2022
+ if (requestFn === void 0) {
2023
+ if (overrideActor === void 0)
2024
+ throw new Error(
2025
+ "workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
2026
+ );
2027
+ return { actor: overrideActor };
2028
+ }
2029
+ const actorPromise = overrideActor !== void 0 ? Promise.resolve(overrideActor) : cachedActor(client, requestFn), grantsPromise = resolveGrants(overrideGrants, args.grantsFromPath, client, requestFn), [actor, grants] = await Promise.all([actorPromise, grantsPromise]);
2030
+ if (actor === void 0)
1801
2031
  throw new Error(
1802
- `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
2032
+ "workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity \u2014 check the token, or pass `access.actor` explicitly."
1803
2033
  );
1804
- return entry;
2034
+ return { actor, ...grants !== void 0 ? { grants } : {} };
1805
2035
  }
1806
- function currentTasks(mutation) {
1807
- return currentStageEntry(mutation).tasks;
2036
+ function cachedActor(client, requestFn) {
2037
+ const cached = actorCache.get(client);
2038
+ if (cached !== void 0) return cached;
2039
+ const pending = fetchActor(requestFn).catch((err) => {
2040
+ throw actorCache.get(client) === pending && actorCache.delete(client), err;
2041
+ });
2042
+ return actorCache.set(client, pending), pending;
1808
2043
  }
1809
- function findTaskInCurrentStage(ctx, taskId) {
1810
- const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1811
- if (task === void 0)
1812
- throw new Error(
1813
- `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1814
- );
1815
- return { stage, task };
2044
+ function resolveGrants(overrideGrants, grantsFromPath, client, requestFn) {
2045
+ return overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : grantsFromPath !== void 0 ? cachedGrants(client, requestFn, grantsFromPath) : Promise.resolve(void 0);
1816
2046
  }
1817
- function requireMutationTaskEntry(mutation, taskId) {
1818
- const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1819
- if (mutEntry === void 0)
1820
- throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1821
- return mutEntry;
2047
+ function cachedGrants(client, requestFn, resourcePath) {
2048
+ let byPath = grantsCache.get(client);
2049
+ byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
2050
+ let cached = byPath.get(resourcePath);
2051
+ return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
1822
2052
  }
1823
- function findCurrentStageEntry(instance) {
1824
- return findOpenStageEntry(instance);
2053
+ async function fetchActor(requestFn) {
2054
+ let user;
2055
+ try {
2056
+ user = await requestFn({
2057
+ uri: "/users/me",
2058
+ tag: "workflow.access.resolve-actor"
2059
+ });
2060
+ } catch (err) {
2061
+ throw new Error(
2062
+ 'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
2063
+ { cause: err }
2064
+ );
2065
+ }
2066
+ if (!user || typeof user.id != "string" || user.id.length === 0) return;
2067
+ const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
2068
+ return {
2069
+ kind: "user",
2070
+ id: user.id,
2071
+ ...roleNames.length > 0 ? { roles: roleNames } : {}
2072
+ };
1825
2073
  }
1826
- function findCurrentTasks(instance) {
1827
- return findCurrentStageEntry(instance)?.tasks ?? [];
2074
+ async function fetchGrantsCached(requestFn, resourcePath) {
2075
+ try {
2076
+ return await fetchGrants({ client: { request: requestFn }, resourcePath });
2077
+ } catch (err) {
2078
+ console.warn(
2079
+ `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2080
+ );
2081
+ return;
2082
+ }
1828
2083
  }
1829
- async function completeEffect(args) {
1830
- const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1831
- ...options?.clock ? { clock: options.clock } : {}
2084
+ const WILDCARD_ROLE = "*";
2085
+ async function evaluateInstance(args) {
2086
+ const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2087
+ validateTags(tags);
2088
+ const { actor, grants } = await resolveAccess(client, {
2089
+ ...args.access !== void 0 ? { override: args.access } : {},
2090
+ ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2091
+ }), instance = await client.getDocument(instanceId);
2092
+ if (!instance)
2093
+ throw new Error(`Workflow instance "${instanceId}" not found`);
2094
+ if (!tags.some((t) => instance.tags?.includes(t)))
2095
+ throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2096
+ const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance });
2097
+ return evaluateFromSnapshot({
2098
+ instance,
2099
+ definition,
2100
+ actor,
2101
+ snapshot,
2102
+ now,
2103
+ ...grants !== void 0 ? { grants } : {}
1832
2104
  });
1833
- return commitCompleteEffect(
1834
- ctx,
1835
- effectKey,
1836
- status,
1837
- outputs,
1838
- detail,
1839
- error,
1840
- durationMs,
1841
- options?.actor
1842
- );
1843
2105
  }
1844
- function buildEffectHistoryEntry(pending, outcome) {
1845
- const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome;
2106
+ async function evaluateFromSnapshot(args) {
2107
+ const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2108
+ if (stage === void 0)
2109
+ throw new Error(
2110
+ `Instance "${instance._id}" currentStageId "${instance.currentStageId}" not in definition`
2111
+ );
2112
+ const currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], taskEvaluations = [];
2113
+ for (const task of stage.tasks ?? [])
2114
+ taskEvaluations.push(
2115
+ await evaluateTask({
2116
+ task,
2117
+ statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2118
+ instance,
2119
+ definition,
2120
+ actor,
2121
+ grants,
2122
+ snapshot,
2123
+ now
2124
+ })
2125
+ );
2126
+ const transitionEvaluations = [];
2127
+ for (const transition of stage.transitions ?? [])
2128
+ transitionEvaluations.push({
2129
+ transition,
2130
+ filterSatisfied: await evaluateFilter({
2131
+ filter: transition.filter,
2132
+ definition,
2133
+ snapshot,
2134
+ params: buildParams(instance, now)
2135
+ })
2136
+ });
2137
+ const currentStage = {
2138
+ stage,
2139
+ tasks: taskEvaluations,
2140
+ transitions: transitionEvaluations
2141
+ }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
1846
2142
  return {
1847
- _key: pending._key,
1848
- effectId: pending.effectId,
1849
- ...pending.title !== void 0 ? { title: pending.title } : {},
1850
- ...pending.description !== void 0 ? { description: pending.description } : {},
1851
- params: pending.params,
1852
- origin: pending.origin,
1853
- ...actor !== void 0 ? { actor } : pending.actor !== void 0 ? { actor: pending.actor } : {},
1854
- ranAt,
1855
- ...durationMs !== void 0 ? { durationMs } : {},
1856
- status,
1857
- ...detail !== void 0 ? { detail } : {},
1858
- ...error !== void 0 ? { error } : {},
1859
- ...outputs !== void 0 ? { outputs } : {}
2143
+ instance,
2144
+ definition,
2145
+ actor,
2146
+ currentStage,
2147
+ pendingOnYou,
2148
+ canInteract
1860
2149
  };
1861
2150
  }
1862
- function upsertEffectsContext(mutation, outputs) {
1863
- for (const [id, value] of Object.entries(outputs)) {
1864
- const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1865
- existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
1866
- }
1867
- }
1868
- async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1869
- const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1870
- if (pending === void 0)
1871
- throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1872
- const mutation = startMutation(ctx.instance);
1873
- mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1874
- const ranAt = ctx.now;
1875
- return mutation.effectHistory.push(
1876
- buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1877
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1878
- _key: randomKey(),
1879
- _type: "workflow.history.effectCompleted",
1880
- at: ranAt,
1881
- effectKey,
1882
- effectId: pending.effectId,
2151
+ async function evaluateTask(args) {
2152
+ const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
2153
+ for (const action of task.actions ?? [])
2154
+ actions.push(
2155
+ await evaluateAction({
2156
+ action,
2157
+ task,
2158
+ ...statusEntry !== void 0 ? { statusEntry } : {},
2159
+ status,
2160
+ instance,
2161
+ definition,
2162
+ actor,
2163
+ ...grants !== void 0 ? { grants } : {},
2164
+ snapshot,
2165
+ now
2166
+ })
2167
+ );
2168
+ return {
2169
+ task,
1883
2170
  status,
1884
- ...outputs !== void 0 ? { outputs } : {},
1885
- ...detail !== void 0 ? { detail } : {},
1886
- ...actor !== void 0 ? { actor } : {}
1887
- }), await persist(ctx, mutation), { effectKey, status };
1888
- }
1889
- function buildQueuedEffect(effect, origin, params, actor, now) {
1890
- const key = randomKey(), pending = {
1891
- _key: key,
1892
- _type: "workflow.pendingEffect",
1893
- effectId: effect.id,
1894
- ...effect.title !== void 0 ? { title: effect.title } : {},
1895
- ...effect.description !== void 0 ? { description: effect.description } : {},
1896
- ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1897
- params,
1898
- origin,
1899
- ...actor !== void 0 ? { actor } : {},
1900
- queuedAt: now
1901
- }, history = {
1902
- _key: randomKey(),
1903
- _type: "workflow.history.effectQueued",
1904
- at: now,
1905
- effectKey: key,
1906
- effectId: effect.id,
1907
- origin
2171
+ // "pending on actor" treats both `pending` and `active` as waiting on
2172
+ // the assignee pending tasks just haven't been auto-invoked yet, but
2173
+ // they're still inbox items.
2174
+ pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2175
+ actions
1908
2176
  };
1909
- return { pending, history };
1910
2177
  }
1911
- async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1912
- if (!effects) return;
1913
- const now = ctx.now, liveInstance = {
1914
- ...ctx.instance,
1915
- state: mutation.state,
1916
- stages: mutation.stages,
1917
- effectsContext: mutation.effectsContext
1918
- };
1919
- for (const effect of effects) {
1920
- const params = await resolveBindings({
1921
- bindings: effect.bindings,
1922
- staticInput: effect.input,
1923
- instance: liveInstance,
1924
- now,
1925
- ...callerParams !== void 0 ? { params: callerParams } : {},
1926
- ...actor !== void 0 ? { actor } : {}
1927
- }), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
1928
- mutation.pendingEffects.push(pending), mutation.history.push(history);
1929
- }
2178
+ async function evaluateAction(args) {
2179
+ const { action, task, status, instance, definition, actor, grants, snapshot, now } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2180
+ if (syncReason !== void 0) return disabled(action, syncReason);
2181
+ const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot, now);
2182
+ return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
1930
2183
  }
1931
- const STATE_OP_TYPES = [
1932
- "workflow.op.state.set",
1933
- "workflow.op.state.append",
1934
- "workflow.op.state.updateWhere",
1935
- "workflow.op.state.removeWhere"
1936
- ];
1937
- function isStateOp(summary) {
1938
- return STATE_OP_TYPES.includes(summary.opType);
2184
+ function lifecycleReason(instance, definition, status) {
2185
+ if (instance.completedAt !== void 0)
2186
+ return { kind: "instance-completed", completedAt: instance.completedAt };
2187
+ if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2188
+ return { kind: "stage-terminal", stageId: instance.currentStageId };
2189
+ if (status === "done" || status === "skipped" || status === "failed")
2190
+ return { kind: "task-not-active", status };
1939
2191
  }
1940
- function validateActionParams(action, taskId, callerParams) {
1941
- const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
1942
- for (const decl of declared) {
1943
- const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
1944
- if (decl.required === !0 && !present) {
1945
- issues.push({ paramId: decl.id, reason: "required but missing" });
1946
- continue;
1947
- }
1948
- present && (checkParamType(value, decl) || issues.push({
1949
- paramId: decl.id,
1950
- reason: `expected paramType=${decl.paramType}, got ${typeof value}`
1951
- }));
1952
- }
1953
- if (issues.length > 0)
1954
- throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
1955
- return params;
2192
+ function actorReason(action, task, actor) {
2193
+ const actorRoles = actor.roles ?? [];
2194
+ if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2195
+ return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2196
+ if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2197
+ return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
1956
2198
  }
1957
- function hasStringField(value, field) {
1958
- return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
2199
+ async function permissionReason(action, instance, actor, grants) {
2200
+ if (grants === void 0) return;
2201
+ const permission = action.requiredPermission ?? "update";
2202
+ if (!await grantsPermissionOn({
2203
+ document: instance,
2204
+ grants,
2205
+ permission,
2206
+ userId: actor.id
2207
+ }))
2208
+ return { kind: "permission-denied", permission, matchingGrants: grants.length };
1959
2209
  }
1960
- function checkParamType(value, decl) {
1961
- switch (decl.paramType) {
1962
- case "string":
1963
- case "url":
1964
- case "dateTime":
1965
- return typeof value == "string";
1966
- case "number":
1967
- return typeof value == "number" && Number.isFinite(value);
1968
- case "boolean":
1969
- return typeof value == "boolean";
1970
- case "actor":
1971
- return hasStringField(value, "kind");
1972
- case "doc.ref":
1973
- return hasStringField(value, "id");
1974
- case "doc.refs":
1975
- return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
1976
- case "json":
2210
+ async function filterReason(action, instance, definition, snapshot, now) {
2211
+ if (!(action.filter === void 0 || await evaluateFilter({
2212
+ filter: action.filter,
2213
+ definition,
2214
+ snapshot,
2215
+ params: buildParams(instance, now)
2216
+ })))
2217
+ return { kind: "filter-failed", filter: action.filter };
2218
+ }
2219
+ function disabled(action, reason) {
2220
+ return { action, allowed: !1, disabledReason: reason };
2221
+ }
2222
+ function actorIsAssignee(actor, assignees) {
2223
+ if (assignees.length === 0) return !1;
2224
+ const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2225
+ for (const assignee of assignees)
2226
+ if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
1977
2227
  return !0;
1978
- default:
1979
- return !1;
1980
- }
2228
+ return !1;
1981
2229
  }
1982
- async function runActionOps(args) {
1983
- const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
1984
- if (ops === void 0 || ops.length === 0) return [];
1985
- const summaries = [];
1986
- for (const op of ops) {
1987
- const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
1988
- summaries.push(summary), mutation.history.push({
1989
- _key: randomKey(),
1990
- _type: "workflow.history.opApplied",
1991
- at: now,
1992
- stageId,
1993
- taskId,
1994
- actionId: actionName,
1995
- opType: summary.opType,
1996
- ...summary.target !== void 0 ? { target: summary.target } : {},
1997
- ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
1998
- ...actor !== void 0 ? { actor } : {}
1999
- });
2230
+ function parseDefinitionSnapshot(instance) {
2231
+ try {
2232
+ return JSON.parse(instance.definitionSnapshot);
2233
+ } catch (err) {
2234
+ throw new Error(
2235
+ `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2236
+ { cause: err }
2237
+ );
2000
2238
  }
2001
- return summaries;
2002
2239
  }
2003
- function applyOp(op, ctx) {
2004
- switch (op.type) {
2005
- case "workflow.op.state.set":
2006
- return applyStateSet(op, ctx);
2007
- case "workflow.op.state.append":
2008
- return applyStateAppend(op, ctx);
2009
- case "workflow.op.state.updateWhere":
2010
- return applyStateUpdateWhere(op, ctx);
2011
- case "workflow.op.state.removeWhere":
2012
- return applyStateRemoveWhere(op, ctx);
2013
- case "workflow.op.status.set":
2014
- return applyStatusSet(op, ctx);
2015
- }
2240
+ const resourceKey = (r) => `${r.type}.${r.id}`;
2241
+ function guardsForResource(client) {
2242
+ return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2016
2243
  }
2017
- function applyStateSet(op, ctx) {
2018
- const value = resolveOpSource(op.value, ctx), slot = locateSlot(
2019
- ctx,
2020
- op.target,
2021
- /*createIfMissing*/
2022
- !0
2244
+ async function guardsForInstance(args) {
2245
+ const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
2246
+ [resourceKey(instance.workflowResource), client]
2247
+ ]), stateUris = [
2248
+ ...collectSlotDocUris(instance.state),
2249
+ ...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
2250
+ ];
2251
+ for (const uri of stateUris) {
2252
+ const parsed = tryParseGdr(uri);
2253
+ if (parsed === void 0) continue;
2254
+ const key = resourceKey(resourceFromParsed(parsed));
2255
+ clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
2256
+ }
2257
+ const perResource = await Promise.all(
2258
+ [...clientsByResource.values()].map(
2259
+ (c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
2260
+ t: GUARD_DOC_TYPE,
2261
+ id: instance._id
2262
+ })
2263
+ )
2023
2264
  );
2024
- return slot !== void 0 && (validateSlotValue({ slotType: slot._type, slotId: slot.id, value }), slot.value = value), { opType: op.type, target: op.target, resolved: { value } };
2265
+ return dedupById(perResource.flat());
2025
2266
  }
2026
- function applyStateAppend(op, ctx) {
2027
- const item = resolveOpSource(op.item, ctx), slot = locateSlot(
2028
- ctx,
2029
- op.target,
2030
- /*createIfMissing*/
2031
- !0
2032
- );
2033
- if (slot !== void 0) {
2034
- validateSlotAppendItem({ slotType: slot._type, slotId: slot.id, item });
2035
- const current = Array.isArray(slot.value) ? slot.value : [];
2036
- slot.value = [...current, withRowKey(item)];
2267
+ function tryParseGdr(uri) {
2268
+ try {
2269
+ return parseGdr(uri);
2270
+ } catch {
2271
+ return;
2037
2272
  }
2038
- return { opType: op.type, target: op.target, resolved: { item } };
2039
2273
  }
2040
- function withRowKey(item) {
2041
- return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
2274
+ function dedupById(guards) {
2275
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2042
2276
  }
2043
- function applyStateUpdateWhere(op, ctx) {
2044
- const slot = locateSlot(
2277
+ function definitionDocId(_workflowResource, prefix, workflowId, version) {
2278
+ return `${prefix}.${workflowId}.v${version}`;
2279
+ }
2280
+ async function sortByDependencies(client, definitions, tags, workflowResource) {
2281
+ const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2282
+ for (const def of definitions) {
2283
+ const list = byWorkflowId.get(def.workflowId) ?? [];
2284
+ list.push(def), byWorkflowId.set(def.workflowId, list);
2285
+ }
2286
+ const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2287
+ return await assertCrossBatchRefsDeployed(client, definitions, {
2288
+ tags,
2289
+ workflowResource,
2290
+ prefix,
2291
+ findInBatch
2292
+ }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2293
+ }
2294
+ function findRefInBatch(byWorkflowId, ref) {
2295
+ const candidates = byWorkflowId.get(ref.workflowId);
2296
+ if (!(candidates === void 0 || candidates.length === 0))
2297
+ return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2298
+ (best, c) => best === void 0 || c.version > best.version ? c : best,
2299
+ void 0
2300
+ );
2301
+ }
2302
+ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2303
+ const missing = [];
2304
+ for (const def of definitions) {
2305
+ const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2306
+ for (const ref of refsOf(def)) {
2307
+ if (ctx.findInBatch(ref) !== void 0) continue;
2308
+ const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2309
+ label !== void 0 && missing.push({ from: fromId, ref: label });
2310
+ }
2311
+ }
2312
+ if (missing.length === 0) return;
2313
+ const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
2314
+ throw new Error(
2315
+ `workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
2316
+ ` + lines.join(`
2317
+ `)
2318
+ );
2319
+ }
2320
+ async function resolveDeployedRefLabel(client, ref, tags) {
2321
+ const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
2322
+ if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2323
+ definitionLookupGroq(wantsExplicit),
2324
+ params
2325
+ ))
2326
+ return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2327
+ }
2328
+ function topoSortDefinitions(definitions, ctx) {
2329
+ const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2330
+ const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2331
+ if (!visited.has(id)) {
2332
+ if (visiting.has(id))
2333
+ throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
2334
+ visiting.add(id);
2335
+ for (const ref of refsOf(def)) {
2336
+ const child = ctx.findInBatch(ref);
2337
+ child !== void 0 && visit(child);
2338
+ }
2339
+ visiting.delete(id), visited.add(id), ordered.push(def);
2340
+ }
2341
+ };
2342
+ for (const def of definitions) visit(def);
2343
+ return ordered;
2344
+ }
2345
+ function refsOf(def) {
2346
+ const out = [];
2347
+ for (const stage of def.stages)
2348
+ for (const task of stage.tasks ?? []) {
2349
+ const ref = task.spawns?.definitionRef;
2350
+ ref !== void 0 && out.push({
2351
+ workflowId: ref.workflowId,
2352
+ ...ref.version !== void 0 ? { version: ref.version } : {}
2353
+ });
2354
+ }
2355
+ return out;
2356
+ }
2357
+ function isDefinitionUnchanged(existing, expected) {
2358
+ return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
2359
+ }
2360
+ function stripSystemFields(doc) {
2361
+ const out = {};
2362
+ for (const [k, v2] of Object.entries(doc))
2363
+ k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v2);
2364
+ return out;
2365
+ }
2366
+ function stableStringify(value) {
2367
+ return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2368
+ ([a], [b]) => a.localeCompare(b)
2369
+ ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2370
+ }
2371
+ async function loadDefinition(client, workflowId, version, tags) {
2372
+ if (version !== void 0) {
2373
+ const doc = await client.fetch(
2374
+ definitionLookupGroq(!0),
2375
+ { workflowId, version, engineTags: tags }
2376
+ );
2377
+ if (!doc)
2378
+ throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2379
+ return doc;
2380
+ }
2381
+ const latest = await client.fetch(definitionLookupGroq(!1), {
2382
+ workflowId,
2383
+ engineTags: tags
2384
+ });
2385
+ if (!latest)
2386
+ throw new Error(`No deployed definition for workflow ${workflowId}`);
2387
+ return latest;
2388
+ }
2389
+ const STATE_OP_TYPES = [
2390
+ "workflow.op.state.set",
2391
+ "workflow.op.state.append",
2392
+ "workflow.op.state.updateWhere",
2393
+ "workflow.op.state.removeWhere"
2394
+ ];
2395
+ function isStateOp(summary) {
2396
+ return STATE_OP_TYPES.includes(summary.opType);
2397
+ }
2398
+ function validateActionParams(action, taskId, callerParams) {
2399
+ const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
2400
+ for (const decl of declared) {
2401
+ const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
2402
+ if (decl.required === !0 && !present) {
2403
+ issues.push({ paramId: decl.id, reason: "required but missing" });
2404
+ continue;
2405
+ }
2406
+ present && (checkParamType(value, decl) || issues.push({
2407
+ paramId: decl.id,
2408
+ reason: `expected paramType=${decl.paramType}, got ${typeof value}`
2409
+ }));
2410
+ }
2411
+ if (issues.length > 0)
2412
+ throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
2413
+ return params;
2414
+ }
2415
+ function hasStringField(value, field) {
2416
+ return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
2417
+ }
2418
+ function checkParamType(value, decl) {
2419
+ switch (decl.paramType) {
2420
+ case "string":
2421
+ case "url":
2422
+ case "dateTime":
2423
+ return typeof value == "string";
2424
+ case "number":
2425
+ return typeof value == "number" && Number.isFinite(value);
2426
+ case "boolean":
2427
+ return typeof value == "boolean";
2428
+ case "actor":
2429
+ return hasStringField(value, "kind");
2430
+ case "doc.ref":
2431
+ return hasStringField(value, "id");
2432
+ case "doc.refs":
2433
+ return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
2434
+ case "json":
2435
+ return !0;
2436
+ default:
2437
+ return !1;
2438
+ }
2439
+ }
2440
+ async function runActionOps(args) {
2441
+ const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
2442
+ if (ops === void 0 || ops.length === 0) return [];
2443
+ const summaries = [];
2444
+ for (const op of ops) {
2445
+ const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
2446
+ summaries.push(summary), mutation.history.push({
2447
+ _key: randomKey(),
2448
+ _type: "workflow.history.opApplied",
2449
+ at: now,
2450
+ stageId,
2451
+ taskId,
2452
+ actionId: actionName,
2453
+ opType: summary.opType,
2454
+ ...summary.target !== void 0 ? { target: summary.target } : {},
2455
+ ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
2456
+ ...actor !== void 0 ? { actor } : {}
2457
+ });
2458
+ }
2459
+ return summaries;
2460
+ }
2461
+ function applyOp(op, ctx) {
2462
+ switch (op.type) {
2463
+ case "workflow.op.state.set":
2464
+ return applyStateSet(op, ctx);
2465
+ case "workflow.op.state.append":
2466
+ return applyStateAppend(op, ctx);
2467
+ case "workflow.op.state.updateWhere":
2468
+ return applyStateUpdateWhere(op, ctx);
2469
+ case "workflow.op.state.removeWhere":
2470
+ return applyStateRemoveWhere(op, ctx);
2471
+ case "workflow.op.status.set":
2472
+ return applyStatusSet(op, ctx);
2473
+ }
2474
+ }
2475
+ function applyStateSet(op, ctx) {
2476
+ const value = resolveOpSource(op.value, ctx), slot = locateSlot(
2477
+ ctx,
2478
+ op.target,
2479
+ /*createIfMissing*/
2480
+ !0
2481
+ );
2482
+ return slot !== void 0 && (validateSlotValue({ slotType: slot._type, slotId: slot.id, value }), slot.value = value), { opType: op.type, target: op.target, resolved: { value } };
2483
+ }
2484
+ function applyStateAppend(op, ctx) {
2485
+ const item = resolveOpSource(op.item, ctx), slot = locateSlot(
2486
+ ctx,
2487
+ op.target,
2488
+ /*createIfMissing*/
2489
+ !0
2490
+ );
2491
+ if (slot !== void 0) {
2492
+ validateSlotAppendItem({ slotType: slot._type, slotId: slot.id, item });
2493
+ const current = Array.isArray(slot.value) ? slot.value : [];
2494
+ slot.value = [...current, withRowKey(item)];
2495
+ }
2496
+ return { opType: op.type, target: op.target, resolved: { item } };
2497
+ }
2498
+ function withRowKey(item) {
2499
+ return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
2500
+ }
2501
+ function applyStateUpdateWhere(op, ctx) {
2502
+ const slot = locateSlot(
2045
2503
  ctx,
2046
2504
  op.target,
2047
2505
  /*createIfMissing*/
@@ -2149,382 +2607,89 @@ async function fireAction(args) {
2149
2607
  if (!isRevisionConflict(error)) throw error;
2150
2608
  }
2151
2609
  }
2152
- throw new ConcurrentFireActionError({
2153
- instanceId,
2154
- taskId,
2155
- action,
2156
- attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2157
- });
2158
- }
2159
- async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
2160
- const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
2161
- if (action === void 0)
2162
- throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
2163
- if (!await ctxEvaluateFilter(ctx, action.filter))
2164
- throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
2165
- const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
2166
- if (entry === void 0 || entry.status !== "active")
2167
- throw new Error(
2168
- `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2169
- );
2170
- const params = validateActionParams(action, taskId, callerParams);
2171
- return { stage, task, action, params };
2172
- }
2173
- function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor, now) {
2174
- if (action.setStatus === void 0) return;
2175
- const initialStatus = mutEntry.status, newStatus = action.setStatus;
2176
- return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
2177
- taskStatusChangedEntry({
2178
- stageId,
2179
- taskId,
2180
- from: initialStatus,
2181
- to: newStatus,
2182
- at: now,
2183
- ...actor !== void 0 ? { actor } : {}
2184
- })
2185
- ), newStatus;
2186
- }
2187
- async function commitAction(ctx, taskId, actionName, callerParams, actor) {
2188
- const { stage, task, action, params } = await resolveActionCommit(
2189
- ctx,
2190
- taskId,
2191
- actionName,
2192
- callerParams
2193
- ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
2194
- mutation.history.push({
2195
- _key: randomKey(),
2196
- _type: "workflow.history.actionFired",
2197
- at: now,
2198
- stageId: stage.id,
2199
- taskId,
2200
- actionId: actionName,
2201
- ...actor !== void 0 ? { actor } : {}
2202
- });
2203
- const ranOps = await runActionOps({
2204
- ops: action.ops,
2205
- mutation,
2206
- stageId: stage.id,
2207
- taskId,
2208
- actionName,
2209
- params,
2210
- actor,
2211
- self: selfGdr(ctx.instance),
2212
- now
2213
- }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor, now);
2214
- return await queueEffects(
2215
- ctx,
2216
- mutation,
2217
- action.effects,
2218
- {
2219
- kind: "action",
2220
- id: `${task.id}:${actionName}`
2221
- },
2222
- actor,
2223
- params
2224
- ), await persistThenDeploy(
2225
- ctx,
2226
- mutation,
2227
- () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
2228
- ), {
2229
- fired: !0,
2230
- taskId,
2231
- action: actionName,
2232
- ...newStatus !== void 0 ? { newStatus } : {},
2233
- ...ranOps.length > 0 ? { ranOps } : {}
2234
- };
2235
- }
2236
- const parsedFilters = /* @__PURE__ */ new Map();
2237
- async function matchesFilter(args) {
2238
- const { document, filter, userId } = args;
2239
- parsedFilters.has(filter) || parsedFilters.set(filter, groqJs.parse(`*[${filter}]`));
2240
- const ast = parsedFilters.get(filter), data = await (await groqJs.evaluate(ast, {
2241
- dataset: [document],
2242
- ...userId !== void 0 ? { identity: userId } : {}
2243
- })).get();
2244
- return Array.isArray(data) && data.length === 1;
2245
- }
2246
- async function grantsPermissionOn(args) {
2247
- const { document, grants, permission, userId } = args;
2248
- if (!document || grants.length === 0) return !1;
2249
- for (const grant of grants)
2250
- if (grant.permissions.includes(permission) && await matchesFilter({
2251
- document,
2252
- filter: grant.filter,
2253
- ...userId !== void 0 ? { userId } : {}
2254
- }))
2255
- return !0;
2256
- return !1;
2257
- }
2258
- async function fetchGrants(args) {
2259
- const { client, resourcePath, signal } = args;
2260
- return client.request({ url: resourcePath, ...signal ? { signal } : {} });
2261
- }
2262
- const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
2263
- async function resolveAccess(client, args = {}) {
2264
- const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2265
- if (overrideActor !== void 0 && overrideGrants !== void 0)
2266
- return { actor: overrideActor, grants: overrideGrants };
2267
- const requestFn = client.request === void 0 ? void 0 : (opts) => client.request(opts);
2268
- if (requestFn === void 0) {
2269
- if (overrideActor === void 0)
2270
- throw new Error(
2271
- "workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
2272
- );
2273
- return { actor: overrideActor };
2274
- }
2275
- const actorPromise = overrideActor !== void 0 ? Promise.resolve(overrideActor) : cachedActor(client, requestFn), grantsPromise = overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : args.grantsFromPath !== void 0 ? cachedGrants(client, requestFn, args.grantsFromPath) : Promise.resolve(void 0), [actor, grants] = await Promise.all([actorPromise, grantsPromise]);
2276
- if (actor === void 0)
2277
- throw new Error(
2278
- "workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity \u2014 check the token, or pass `access.actor` explicitly."
2279
- );
2280
- return { actor, ...grants !== void 0 ? { grants } : {} };
2281
- }
2282
- function cachedActor(client, requestFn) {
2283
- const cached = actorCache.get(client);
2284
- if (cached !== void 0) return cached;
2285
- const pending = fetchActor(requestFn).catch((err) => {
2286
- throw actorCache.get(client) === pending && actorCache.delete(client), err;
2287
- });
2288
- return actorCache.set(client, pending), pending;
2289
- }
2290
- function cachedGrants(client, requestFn, resourcePath) {
2291
- let byPath = grantsCache.get(client);
2292
- byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
2293
- let cached = byPath.get(resourcePath);
2294
- return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
2295
- }
2296
- async function fetchActor(requestFn) {
2297
- let user;
2298
- try {
2299
- user = await requestFn({
2300
- uri: "/users/me",
2301
- tag: "workflow.access.resolve-actor"
2302
- });
2303
- } catch (err) {
2304
- throw new Error(
2305
- 'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
2306
- { cause: err }
2307
- );
2308
- }
2309
- if (!user || typeof user.id != "string" || user.id.length === 0) return;
2310
- const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
2311
- return {
2312
- kind: "user",
2313
- id: user.id,
2314
- ...roleNames.length > 0 ? { roles: roleNames } : {}
2315
- };
2316
- }
2317
- async function fetchGrantsCached(requestFn, resourcePath) {
2318
- try {
2319
- return await fetchGrants({ client: { request: requestFn }, resourcePath });
2320
- } catch (err) {
2321
- console.warn(
2322
- `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2323
- );
2324
- return;
2325
- }
2326
- }
2327
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
2328
- function validateTags(tags) {
2329
- if (tags.length === 0)
2330
- throw new Error("tags: must be a non-empty array");
2331
- for (const tag of tags)
2332
- if (!TAG_RE.test(tag))
2333
- throw new Error(
2334
- `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
2335
- );
2336
- }
2337
- function canonicalTag(tags) {
2338
- return tags[0];
2339
- }
2340
- const WILDCARD_ROLE = "*";
2341
- async function evaluateInstance(args) {
2342
- const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2343
- validateTags(tags);
2344
- const { actor, grants } = await resolveAccess(client, {
2345
- ...args.access !== void 0 ? { override: args.access } : {},
2346
- ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2347
- }), instance = await client.getDocument(instanceId);
2348
- if (!instance)
2349
- throw new Error(`Workflow instance "${instanceId}" not found`);
2350
- if (!tags.some((t) => instance.tags?.includes(t)))
2351
- throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2352
- const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2353
- if (stage === void 0)
2354
- throw new Error(
2355
- `Instance "${instanceId}" currentStageId "${instance.currentStageId}" not in definition`
2356
- );
2357
- const snapshot = await hydrateSnapshot({
2358
- client,
2359
- clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client,
2360
- instance,
2361
- definition
2362
- }), currentTaskEntries = instance.stages.find(
2363
- (s) => s.id === instance.currentStageId && s.exitedAt === void 0
2364
- )?.tasks ?? [], taskEvaluations = [];
2365
- for (const task of stage.tasks ?? [])
2366
- taskEvaluations.push(
2367
- await evaluateTask({
2368
- task,
2369
- statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2370
- instance,
2371
- definition,
2372
- actor,
2373
- grants,
2374
- snapshot,
2375
- now
2376
- })
2377
- );
2378
- const transitionEvaluations = [];
2379
- for (const transition of stage.transitions ?? [])
2380
- transitionEvaluations.push({
2381
- transition,
2382
- filterSatisfied: await evaluateFilter({
2383
- filter: transition.filter,
2384
- definition,
2385
- snapshot,
2386
- params: buildParams(instance, now)
2387
- })
2388
- });
2389
- const currentStage = {
2390
- stage,
2391
- tasks: taskEvaluations,
2392
- transitions: transitionEvaluations
2393
- }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2394
- return {
2395
- instance,
2396
- definition,
2397
- actor,
2398
- currentStage,
2399
- pendingOnYou,
2400
- canInteract
2401
- };
2402
- }
2403
- async function evaluateTask(args) {
2404
- const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
2405
- for (const action of task.actions ?? [])
2406
- actions.push(
2407
- await evaluateAction({
2408
- action,
2409
- task,
2410
- ...statusEntry !== void 0 ? { statusEntry } : {},
2411
- status,
2412
- instance,
2413
- definition,
2414
- actor,
2415
- ...grants !== void 0 ? { grants } : {},
2416
- snapshot,
2417
- now
2418
- })
2419
- );
2420
- return {
2421
- task,
2422
- status,
2423
- // "pending on actor" treats both `pending` and `active` as waiting on
2424
- // the assignee — pending tasks just haven't been auto-invoked yet, but
2425
- // they're still inbox items.
2426
- pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2427
- actions
2428
- };
2429
- }
2430
- async function evaluateAction(args) {
2431
- const { action, task, status, instance, definition, actor, grants, snapshot, now } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2432
- if (syncReason !== void 0) return disabled(action, syncReason);
2433
- const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot, now);
2434
- return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
2435
- }
2436
- function lifecycleReason(instance, definition, status) {
2437
- if (instance.completedAt !== void 0)
2438
- return { kind: "instance-completed", completedAt: instance.completedAt };
2439
- if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2440
- return { kind: "stage-terminal", stageId: instance.currentStageId };
2441
- if (status === "done" || status === "skipped" || status === "failed")
2442
- return { kind: "task-not-active", status };
2443
- }
2444
- function actorReason(action, task, actor) {
2445
- const actorRoles = actor.roles ?? [];
2446
- if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2447
- return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2448
- if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2449
- return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
2450
- }
2451
- async function permissionReason(action, instance, actor, grants) {
2452
- if (grants === void 0) return;
2453
- const permission = action.requiredPermission ?? "update";
2454
- if (!await grantsPermissionOn({
2455
- document: instance,
2456
- grants,
2457
- permission,
2458
- userId: actor.id
2459
- }))
2460
- return { kind: "permission-denied", permission, matchingGrants: grants.length };
2461
- }
2462
- async function filterReason(action, instance, definition, snapshot, now) {
2463
- if (!(action.filter === void 0 || await evaluateFilter({
2464
- filter: action.filter,
2465
- definition,
2466
- snapshot,
2467
- params: buildParams(instance, now)
2468
- })))
2469
- return { kind: "filter-failed", filter: action.filter };
2470
- }
2471
- function disabled(action, reason) {
2472
- return { action, allowed: !1, disabledReason: reason };
2473
- }
2474
- function actorIsAssignee(actor, assignees) {
2475
- if (assignees.length === 0) return !1;
2476
- const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2477
- for (const assignee of assignees)
2478
- if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
2479
- return !0;
2480
- return !1;
2481
- }
2482
- function parseDefinitionSnapshot(instance) {
2483
- try {
2484
- return JSON.parse(instance.definitionSnapshot);
2485
- } catch (err) {
2486
- throw new Error(
2487
- `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2488
- { cause: err }
2489
- );
2490
- }
2491
- }
2492
- const resourceKey = (r) => `${r.type}.${r.id}`;
2493
- function guardsForResource(client) {
2494
- return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2495
- }
2496
- async function guardsForInstance(args) {
2497
- const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
2498
- [resourceKey(instance.workflowResource), client]
2499
- ]), stateUris = [
2500
- ...collectSlotDocUris(instance.state),
2501
- ...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
2502
- ];
2503
- for (const uri of stateUris) {
2504
- const parsed = tryParseGdr(uri);
2505
- if (parsed === void 0) continue;
2506
- const key = resourceKey(resourceFromParsed(parsed));
2507
- clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
2508
- }
2509
- const perResource = await Promise.all(
2510
- [...clientsByResource.values()].map(
2511
- (c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
2512
- t: GUARD_DOC_TYPE,
2513
- id: instance._id
2514
- })
2515
- )
2516
- );
2517
- return dedupById(perResource.flat());
2610
+ throw new ConcurrentFireActionError({
2611
+ instanceId,
2612
+ taskId,
2613
+ action,
2614
+ attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2615
+ });
2518
2616
  }
2519
- function tryParseGdr(uri) {
2520
- try {
2521
- return parseGdr(uri);
2522
- } catch {
2523
- return;
2524
- }
2617
+ async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
2618
+ const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
2619
+ if (action === void 0)
2620
+ throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
2621
+ if (!await ctxEvaluateFilter(ctx, action.filter))
2622
+ throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
2623
+ const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
2624
+ if (entry === void 0 || entry.status !== "active")
2625
+ throw new Error(
2626
+ `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2627
+ );
2628
+ const params = validateActionParams(action, taskId, callerParams);
2629
+ return { stage, task, action, params };
2525
2630
  }
2526
- function dedupById(guards) {
2527
- return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2631
+ function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor, now) {
2632
+ if (action.setStatus === void 0) return;
2633
+ const initialStatus = mutEntry.status, newStatus = action.setStatus;
2634
+ return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
2635
+ taskStatusChangedEntry({
2636
+ stageId,
2637
+ taskId,
2638
+ from: initialStatus,
2639
+ to: newStatus,
2640
+ at: now,
2641
+ ...actor !== void 0 ? { actor } : {}
2642
+ })
2643
+ ), newStatus;
2644
+ }
2645
+ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
2646
+ const { stage, task, action, params } = await resolveActionCommit(
2647
+ ctx,
2648
+ taskId,
2649
+ actionName,
2650
+ callerParams
2651
+ ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
2652
+ mutation.history.push({
2653
+ _key: randomKey(),
2654
+ _type: "workflow.history.actionFired",
2655
+ at: now,
2656
+ stageId: stage.id,
2657
+ taskId,
2658
+ actionId: actionName,
2659
+ ...actor !== void 0 ? { actor } : {}
2660
+ });
2661
+ const ranOps = await runActionOps({
2662
+ ops: action.ops,
2663
+ mutation,
2664
+ stageId: stage.id,
2665
+ taskId,
2666
+ actionName,
2667
+ params,
2668
+ actor,
2669
+ self: selfGdr(ctx.instance),
2670
+ now
2671
+ }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor, now);
2672
+ return await queueEffects(
2673
+ ctx,
2674
+ mutation,
2675
+ action.effects,
2676
+ {
2677
+ kind: "action",
2678
+ id: `${task.id}:${actionName}`
2679
+ },
2680
+ actor,
2681
+ params
2682
+ ), await persistThenDeploy(
2683
+ ctx,
2684
+ mutation,
2685
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
2686
+ ), {
2687
+ fired: !0,
2688
+ taskId,
2689
+ action: actionName,
2690
+ ...newStatus !== void 0 ? { newStatus } : {},
2691
+ ...ranOps.length > 0 ? { ranOps } : {}
2692
+ };
2528
2693
  }
2529
2694
  class ActionDisabledError extends Error {
2530
2695
  reason;
@@ -2548,121 +2713,6 @@ function formatDisabledReason(taskId, action, reason) {
2548
2713
  const detail = disabledReasonDetail[reason.kind](reason);
2549
2714
  return `Action "${taskId}:${action}" is not allowed: ${detail}`;
2550
2715
  }
2551
- function definitionDocId(_workflowResource, prefix, workflowId, version) {
2552
- return `${prefix}.${workflowId}.v${version}`;
2553
- }
2554
- async function sortByDependencies(client, definitions, tags, workflowResource) {
2555
- const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2556
- for (const def of definitions) {
2557
- const list = byWorkflowId.get(def.workflowId) ?? [];
2558
- list.push(def), byWorkflowId.set(def.workflowId, list);
2559
- }
2560
- const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2561
- return await assertCrossBatchRefsDeployed(client, definitions, {
2562
- tags,
2563
- workflowResource,
2564
- prefix,
2565
- findInBatch
2566
- }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2567
- }
2568
- function findRefInBatch(byWorkflowId, ref) {
2569
- const candidates = byWorkflowId.get(ref.workflowId);
2570
- if (!(candidates === void 0 || candidates.length === 0))
2571
- return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2572
- (best, c) => best === void 0 || c.version > best.version ? c : best,
2573
- void 0
2574
- );
2575
- }
2576
- async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2577
- const missing = [];
2578
- for (const def of definitions) {
2579
- const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2580
- for (const ref of refsOf(def)) {
2581
- if (ctx.findInBatch(ref) !== void 0) continue;
2582
- const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2583
- label !== void 0 && missing.push({ from: fromId, ref: label });
2584
- }
2585
- }
2586
- if (missing.length === 0) return;
2587
- const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
2588
- throw new Error(
2589
- `workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
2590
- ` + lines.join(`
2591
- `)
2592
- );
2593
- }
2594
- async function resolveDeployedRefLabel(client, ref, tags) {
2595
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
2596
- if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2597
- definitionLookupGroq(wantsExplicit),
2598
- params
2599
- ))
2600
- return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2601
- }
2602
- function topoSortDefinitions(definitions, ctx) {
2603
- const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2604
- const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2605
- if (!visited.has(id)) {
2606
- if (visiting.has(id))
2607
- throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
2608
- visiting.add(id);
2609
- for (const ref of refsOf(def)) {
2610
- const child = ctx.findInBatch(ref);
2611
- child !== void 0 && visit(child);
2612
- }
2613
- visiting.delete(id), visited.add(id), ordered.push(def);
2614
- }
2615
- };
2616
- for (const def of definitions) visit(def);
2617
- return ordered;
2618
- }
2619
- function definitionLookupGroq(wantsExplicit) {
2620
- return wantsExplicit ? '*[_type == "workflow.definition" && workflowId == $workflowId && version == $version && count(tags[@ in $tags]) > 0][0]' : '*[_type == "workflow.definition" && workflowId == $workflowId && count(tags[@ in $tags]) > 0] | order(version desc)[0]';
2621
- }
2622
- function refsOf(def) {
2623
- const out = [];
2624
- for (const stage of def.stages)
2625
- for (const task of stage.tasks ?? []) {
2626
- const ref = task.spawns?.definitionRef;
2627
- ref !== void 0 && out.push({
2628
- workflowId: ref.workflowId,
2629
- ...ref.version !== void 0 ? { version: ref.version } : {}
2630
- });
2631
- }
2632
- return out;
2633
- }
2634
- function isDefinitionUnchanged(existing, expected) {
2635
- return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
2636
- }
2637
- function stripSystemFields(doc) {
2638
- const out = {};
2639
- for (const [k, v2] of Object.entries(doc))
2640
- k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v2);
2641
- return out;
2642
- }
2643
- function stableStringify(value) {
2644
- return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2645
- ([a], [b]) => a.localeCompare(b)
2646
- ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2647
- }
2648
- async function loadDefinition(client, workflowId, version, tags) {
2649
- if (version !== void 0) {
2650
- const doc = await client.fetch(
2651
- "*[_type == 'workflow.definition' && workflowId == $id && version == $v && count(tags[@ in $engineTags]) > 0][0]",
2652
- { id: workflowId, v: version, engineTags: tags }
2653
- );
2654
- if (!doc)
2655
- throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2656
- return doc;
2657
- }
2658
- const latest = await client.fetch(
2659
- "*[_type == 'workflow.definition' && workflowId == $id && count(tags[@ in $engineTags]) > 0] | order(version desc)[0]",
2660
- { id: workflowId, engineTags: tags }
2661
- );
2662
- if (!latest)
2663
- throw new Error(`No deployed definition for workflow ${workflowId}`);
2664
- return latest;
2665
- }
2666
2716
  function bareIdFromSpawnRef(uri) {
2667
2717
  if (!uri.includes(":")) return [uri];
2668
2718
  try {
@@ -2683,8 +2733,15 @@ async function resolveOperationContext(args) {
2683
2733
  clientForGdr: buildClientForGdr(args.client, args.resourceClients)
2684
2734
  };
2685
2735
  }
2686
- async function cascade(client, instanceId, actor, clientForGdr, clock) {
2687
- const count = await cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock);
2736
+ async function cascade(client, instanceId, actor, clientForGdr, clock, overlay) {
2737
+ const count = await cascadeAutoTransitions(
2738
+ client,
2739
+ instanceId,
2740
+ actor,
2741
+ clientForGdr,
2742
+ clock,
2743
+ overlay
2744
+ );
2688
2745
  return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
2689
2746
  }
2690
2747
  function buildClientForGdr(defaultClient, resolver) {
@@ -2703,6 +2760,22 @@ function intersectsTags(docTags, engineTags) {
2703
2760
  function toEffectsContextEntries(ctx) {
2704
2761
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2705
2762
  }
2763
+ function assertActionAllowed(evaluation, taskId, action) {
2764
+ const actionEval = evaluation.currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2765
+ if (actionEval?.allowed === !1 && actionEval.disabledReason)
2766
+ throw new ActionDisabledError({ taskId, action, reason: actionEval.disabledReason });
2767
+ }
2768
+ async function applyAction(args) {
2769
+ const { client, instance, taskId, action, params, actor, clock } = args, options = { actor, ...clock !== void 0 ? { clock } : {} };
2770
+ return findOpenStageEntry(instance)?.tasks.find((t) => t.id === taskId)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, taskId, options }), (await fireAction({
2771
+ client,
2772
+ instanceId: instance._id,
2773
+ taskId,
2774
+ action,
2775
+ ...params !== void 0 ? { params } : {},
2776
+ options
2777
+ })).ranOps;
2778
+ }
2706
2779
  const workflow = {
2707
2780
  /**
2708
2781
  * Persist a workflow definition to the lake. Stored as a
@@ -2735,7 +2808,7 @@ const workflow = {
2735
2808
  const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
2736
2809
  ...def,
2737
2810
  _id: id,
2738
- _type: "workflow.definition",
2811
+ _type: schema.WORKFLOW_DEFINITION_TYPE,
2739
2812
  tags
2740
2813
  }, existing = await client.getDocument(id);
2741
2814
  if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
@@ -2831,44 +2904,32 @@ const workflow = {
2831
2904
  params,
2832
2905
  idempotent,
2833
2906
  resourceClients
2834
- } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags), taskEntry = before.stages.find(
2835
- (s) => s.id === before.currentStageId && s.exitedAt === void 0
2836
- )?.tasks.find((t) => t.id === taskId);
2837
- if (taskEntry === void 0 && idempotent === !0)
2907
+ } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags);
2908
+ if (findOpenStageEntry(before)?.tasks.find((t) => t.id === taskId) === void 0 && idempotent === !0)
2838
2909
  return { instance: before, cascaded: 0, fired: !1 };
2839
- const actionEval = (await evaluateInstance({
2910
+ const evaluation = await evaluateInstance({
2840
2911
  client,
2841
2912
  tags,
2842
2913
  instanceId,
2843
2914
  access,
2844
2915
  clock,
2845
2916
  ...resourceClients !== void 0 ? { resourceClients } : {}
2846
- })).currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2847
- if (actionEval !== void 0 && !actionEval.allowed && actionEval.disabledReason)
2848
- throw new ActionDisabledError({
2849
- taskId,
2850
- action,
2851
- reason: actionEval.disabledReason
2852
- });
2853
- taskEntry?.status === "pending" && await invokeTask({
2854
- client,
2855
- instanceId,
2856
- taskId,
2857
- options: { actor, clock }
2858
2917
  });
2859
- const actionResult = await fireAction({
2918
+ assertActionAllowed(evaluation, taskId, action);
2919
+ const ranOps = await applyAction({
2860
2920
  client,
2861
- instanceId,
2921
+ instance: before,
2862
2922
  taskId,
2863
2923
  action,
2864
- ...params !== void 0 ? { params } : {},
2865
- options: { actor, clock }
2924
+ actor,
2925
+ clock,
2926
+ ...params !== void 0 ? { params } : {}
2866
2927
  }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2867
2928
  return {
2868
2929
  instance: await reload(client, instanceId, tags),
2869
2930
  cascaded,
2870
2931
  fired: !0,
2871
- ...actionResult.ranOps !== void 0 ? { ranOps: actionResult.ranOps } : {}
2932
+ ...ranOps !== void 0 ? { ranOps } : {}
2872
2933
  };
2873
2934
  },
2874
2935
  /**
@@ -2993,7 +3054,7 @@ const workflow = {
2993
3054
  queryInScope: async (args) => {
2994
3055
  const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
2995
3056
  validateTags(tags);
2996
- const instance = await reload(client, instanceId, tags), definition = JSON.parse(instance.definitionSnapshot), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition }), reserved = buildParams(instance, clock()), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
3057
+ const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams(instance, clock()), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
2997
3058
  return await (await groqJs.evaluate(tree, {
2998
3059
  dataset: snapshot.docs,
2999
3060
  params: { ...reserved, ...params }
@@ -3036,7 +3097,7 @@ const workflow = {
3036
3097
  validateTags(tags);
3037
3098
  const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "workflow.history.spawned", ids = parent.history.filter(isSpawned).filter((h) => taskId === void 0 || h.taskId === taskId).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
3038
3099
  return ids.length === 0 ? [] : client.fetch(
3039
- "*[_id in $ids && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)",
3100
+ `*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
3040
3101
  { ids, engineTags: tags }
3041
3102
  );
3042
3103
  },
@@ -3055,7 +3116,7 @@ async function verifyDeployedDefinitionsInternal(args) {
3055
3116
  const { client, tags, effectHandlers, missingHandler, logger } = args;
3056
3117
  validateTags(tags);
3057
3118
  const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
3058
- '*[_type == "workflow.definition" && count(tags[@ in $engineTags]) > 0] | order(workflowId asc, version asc)',
3119
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(workflowId asc, version asc)`,
3059
3120
  { engineTags: tags }
3060
3121
  ), seen = [], missingByName = /* @__PURE__ */ new Map();
3061
3122
  for (const def of definitions)
@@ -3206,6 +3267,89 @@ async function applyMissingHandler(policy, info, log) {
3206
3267
  return "fail";
3207
3268
  }
3208
3269
  }
3270
+ function createInstanceSession(args) {
3271
+ const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
3272
+ let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), committing = !1, buffered;
3273
+ const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
3274
+ const next = /* @__PURE__ */ new Map();
3275
+ for (const ld of docs) {
3276
+ const owned = { doc: structuredClone(ld.doc), resource: ld.resource }, uri = gdrFromResource(owned.resource, owned.doc._id);
3277
+ if (uri === selfUri()) {
3278
+ instance = asHeldInstance(owned.doc);
3279
+ continue;
3280
+ }
3281
+ next.set(uri, owned);
3282
+ }
3283
+ overlay = next;
3284
+ }, access = () => resolveAccess(client, {
3285
+ ...args.access !== void 0 ? { override: args.access } : {},
3286
+ ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
3287
+ }), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held) => {
3288
+ const { actor, grants } = await access();
3289
+ return evaluateFromSnapshot({
3290
+ instance,
3291
+ definition: parseDefinitionSnapshot(instance),
3292
+ actor,
3293
+ snapshot: snapshotFrom(held),
3294
+ ...clock !== void 0 ? { now: clock() } : {},
3295
+ ...grants !== void 0 ? { grants } : {}
3296
+ });
3297
+ }, commit = async (run) => {
3298
+ committing = !0;
3299
+ const held = new Map(overlay);
3300
+ try {
3301
+ const { actor } = await access();
3302
+ return await run(actor, held);
3303
+ } finally {
3304
+ if (committing = !1, buffered !== void 0) {
3305
+ const next = buffered;
3306
+ buffered = void 0, applyUpdate(next);
3307
+ }
3308
+ }
3309
+ };
3310
+ return {
3311
+ get subscriptionDocuments() {
3312
+ return subscriptionDocumentsForInstance(instance);
3313
+ },
3314
+ update(docs) {
3315
+ if (committing) {
3316
+ buffered = docs;
3317
+ return;
3318
+ }
3319
+ applyUpdate(docs);
3320
+ },
3321
+ evaluate() {
3322
+ return evaluateWith(overlay);
3323
+ },
3324
+ tick() {
3325
+ return commit(async (actor, held) => {
3326
+ const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3327
+ return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
3328
+ });
3329
+ },
3330
+ fireAction({ taskId, action, params }) {
3331
+ return commit(async (actor, held) => {
3332
+ assertActionAllowed(await evaluateWith(held), taskId, action);
3333
+ const ranOps = await applyAction({
3334
+ client,
3335
+ instance,
3336
+ taskId,
3337
+ action,
3338
+ actor,
3339
+ ...clock !== void 0 ? { clock } : {},
3340
+ ...params !== void 0 ? { params } : {}
3341
+ }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3342
+ return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
3343
+ });
3344
+ }
3345
+ };
3346
+ }
3347
+ function asHeldInstance(doc) {
3348
+ const candidate = doc;
3349
+ if (candidate._type !== "workflow.instance" || typeof candidate.currentStageId != "string" || !Array.isArray(candidate.stages))
3350
+ throw new Error(`update: self-doc ${doc._id} is not a valid workflow.instance`);
3351
+ return doc;
3352
+ }
3209
3353
  const defaultLoggerFactory = (name) => ({
3210
3354
  // info/warn/error all go to stderr — these are diagnostic, not
3211
3355
  // program output. Keeps stdout reserved for the caller's own data
@@ -3248,6 +3392,16 @@ function createEngine(args) {
3248
3392
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
3249
3393
  setStage: (rest) => workflow.setStage(bind(rest)),
3250
3394
  getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
3395
+ subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
3396
+ instance: (instanceDoc, opts) => createInstanceSession({
3397
+ client,
3398
+ tags,
3399
+ instance: instanceDoc,
3400
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3401
+ ...clock !== void 0 ? { clock } : {},
3402
+ ...opts?.access !== void 0 ? { access: opts.access } : {},
3403
+ ...opts?.grantsFromPath !== void 0 ? { grantsFromPath: opts.grantsFromPath } : {}
3404
+ }),
3251
3405
  guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
3252
3406
  client,
3253
3407
  tags,
@@ -3498,6 +3652,7 @@ function displayDescription(typeKey) {
3498
3652
  if (typeKey)
3499
3653
  return DISPLAY[typeKey]?.description;
3500
3654
  }
3655
+ exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
3501
3656
  exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
3502
3657
  exports.ActionDisabledError = ActionDisabledError;
3503
3658
  exports.ActionParamsInvalidError = ActionParamsInvalidError;
@@ -3512,15 +3667,19 @@ exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
3512
3667
  exports.MutationGuardDeniedError = MutationGuardDeniedError;
3513
3668
  exports.OP_DISPLAY = OP_DISPLAY;
3514
3669
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
3670
+ exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
3515
3671
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
3672
+ exports.buildSnapshot = buildSnapshot;
3516
3673
  exports.canonicalTag = canonicalTag;
3517
3674
  exports.compileGuard = compileGuard;
3675
+ exports.contentReleaseName = contentReleaseName;
3518
3676
  exports.createEngine = createEngine;
3519
3677
  exports.defaultLoggerFactory = defaultLoggerFactory;
3520
3678
  exports.denyingGuards = denyingGuards;
3521
3679
  exports.deployStageGuards = deployStageGuards;
3522
3680
  exports.displayDescription = displayDescription;
3523
3681
  exports.displayTitle = displayTitle;
3682
+ exports.evaluateFromSnapshot = evaluateFromSnapshot;
3524
3683
  exports.evaluateMutationGuard = evaluateMutationGuard;
3525
3684
  exports.extractDocumentId = extractDocumentId;
3526
3685
  exports.gdrFromResource = gdrFromResource;
@@ -3533,14 +3692,18 @@ exports.isDefinitionUnchanged = isDefinitionUnchanged;
3533
3692
  exports.isGdr = isGdr;
3534
3693
  exports.lakeGuardId = lakeGuardId;
3535
3694
  exports.parseGdr = parseGdr;
3695
+ exports.readsRaw = readsRaw;
3536
3696
  exports.refCanvas = refCanvas;
3537
3697
  exports.refDashboard = refDashboard;
3538
3698
  exports.refDataset = refDataset;
3539
3699
  exports.refMediaLibrary = refMediaLibrary;
3540
3700
  exports.resolveAccess = resolveAccess;
3701
+ exports.resourceFromParsed = resourceFromParsed;
3541
3702
  exports.retractStageGuards = retractStageGuards;
3542
3703
  exports.silentLogger = silentLogger;
3543
3704
  exports.stripSystemFields = stripSystemFields;
3705
+ exports.subscriptionDocumentsForInstance = subscriptionDocumentsForInstance;
3706
+ exports.tagScopeFilter = tagScopeFilter;
3544
3707
  exports.validateDefinition = validateDefinition;
3545
3708
  exports.validateTags = validateTags;
3546
3709
  exports.wallClock = wallClock;