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