@sanity/workflow-engine 0.1.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,17 +1,18 @@
1
1
  import { parse, evaluate } from "groq-js";
2
- import { z } from "zod";
2
+ import { WORKFLOW_DEFINITION_TYPE } from "./_chunks-es/schema.js";
3
+ import * as v from "valibot";
3
4
  function validateDefinition(definition) {
4
- const v = createDefinitionValidator();
5
+ const v2 = createDefinitionValidator();
5
6
  for (const slot of definition.state ?? [])
6
- v.checkSlot(slot, "workflow.state", `workflow.state "${slot.id}"`);
7
+ v2.checkSlot(slot, `workflow.state "${slot.id}"`);
7
8
  for (const p of definition.predicates ?? [])
8
- p.groq && v.checkFilter(p.groq, `predicate "${p.id}"`);
9
+ p.groq && v2.checkFilter(p.groq, `predicate "${p.id}"`);
9
10
  for (const stage of definition.stages)
10
- validateStage(v, stage);
11
- if (v.errors.length > 0)
11
+ validateStage(v2, stage);
12
+ if (v2.errors.length > 0)
12
13
  throw new Error(
13
- `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v.errors.length} validation error${v.errors.length === 1 ? "" : "s"}:
14
- ` + v.errors.join(`
14
+ `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
15
+ ` + v2.errors.join(`
15
16
  `)
16
17
  );
17
18
  }
@@ -30,31 +31,29 @@ function createDefinitionValidator() {
30
31
  };
31
32
  return { errors, tryParse, checkFilter: (groq, where) => {
32
33
  tryParse(groq, where), rejectTypeScan(groq, where);
33
- }, checkSlot: (slot, scopeLabel, slotLabel) => {
34
- slot.source.kind === "computed" && errors.push(
35
- ` \xB7 ${scopeLabel} slot "${slot.id}": source.kind="computed" is reserved (not implemented in v0.1)`
36
- ), slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
34
+ }, checkSlot: (slot, slotLabel) => {
35
+ slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
37
36
  } };
38
37
  }
39
- function validateStage(v, stage) {
38
+ function validateStage(v2, stage) {
40
39
  for (const slot of stage.state ?? [])
41
- v.checkSlot(slot, `stage "${stage.id}".state`, `stage "${stage.id}".state "${slot.id}"`);
42
- typeof stage.completion == "string" && v.tryParse(stage.completion, `stage "${stage.id}".completion`);
40
+ v2.checkSlot(slot, `stage "${stage.id}".state "${slot.id}"`);
41
+ typeof stage.completion == "string" && v2.tryParse(stage.completion, `stage "${stage.id}".completion`);
43
42
  for (const t of stage.transitions ?? [])
44
- typeof t.filter == "string" && v.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
43
+ typeof t.filter == "string" && v2.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
45
44
  for (const task of stage.tasks ?? [])
46
- validateTask(v, stage.id, task);
45
+ validateTask(v2, stage.id, task);
47
46
  }
48
- function validateTask(v, stageId, task) {
47
+ function validateTask(v2, stageId, task) {
49
48
  const where = `stage "${stageId}" task "${task.id}"`;
50
49
  for (const slot of task.state ?? [])
51
- v.checkSlot(slot, `${where}.state`, `${where}.state "${slot.id}"`);
52
- typeof task.filter == "string" && v.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v.checkFilter(task.completeWhen, `${where}.completeWhen`);
50
+ v2.checkSlot(slot, `${where}.state "${slot.id}"`);
51
+ typeof task.filter == "string" && v2.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v2.checkFilter(task.completeWhen, `${where}.completeWhen`);
53
52
  for (const a of task.actions ?? [])
54
- typeof a.filter == "string" && v.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
55
- task.spawns?.forEach?.groq && v.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
53
+ typeof a.filter == "string" && v2.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
54
+ task.spawns?.forEach?.groq && v2.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
56
55
  }
57
- const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
56
+ const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
58
57
  function parseGdr(uri) {
59
58
  const colon = uri.indexOf(":");
60
59
  if (colon < 0)
@@ -121,6 +120,9 @@ function gdrFromResource(res, documentId) {
121
120
  }
122
121
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
123
122
  }
123
+ function selfGdr(doc) {
124
+ return gdrFromResource(doc.workflowResource, doc._id);
125
+ }
124
126
  function isGdrUri(value) {
125
127
  if (typeof value != "string") return !1;
126
128
  try {
@@ -135,15 +137,15 @@ function gdrRef(res, documentId, type) {
135
137
  function isGdr(value) {
136
138
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
137
139
  }
138
- function buildParams(instance, extra) {
140
+ function buildParams(instance, now, extra) {
139
141
  return {
140
- self: gdrFromResource(instance.workflowResource, instance._id),
142
+ self: selfGdr(instance),
141
143
  state: stateMap(instance.state ?? []),
142
144
  parent: instance.ancestors.at(-1)?.id ?? null,
143
145
  ancestors: instance.ancestors.map((a) => a.id),
144
146
  /** Current stage id — bind for filters that scope to the current stage. */
145
147
  stage: instance.currentStageId,
146
- now: (/* @__PURE__ */ new Date()).toISOString(),
148
+ now,
147
149
  ...extra
148
150
  };
149
151
  }
@@ -167,8 +169,8 @@ function stripStateForLake(value) {
167
169
  if (Array.isArray(value)) return value.map(stripStateForLake);
168
170
  if (typeof value == "object") {
169
171
  const out = {};
170
- for (const [k, v] of Object.entries(value))
171
- out[k] = stripStateForLake(v);
172
+ for (const [k, v2] of Object.entries(value))
173
+ out[k] = stripStateForLake(v2);
172
174
  return out;
173
175
  }
174
176
  return value;
@@ -176,9 +178,94 @@ function stripStateForLake(value) {
176
178
  function bareId(id) {
177
179
  return isGdrUri(id) ? extractDocumentId(id) : id;
178
180
  }
179
- function randomKey(length = 12) {
180
- const bytes = new Uint8Array(length);
181
- 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 };
182
269
  }
183
270
  async function evaluateFilter(args) {
184
271
  const { filter, definition, snapshot, params } = args;
@@ -226,7 +313,7 @@ function matchesParamType(type, value) {
226
313
  case "boolean":
227
314
  return typeof value == "boolean";
228
315
  case "string[]":
229
- return Array.isArray(value) && value.every((v) => typeof v == "string");
316
+ return Array.isArray(value) && value.every((v2) => typeof v2 == "string");
230
317
  case "reference":
231
318
  return isGdr(value);
232
319
  }
@@ -234,6 +321,183 @@ function matchesParamType(type, value) {
234
321
  function describeValue(value) {
235
322
  return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
236
323
  }
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;
332
+ }
333
+ }
334
+ function lakeGuardId(args) {
335
+ return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
336
+ }
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
352
+ };
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;
369
+ }
370
+ return !0;
371
+ }
372
+ async function evaluateMutationGuard(args) {
373
+ const { guard, context } = args;
374
+ if (guard.predicate === "") return !1;
375
+ const params = { guard, mutation: { action: context.action } };
376
+ try {
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;
384
+ } catch {
385
+ return !1;
386
+ }
387
+ }
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;
393
+ }
394
+ function resourceOf(p) {
395
+ return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
396
+ }
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) };
405
+ }
406
+ return null;
407
+ }
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);
414
+ }
415
+ return targets.length === 0 ? null : targets;
416
+ }
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
+ );
425
+ }
426
+ return resource;
427
+ }
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
+ }
237
501
  function buildSnapshot(args) {
238
502
  const docs = [], knownIds = /* @__PURE__ */ new Set();
239
503
  for (const { doc, resource } of args.docs) {
@@ -247,74 +511,100 @@ function restampForSnapshot(doc, gdrUri2, resource) {
247
511
  }
248
512
  function rewriteRefsRecursive(value, resource) {
249
513
  if (Array.isArray(value))
250
- return value.map((v) => rewriteRefsRecursive(v, resource));
514
+ return value.map((v2) => rewriteRefsRecursive(v2, resource));
251
515
  if (value === null || typeof value != "object") return value;
252
516
  const obj = value, out = {};
253
- for (const [k, v] of Object.entries(obj))
254
- k === "_ref" && typeof v == "string" ? out[k] = v.includes(":") ? v : gdrFromResource(resource, v) : out[k] = rewriteRefsRecursive(v, resource);
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);
255
519
  return out;
256
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
+ }
257
565
  async function hydrateSnapshot(args) {
258
- const { client, clientForGdr, instance, definition } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri) => {
566
+ const { client, clientForGdr, instance, overlay } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri, perspective) => {
259
567
  if (visited.has(uri)) return;
260
- const fetched = await loadByGdr(client, clientForGdr, instance.workflowResource, uri);
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
+ );
261
580
  fetched && (loaded.push(fetched), visited.add(uri));
262
581
  };
263
- loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(gdrFromResource(instance.workflowResource, instance._id));
264
- for (const ancestor of instance.ancestors)
265
- await loadInto(ancestor.id);
266
- for (const uri of collectSlotDocUris(instance.state))
267
- await loadInto(uri);
268
- if (definition) {
269
- const stageEntry = instance.stages.find(
270
- (s) => s.id === instance.currentStageId && s.exitedAt === void 0
271
- );
272
- for (const uri of collectSlotDocUris(stageEntry?.state))
273
- await loadInto(uri);
274
- }
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);
275
585
  return buildSnapshot({ docs: loaded });
276
586
  }
277
- async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri) {
587
+ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
278
588
  let parsed;
279
589
  try {
280
590
  parsed = parseGdr(uri);
281
591
  } catch {
282
- const doc2 = await defaultClient.getDocument(uri);
592
+ const doc2 = await readDoc(defaultClient, uri, perspective);
283
593
  return doc2 ? { doc: doc2, resource: defaultResource } : null;
284
594
  }
285
- const doc = await clientForGdr(parsed).getDocument(parsed.documentId);
595
+ const routed = clientForGdr(parsed), doc = await readDoc(routed, parsed.documentId, perspective);
286
596
  return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
287
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
+ }
288
601
  function resourceFromParsed(parsed) {
289
602
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
290
603
  }
291
604
  function collectSlotDocUris(resolvedStateSlots) {
292
- return Array.isArray(resolvedStateSlots) ? resolvedStateSlots.flatMap(slotDocUris) : [];
293
- }
294
- function slotDocUris(slot) {
295
- if (!slot || typeof slot != "object") return [];
296
- const s = slot;
297
- if (s._type === "workflow.state.doc.ref") {
298
- const v = s.value, id = refId(v);
299
- return id !== void 0 ? [id] : [];
300
- }
301
- if (s._type === "workflow.state.doc.refs") {
302
- const v = s.value;
303
- return Array.isArray(v) ? v.map(refId).filter((id) => id !== void 0) : [];
304
- }
305
- return [];
306
- }
307
- function refId(ref) {
308
- return ref && typeof ref.id == "string" ? ref.id : void 0;
309
- }
310
- function getPath(value, path) {
311
- let current = value;
312
- for (const part of path.split(".")) {
313
- if (current == null || typeof current != "object") return;
314
- current = current[part];
315
- }
316
- return current;
605
+ return slotDocRefs(resolvedStateSlots).map((ref) => ref.id);
317
606
  }
607
+ const WORKFLOW_INSTANCE_TYPE = "workflow.instance";
318
608
  class SlotValueShapeError extends Error {
319
609
  slotType;
320
610
  slotId;
@@ -324,50 +614,61 @@ class SlotValueShapeError extends Error {
324
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;
325
615
  }
326
616
  }
327
- const GdrShape = z.object({
328
- id: z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
329
- type: z.string().min(1)
330
- }).passthrough(), ReleaseRefShape = z.object({
331
- id: z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
332
- type: z.literal("system.release"),
333
- releaseName: z.string().min(1)
334
- }).passthrough(), ActorShape = z.object({
335
- kind: z.enum(["user", "ai", "system"]),
336
- id: z.string().min(1),
337
- roles: z.array(z.string()).optional(),
338
- onBehalfOf: z.string().optional()
339
- }).passthrough(), AssigneeShape = z.union([
340
- z.object({ kind: z.literal("user"), id: z.string().min(1) }).passthrough(),
341
- z.object({ kind: z.literal("role"), role: z.string().min(1) }).passthrough()
342
- ]), ChecklistItemShape = z.object({
343
- label: z.string().min(1),
344
- done: z.boolean(),
345
- doneBy: z.string().optional(),
346
- doneAt: z.string().optional(),
347
- _key: z.string().min(1).optional()
348
- }).passthrough(), NoteItemShape = z.object({
349
- _key: z.string().min(1).optional()
350
- }).passthrough().refine((v) => typeof v == "object" && v !== null && !Array.isArray(v), {
351
- message: "must be an object"
352
- }), NullableString = z.union([z.null(), z.string()]), NullableNumber = z.union([z.null(), z.number()]), NullableBoolean = z.union([z.null(), z.boolean()]), NullableDateTime = z.union([
353
- z.null(),
354
- z.string().refine((s) => !Number.isNaN(Date.parse(s)), {
355
- message: "must be an ISO-8601 datetime string"
356
- })
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"
651
+ )
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([
653
+ v.null(),
654
+ v.pipe(
655
+ v.string(),
656
+ v.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
657
+ )
357
658
  ]), NullableUrl = NullableString, valueSchemas = {
358
- "workflow.state.doc.ref": z.union([z.null(), GdrShape]),
359
- "workflow.state.doc.refs": z.array(GdrShape),
360
- "workflow.state.release.ref": z.union([z.null(), ReleaseRefShape]),
361
- "workflow.state.query": z.any(),
659
+ "workflow.state.doc.ref": v.union([v.null(), GdrShape]),
660
+ "workflow.state.doc.refs": v.array(GdrShape),
661
+ "workflow.state.release.ref": v.union([v.null(), ReleaseRefShape]),
662
+ "workflow.state.query": v.any(),
362
663
  "workflow.state.value.string": NullableString,
363
664
  "workflow.state.value.url": NullableUrl,
364
665
  "workflow.state.value.number": NullableNumber,
365
666
  "workflow.state.value.boolean": NullableBoolean,
366
667
  "workflow.state.value.dateTime": NullableDateTime,
367
- "workflow.state.value.actor": z.union([z.null(), ActorShape]),
368
- "workflow.state.checklist": z.array(ChecklistItemShape),
369
- "workflow.state.notes": z.array(NoteItemShape),
370
- "workflow.state.assignees": z.array(AssigneeShape)
668
+ "workflow.state.value.actor": v.union([v.null(), ActorShape]),
669
+ "workflow.state.checklist": v.array(ChecklistItemShape),
670
+ "workflow.state.notes": v.array(NoteItemShape),
671
+ "workflow.state.assignees": v.array(AssigneeShape)
371
672
  }, itemSchemas = {
372
673
  "workflow.state.doc.refs": GdrShape,
373
674
  "workflow.state.checklist": ChecklistItemShape,
@@ -386,12 +687,12 @@ function validateSlotValue(args) {
386
687
  issues: [`unknown slot type ${args.slotType}`],
387
688
  mode: "value"
388
689
  });
389
- const result = schema.safeParse(args.value);
690
+ const result = v.safeParse(schema, args.value);
390
691
  if (!result.success)
391
692
  throw new SlotValueShapeError({
392
693
  slotType: args.slotType,
393
694
  slotId: args.slotId,
394
- issues: formatIssues(result.error.issues),
695
+ issues: formatIssues(result.issues),
395
696
  mode: "value"
396
697
  });
397
698
  }
@@ -403,17 +704,20 @@ function validateSlotAppendItem(args) {
403
704
  issues: [`slot type ${args.slotType} does not support append`],
404
705
  mode: "item"
405
706
  });
406
- const result = itemSchemas[args.slotType].safeParse(args.item);
707
+ const schema = itemSchemas[args.slotType], result = v.safeParse(schema, args.item);
407
708
  if (!result.success)
408
709
  throw new SlotValueShapeError({
409
710
  slotType: args.slotType,
410
711
  slotId: args.slotId,
411
- issues: formatIssues(result.error.issues),
712
+ issues: formatIssues(result.issues),
412
713
  mode: "item"
413
714
  });
414
715
  }
415
716
  function formatIssues(issues) {
416
- return issues.map((i) => `${i.path.length > 0 ? `at ${i.path.join(".")}: ` : ""}${i.message}`);
717
+ return issues.map((i) => {
718
+ const keys = i.path?.map((p) => p.key) ?? [];
719
+ return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
720
+ });
417
721
  }
418
722
  function derivePerspectiveFromState(slots) {
419
723
  if (slots !== void 0) {
@@ -458,7 +762,7 @@ async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
458
762
  state: stateMapFromResolved(ctx.resolvedState ?? []),
459
763
  stage: ctx.stageId ?? null,
460
764
  task: ctx.taskId ?? null,
461
- now: (/* @__PURE__ */ new Date()).toISOString(),
765
+ now: ctx.now,
462
766
  engineTags: ctx.tags ?? []
463
767
  });
464
768
  try {
@@ -475,7 +779,7 @@ async function resolveSlotValue(slot, initialState, ctx, defaultValue) {
475
779
  const source = slot.source;
476
780
  return source.kind === "init" ? resolveInitValue(slot, initialState, defaultValue) : source.kind === "literal" ? source.value ?? defaultValue : source.kind === "stateRead" ? resolveStateReadValue(source, ctx, defaultValue) : source.kind === "query" && ctx.client !== void 0 ? resolveQueryValue(slot, source, ctx, ctx.client, defaultValue) : defaultValue;
477
781
  }
478
- function buildResolvedSlot(slot, value, _key) {
782
+ function buildResolvedSlot(slot, value, _key, now) {
479
783
  const titleProp = slot.title !== void 0 ? { title: slot.title } : {}, descriptionProp = slot.description !== void 0 ? { description: slot.description } : {};
480
784
  return slot.type === "workflow.state.query" ? {
481
785
  _key,
@@ -484,7 +788,7 @@ function buildResolvedSlot(slot, value, _key) {
484
788
  ...titleProp,
485
789
  ...descriptionProp,
486
790
  value,
487
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
791
+ resolvedAt: now
488
792
  } : {
489
793
  _key,
490
794
  _type: slot.type,
@@ -496,7 +800,7 @@ function buildResolvedSlot(slot, value, _key) {
496
800
  }
497
801
  async function resolveOneSlot(slot, initialState, ctx, randomKey2) {
498
802
  const defaultValue = defaultSlotValue(slot.type), value = await resolveSlotValue(slot, initialState, ctx, defaultValue);
499
- return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2());
803
+ return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2(), ctx.now);
500
804
  }
501
805
  function stateMapFromResolved(slots) {
502
806
  const out = {};
@@ -510,10 +814,10 @@ function assertInitValueShape(slot, value) {
510
814
  }
511
815
  if (slot.type === "workflow.state.release.ref") {
512
816
  assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
513
- const v = value;
514
- if (typeof v.releaseName != "string" || v.releaseName.length === 0)
817
+ const v2 = value;
818
+ if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
515
819
  throw new Error(
516
- `Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v.releaseName)}.`
820
+ `Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
517
821
  );
518
822
  return;
519
823
  }
@@ -531,18 +835,18 @@ function assertGdrShape(value, context) {
531
835
  throw new Error(
532
836
  `Invalid GDR for ${context}: expected { id: "<scheme>:...", type: "<schema>" }, got ${typeof value}.`
533
837
  );
534
- const v = value;
535
- if (typeof v.id != "string" || !isGdrUri(v.id))
838
+ const v2 = value;
839
+ if (typeof v2.id != "string" || !isGdrUri(v2.id))
536
840
  throw new Error(
537
- `Invalid GDR for ${context}: \`id\` must be a GDR URI ("<scheme>:<...id-parts>" with scheme dataset|canvas|media-library|dashboard). Got ${JSON.stringify(v.id)}. Construct via \`gdrFromResource\` / \`refDataset\` / \`refCanvas\` etc. \u2014 bare document ids are not accepted.`
841
+ `Invalid GDR for ${context}: \`id\` must be a GDR URI ("<scheme>:<...id-parts>" with scheme dataset|canvas|media-library|dashboard). Got ${JSON.stringify(v2.id)}. Construct via \`gdrFromResource\` / \`refDataset\` / \`refCanvas\` etc. \u2014 bare document ids are not accepted.`
538
842
  );
539
- if (typeof v.type != "string" || v.type.length === 0)
843
+ if (typeof v2.type != "string" || v2.type.length === 0)
540
844
  throw new Error(
541
- `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v.type)}.`
845
+ `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
542
846
  );
543
847
  }
544
848
  function normalizeQueryResult(slotType, raw, workflowResource) {
545
- return raw == null ? raw : slotType === "workflow.state.doc.ref" ? coerceToGdr(raw, workflowResource) : slotType === "workflow.state.doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v) => v !== null) : [] : raw;
849
+ return raw == null ? raw : slotType === "workflow.state.doc.ref" ? coerceToGdr(raw, workflowResource) : slotType === "workflow.state.doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v2) => v2 !== null) : [] : raw;
546
850
  }
547
851
  function toGdrUri(docId, workflowResource) {
548
852
  return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
@@ -567,25 +871,32 @@ async function loadContext(client, instanceId, options) {
567
871
  const instance = await client.getDocument(instanceId);
568
872
  if (!instance)
569
873
  throw new Error(`Workflow instance ${instanceId} not found`);
570
- const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition });
571
- return { client, clientForGdr, instance, definition, snapshot };
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
+ });
880
+ return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
572
881
  }
573
882
  function ctxEvaluateFilter(ctx, filter) {
574
883
  return evaluateFilter({
575
884
  filter,
576
885
  definition: ctx.definition,
577
886
  snapshot: ctx.snapshot,
578
- params: buildParams(ctx.instance)
887
+ params: buildParams(ctx.instance, ctx.now)
579
888
  });
580
889
  }
581
890
  async function buildEngineContext(args) {
582
- const { client, clientForGdr, instance, definition } = args;
891
+ const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
583
892
  return {
584
893
  client,
585
894
  clientForGdr,
895
+ clock,
896
+ now: clock(),
586
897
  instance,
587
898
  definition,
588
- snapshot: await hydrateSnapshot({ client, clientForGdr, instance, definition })
899
+ snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
589
900
  };
590
901
  }
591
902
  function findStage(definition, stageId) {
@@ -595,12 +906,13 @@ function findStage(definition, stageId) {
595
906
  return stage;
596
907
  }
597
908
  async function resolveStageStateSlots(args) {
598
- const { client, instance, stage, initialState } = args;
909
+ const { client, instance, stage, now, initialState } = args;
599
910
  return resolveDeclaredState({
600
911
  slotDefs: stage.state,
601
912
  initialState: initialState ?? [],
602
913
  ctx: {
603
914
  client,
915
+ now,
604
916
  selfId: instance._id,
605
917
  tags: instance.tags ?? [],
606
918
  stageId: stage.id,
@@ -615,12 +927,13 @@ async function resolveStageStateSlots(args) {
615
927
  });
616
928
  }
617
929
  async function resolveTaskStateSlots(args) {
618
- const { client, instance, stage, task } = args;
930
+ const { client, instance, stage, task, now } = args;
619
931
  return resolveDeclaredState({
620
932
  slotDefs: task.state,
621
933
  initialState: [],
622
934
  ctx: {
623
935
  client,
936
+ now,
624
937
  selfId: instance._id,
625
938
  tags: instance.tags ?? [],
626
939
  stageId: stage.id,
@@ -632,73 +945,6 @@ async function resolveTaskStateSlots(args) {
632
945
  randomKey
633
946
  });
634
947
  }
635
- function readStateSlot(instance, scope, slotId, stageId, taskId) {
636
- if (scope === "workflow")
637
- return slotValue$1(instance.state?.find((s) => s.id === slotId));
638
- const stageEntry = instance.stages.find(
639
- (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
640
- );
641
- if (stageEntry === void 0) return;
642
- if (scope === "stage")
643
- return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
644
- const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
645
- return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
646
- }
647
- function slotValue$1(slot) {
648
- if (slot != null && typeof slot == "object")
649
- return slot.value;
650
- }
651
- function resolveSource(src, ctx) {
652
- switch (src.source) {
653
- case "literal":
654
- return src.value;
655
- case "param":
656
- return ctx.params?.[src.paramId];
657
- case "actor":
658
- return ctx.actor;
659
- case "now":
660
- return (/* @__PURE__ */ new Date()).toISOString();
661
- case "self":
662
- return ctx.instance._id;
663
- case "stageId":
664
- return ctx.stageId ?? ctx.instance.currentStageId;
665
- case "stateRead":
666
- return resolveStateRead(src, ctx);
667
- case "effectOutput":
668
- return resolveEffectOutput(src, ctx);
669
- case "object":
670
- return resolveObject(src, ctx);
671
- }
672
- }
673
- function resolveStateRead(src, ctx) {
674
- const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
675
- return src.path !== void 0 ? getPath(value, src.path) : value;
676
- }
677
- function resolveEffectOutput(src, ctx) {
678
- const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
679
- if (entry === void 0) return null;
680
- const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
681
- return value === void 0 ? null : value;
682
- }
683
- function resolveObject(src, ctx) {
684
- const out = {};
685
- for (const [field, fieldSrc] of Object.entries(src.fields))
686
- out[field] = resolveSource(fieldSrc, ctx);
687
- return out;
688
- }
689
- async function resolveBindings(args) {
690
- const { bindings, staticInput, instance, params, actor } = args, resolved = {};
691
- if (bindings) {
692
- const ctx = {
693
- instance,
694
- ...params !== void 0 ? { params } : {},
695
- ...actor !== void 0 ? { actor } : {}
696
- };
697
- for (const [key, src] of Object.entries(bindings))
698
- resolved[key] = resolveSource(src, ctx);
699
- }
700
- return { ...resolved, ...staticInput };
701
- }
702
948
  function effectsContextEntry(id, value) {
703
949
  const _key = randomKey();
704
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 };
@@ -707,7 +953,7 @@ function buildInstanceBase(args) {
707
953
  const { id, now, actor, perspective } = args;
708
954
  return {
709
955
  _id: id,
710
- _type: "workflow.instance",
956
+ _type: WORKFLOW_INSTANCE_TYPE,
711
957
  _rev: "",
712
958
  _createdAt: now,
713
959
  _updatedAt: now,
@@ -737,175 +983,337 @@ function buildInstanceBase(args) {
737
983
  lastChangedAt: now
738
984
  };
739
985
  }
740
- function lakeGuardId(args) {
741
- return `temp.system.guard.${args.instanceDocId}.${args.stageId}.${args.index}`;
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
+ };
742
1008
  }
743
- function compileGuard(args) {
1009
+ function taskStatusChangedEntry(args) {
744
1010
  return {
745
- _id: args.id,
746
- _type: "temp.system.guard",
747
- resourceType: args.resourceType,
748
- resourceId: args.resourceId,
749
- owner: args.owner,
750
- ...args.name !== void 0 ? { name: args.name } : {},
751
- ...args.description !== void 0 ? { description: args.description } : {},
752
- match: args.match,
753
- predicate: args.predicate,
754
- metadata: args.metadata
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 } : {}
755
1019
  };
756
1020
  }
757
- function toGroqJsPredicate(predicate) {
758
- return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
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
+ ];
759
1052
  }
760
- function globMatch(pattern, value) {
761
- if (!pattern.includes("*")) return pattern === value;
762
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
763
- return new RegExp(`^${escaped}$`).test(value);
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;
764
1064
  }
765
- function guardMatches(guard, doc, action) {
766
- const m = guard.match;
767
- if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
768
- return !1;
769
- if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
770
- const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
771
- if (!byRef && !byPattern) return !1;
772
- }
773
- return !0;
1065
+ function materializeInstance(base, mutation) {
1066
+ return {
1067
+ ...base,
1068
+ currentStageId: mutation.currentStageId,
1069
+ state: mutation.state,
1070
+ stages: mutation.stages
1071
+ };
774
1072
  }
775
- async function evaluateMutationGuard(args) {
776
- const { guard, context } = args;
777
- if (guard.predicate === "") return !1;
778
- const params = { guard, mutation: { action: context.action } };
779
- try {
780
- const tree = parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
781
- return await (await evaluate(tree, {
782
- before: context.before,
783
- after: context.after,
784
- params,
785
- ...context.identity !== void 0 ? { identity: context.identity } : {}
786
- })).get() === !0;
787
- } catch {
788
- return !1;
789
- }
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;
790
1090
  }
791
- async function denyingGuards(args) {
792
- const { guards, doc, context } = args, denied = [];
793
- for (const guard of guards)
794
- guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
795
- return denied;
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;
796
1098
  }
797
- function resourceOf(p) {
798
- return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
1099
+ function currentTasks(mutation) {
1100
+ return currentStageEntry(mutation).tasks;
799
1101
  }
800
- function resolveIdRefTarget(src, ctx) {
801
- const value = resolveSource(src, ctx);
802
- if (typeof value == "string" && isGdrUri(value))
803
- return { parsed: parseGdr(value) };
804
- if (value && typeof value == "object") {
805
- const v = value;
806
- if (typeof v.id == "string" && isGdrUri(v.id))
807
- return typeof v.type == "string" ? { parsed: parseGdr(v.id), type: v.type } : { parsed: parseGdr(v.id) };
808
- }
809
- return null;
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 };
810
1109
  }
811
- function resolveIdRefTargets(idRefs, ctx) {
812
- const targets = [];
813
- for (const src of idRefs ?? []) {
814
- const target = resolveIdRefTarget(src, ctx);
815
- if (target === null) return null;
816
- targets.push(target);
817
- }
818
- return targets.length === 0 ? null : targets;
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;
819
1115
  }
820
- function assertSingleResource(targets) {
821
- const resource = resourceOf(targets[0].parsed);
822
- for (const g of targets) {
823
- const r = resourceOf(g.parsed);
824
- if (r.type !== resource.type || r.id !== resource.id)
825
- throw new Error(
826
- `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
827
- );
828
- }
829
- return resource;
1116
+ function findCurrentStageEntry(instance) {
1117
+ return findOpenStageEntry(instance);
830
1118
  }
831
- function bareIdRefs(targets) {
832
- return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
1119
+ function findCurrentTasks(instance) {
1120
+ return findCurrentStageEntry(instance)?.tasks ?? [];
833
1121
  }
834
- function resolveMatchTypes(targets, authorTypes) {
835
- if (authorTypes !== void 0) return authorTypes;
836
- const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
837
- return inferred.length > 0 ? inferred : void 0;
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
+ );
838
1136
  }
839
- function resolveMetadata(metadata, ctx) {
840
- const out = {};
841
- for (const [k, src] of Object.entries(metadata ?? {}))
842
- out[k] = resolveSource(src, ctx);
843
- return out;
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
+ };
844
1154
  }
845
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
846
- function resolveGuard(guard, index, instance, stageId) {
847
- const ctx = { instance, stageId }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
848
- if (targets === null) return null;
849
- const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
850
- return { doc: compileGuard({
851
- id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
852
- resourceType: resource.type,
853
- resourceId: resource.id,
854
- owner: GUARD_OWNER,
855
- ...guard.name !== void 0 ? { name: guard.name } : {},
856
- ...guard.description !== void 0 ? { description: guard.description } : {},
857
- match: {
858
- ...types !== void 0 ? { types } : {},
859
- idRefs: bareIdRefs(targets),
860
- ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
861
- actions: guard.match.actions
862
- },
863
- predicate: guard.predicate ?? "",
864
- metadata: resolveMetadata(guard.metadata, ctx)
865
- }), routeGdr: targets[0].parsed };
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);
1159
+ }
866
1160
  }
867
- async function upsertGuard(client, doc) {
868
- const existing = await client.getDocument(doc._id), set = {
869
- resourceType: doc.resourceType,
870
- resourceId: doc.resourceId,
871
- owner: doc.owner,
872
- match: doc.match,
873
- predicate: doc.predicate,
874
- metadata: doc.metadata
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 };
1181
+ }
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
875
1201
  };
876
- doc.name !== void 0 && (set.name = doc.name), doc.description !== void 0 && (set.description = doc.description), existing ? await client.patch(doc._id).set(set).commit() : await client.create(doc);
1202
+ return { pending, history };
877
1203
  }
878
- function resolvedStageGuards(args) {
879
- const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
880
- for (const [index, guard] of (stage?.guards ?? []).entries()) {
881
- const resolved = resolveGuard(guard, index, args.instance, args.stageId);
882
- resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
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);
883
1222
  }
884
- return out;
885
1223
  }
886
- async function deployStageGuards(args) {
887
- for (const { client, doc } of resolvedStageGuards(args))
888
- await upsertGuard(client, doc);
1224
+ class ActionParamsInvalidError extends Error {
1225
+ action;
1226
+ taskId;
1227
+ issues;
1228
+ constructor(args) {
1229
+ const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1230
+ `);
1231
+ super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
1232
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
1233
+ }
889
1234
  }
890
- async function retractStageGuards(args) {
891
- for (const { client, doc } of resolvedStageGuards(args))
892
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
1235
+ class WorkflowStateDivergedError extends Error {
1236
+ instanceId;
1237
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
1238
+ guardError;
1239
+ /** The rollback failure, when rollback was attempted and itself failed. */
1240
+ rollbackError;
1241
+ constructor(args) {
1242
+ super(
1243
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1244
+ { cause: args.guardError }
1245
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1246
+ }
893
1247
  }
894
- async function reconcileStageGuards(args) {
895
- const { client, clientForGdr, instance, definition, exitedStageId, enteredStageId } = args;
896
- exitedStageId !== void 0 && await retractStageGuards({
897
- client,
898
- clientForGdr,
899
- instance,
900
- definition,
901
- stageId: exitedStageId
902
- }), enteredStageId !== void 0 && await deployStageGuards({
903
- client,
904
- clientForGdr,
905
- instance,
906
- definition,
907
- stageId: enteredStageId
908
- });
1248
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1249
+ class ConcurrentFireActionError extends Error {
1250
+ instanceId;
1251
+ taskId;
1252
+ action;
1253
+ attempts;
1254
+ constructor(args) {
1255
+ super(
1256
+ `Action "${args.action}" on task "${args.taskId}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
1257
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.taskId = args.taskId, this.action = args.action, this.attempts = args.attempts;
1258
+ }
1259
+ }
1260
+ function isRevisionConflict(error) {
1261
+ if (typeof error != "object" || error === null) return !1;
1262
+ const { statusCode, message } = error;
1263
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1264
+ }
1265
+ class CascadeLimitError extends Error {
1266
+ instanceId;
1267
+ limit;
1268
+ constructor(args) {
1269
+ super(
1270
+ `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
1271
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1272
+ }
1273
+ }
1274
+ async function deployOrRollback(args) {
1275
+ try {
1276
+ await args.deploy();
1277
+ } catch (guardError) {
1278
+ if (!args.reversible)
1279
+ throw new WorkflowStateDivergedError({
1280
+ instanceId: args.instanceId,
1281
+ guardError,
1282
+ reason: "the move created child instances a parent-only rollback cannot undo"
1283
+ });
1284
+ try {
1285
+ let patch = args.client.patch(args.instanceId).set(args.restore);
1286
+ args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1287
+ } catch (rollbackError) {
1288
+ throw new WorkflowStateDivergedError({
1289
+ instanceId: args.instanceId,
1290
+ guardError,
1291
+ rollbackError,
1292
+ reason: "rollback of the committed move failed"
1293
+ });
1294
+ }
1295
+ throw guardError;
1296
+ }
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]`;
909
1317
  }
910
1318
  async function discoverItems(args) {
911
1319
  const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
@@ -922,31 +1330,29 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
922
1330
  const MAX_SPAWN_DEPTH = 6;
923
1331
  async function spawnChildren(ctx, mutation, task, spawn, actor) {
924
1332
  if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
925
- const chain = [
926
- ...ctx.instance.ancestors.map((a) => a.id),
927
- gdrFromResource(ctx.instance.workflowResource, ctx.instance._id)
928
- ].join(" \u2192 ");
1333
+ const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
929
1334
  throw new Error(
930
1335
  `Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
931
1336
  );
932
1337
  }
933
- const rows = await resolveSpawnRows(ctx, spawn), refs = [];
1338
+ const now = ctx.now, rows = await resolveSpawnRows(ctx, spawn), refs = [];
934
1339
  for (const row of rows) {
935
- const projected = projectSpawnRow(ctx.instance, spawn, row), { ref, body } = await prepareChildInstance({
1340
+ const projected = projectSpawnRow(ctx.instance, spawn, row, now), { ref, body } = await prepareChildInstance({
936
1341
  client: ctx.client,
937
1342
  parent: ctx.instance,
938
1343
  task,
939
1344
  spawn,
940
1345
  initialState: projected.initialState,
941
1346
  effectsContext: projected.effectsContext,
942
- actor
1347
+ actor,
1348
+ now
943
1349
  });
944
1350
  refs.push(ref), mutation.pendingCreates.push(body);
945
1351
  }
946
1352
  return refs;
947
1353
  }
948
1354
  async function resolveSpawnRows(ctx, spawn) {
949
- const params = buildParams(ctx.instance), groq = spawn.forEach.groq;
1355
+ const params = buildParams(ctx.instance, ctx.now), groq = spawn.forEach.groq;
950
1356
  let value;
951
1357
  if (groq.includes("*")) {
952
1358
  const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
@@ -968,7 +1374,7 @@ function firstDocRefGdrUri(slots) {
968
1374
  if (s._type === "workflow.state.doc.ref" && s.value !== null) return s.value.id;
969
1375
  }
970
1376
  }
971
- function resolveSpawnSource(source, parent, row) {
1377
+ function resolveSpawnSource(source, parent, row, now) {
972
1378
  switch (source.source) {
973
1379
  case "literal":
974
1380
  return source.value;
@@ -979,15 +1385,15 @@ function resolveSpawnSource(source, parent, row) {
979
1385
  return slot === void 0 ? null : source.path !== void 0 ? getPath(slot, source.path) : slot;
980
1386
  }
981
1387
  case "self":
982
- return gdrFromResource(parent.workflowResource, parent._id);
1388
+ return selfGdr(parent);
983
1389
  case "stageId":
984
1390
  return parent.currentStageId;
985
1391
  case "now":
986
- return (/* @__PURE__ */ new Date()).toISOString();
1392
+ return now;
987
1393
  case "object": {
988
1394
  const out = {};
989
- for (const [k, v] of Object.entries(source.fields))
990
- out[k] = resolveSpawnSource(v, parent, row);
1395
+ for (const [k, v2] of Object.entries(source.fields))
1396
+ out[k] = resolveSpawnSource(v2, parent, row, now);
991
1397
  return out;
992
1398
  }
993
1399
  case "actor":
@@ -997,10 +1403,10 @@ function resolveSpawnSource(source, parent, row) {
997
1403
  return null;
998
1404
  }
999
1405
  }
1000
- function projectSpawnRow(parent, spawn, row) {
1406
+ function projectSpawnRow(parent, spawn, row, now) {
1001
1407
  const initialState = [];
1002
1408
  for (const entry of spawn.forEach.as.initialState ?? []) {
1003
- const raw = resolveSpawnSource(entry.value, parent, row), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1409
+ const raw = resolveSpawnSource(entry.value, parent, row, now), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1004
1410
  initialState.push({
1005
1411
  type: entry.type,
1006
1412
  id: entry.id,
@@ -1009,13 +1415,13 @@ function projectSpawnRow(parent, spawn, row) {
1009
1415
  }
1010
1416
  const effectsContext = {};
1011
1417
  for (const [k, src] of Object.entries(spawn.forEach.as.effectsContext ?? {})) {
1012
- const v = resolveSpawnSource(src, parent, row);
1013
- v != null && (typeof v == "string" || typeof v == "number" || typeof v == "boolean" || typeof v == "object" && "id" in v && "type" in v) && (effectsContext[k] = v);
1418
+ const v2 = resolveSpawnSource(src, parent, row, now);
1419
+ v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (effectsContext[k] = v2);
1014
1420
  }
1015
1421
  return { initialState, effectsContext };
1016
1422
  }
1017
1423
  function canonicaliseSpawnValue(slotType, value, workflowResource) {
1018
- return slotType === "workflow.state.doc.ref" ? coerceSpawnGdr(value, workflowResource) : slotType === "workflow.state.doc.refs" && Array.isArray(value) ? value.map((v) => coerceSpawnGdr(v, workflowResource)).filter((v) => v !== null) : value;
1424
+ return slotType === "workflow.state.doc.ref" ? coerceSpawnGdr(value, workflowResource) : slotType === "workflow.state.doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
1019
1425
  }
1020
1426
  function coerceSpawnGdr(raw, workflowResource) {
1021
1427
  const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
@@ -1032,30 +1438,32 @@ function coerceSpawnGdr(raw, workflowResource) {
1032
1438
  return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1033
1439
  }
1034
1440
  async function resolveLogicalDefinition(client, ref, tags) {
1035
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
1036
- wantsExplicit && (params.version = ref.version);
1037
- 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]';
1038
- 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;
1039
1446
  }
1040
1447
  async function prepareChildInstance(args) {
1041
- const { client, parent, task, spawn, initialState, effectsContext, actor } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1448
+ const { client, parent, task, spawn, initialState, effectsContext, actor, now } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1042
1449
  if (definition === null) {
1043
1450
  const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
1044
1451
  throw new Error(
1045
1452
  `Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
1046
1453
  );
1047
1454
  }
1048
- 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 = [
1049
1456
  ...parent.ancestors,
1050
1457
  {
1051
- id: gdrFromResource(parent.workflowResource, parent._id),
1052
- type: "workflow.instance"
1458
+ id: selfGdr(parent),
1459
+ type: WORKFLOW_INSTANCE_TYPE
1053
1460
  }
1054
1461
  ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1055
1462
  slotDefs: definition.state,
1056
1463
  initialState,
1057
1464
  ctx: {
1058
1465
  client,
1466
+ now,
1059
1467
  selfId: childDocId,
1060
1468
  tags: childTags,
1061
1469
  workflowResource,
@@ -1075,7 +1483,7 @@ async function prepareChildInstance(args) {
1075
1483
  }
1076
1484
  ), base = buildInstanceBase({
1077
1485
  id: childDocId,
1078
- now: (/* @__PURE__ */ new Date()).toISOString(),
1486
+ now,
1079
1487
  tags: childTags,
1080
1488
  workflowResource,
1081
1489
  workflowId: definition.workflowId,
@@ -1111,7 +1519,9 @@ async function checkSpawnCompletion(client, entry, spawn) {
1111
1519
  }
1112
1520
  }
1113
1521
  async function invokeTask(args) {
1114
- const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId);
1522
+ const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId, {
1523
+ ...options?.clock ? { clock: options.clock } : {}
1524
+ });
1115
1525
  return commitInvoke(ctx, taskId, options?.actor);
1116
1526
  }
1117
1527
  async function commitInvoke(ctx, taskId, actor) {
@@ -1123,41 +1533,41 @@ async function commitInvoke(ctx, taskId, actor) {
1123
1533
  const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1124
1534
  return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, taskId };
1125
1535
  }
1126
- async function autoResolveOnActivate(ctx, mutation, task, entry) {
1536
+ async function autoResolveOnActivate(ctx, mutation, task, entry, now) {
1127
1537
  const checks = [
1128
1538
  { filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
1129
1539
  { filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
1130
1540
  ];
1131
- for (const { filter, outcome, systemId } of checks) {
1132
- if (filter === void 0 || !await ctxEvaluateFilter(ctx, filter)) continue;
1133
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1134
- return entry.status = outcome, entry.completedAt = completedAt, entry.completedBy = systemId, mutation.history.push(
1135
- taskStatusChangedEntry({
1136
- stageId: mutation.currentStageId,
1137
- taskId: task.id,
1138
- from: "active",
1139
- to: outcome,
1140
- actor: { kind: "system", id: systemId },
1141
- at: completedAt
1142
- })
1143
- ), !0;
1144
- }
1541
+ for (const { filter, outcome, systemId } of checks)
1542
+ if (filter !== void 0 && await ctxEvaluateFilter(ctx, filter))
1543
+ return entry.status = outcome, entry.completedAt = now, entry.completedBy = systemId, mutation.history.push(
1544
+ taskStatusChangedEntry({
1545
+ stageId: mutation.currentStageId,
1546
+ taskId: task.id,
1547
+ from: "active",
1548
+ to: outcome,
1549
+ at: now,
1550
+ actor: { kind: "system", id: systemId }
1551
+ })
1552
+ ), !0;
1145
1553
  return !1;
1146
1554
  }
1147
1555
  async function activateTask(ctx, mutation, task, entry, actor) {
1148
- if (entry.status = "active", entry.startedAt = (/* @__PURE__ */ new Date()).toISOString(), task.state !== void 0 && task.state.length > 0) {
1556
+ const now = ctx.now;
1557
+ if (entry.status = "active", entry.startedAt = now, task.state !== void 0 && task.state.length > 0) {
1149
1558
  const stage = findStage(ctx.definition, mutation.currentStageId);
1150
1559
  entry.state = await resolveTaskStateSlots({
1151
1560
  client: ctx.client,
1152
1561
  instance: ctx.instance,
1153
1562
  stage,
1154
- task
1563
+ task,
1564
+ now
1155
1565
  });
1156
1566
  }
1157
- if (!await autoResolveOnActivate(ctx, mutation, task, entry) && (mutation.history.push({
1567
+ if (!await autoResolveOnActivate(ctx, mutation, task, entry, now) && (mutation.history.push({
1158
1568
  _key: randomKey(),
1159
1569
  _type: "workflow.history.taskActivated",
1160
- at: (/* @__PURE__ */ new Date()).toISOString(),
1570
+ at: now,
1161
1571
  stageId: mutation.currentStageId,
1162
1572
  taskId: task.id,
1163
1573
  ...actor !== void 0 ? { actor } : {}
@@ -1177,18 +1587,19 @@ async function activateTask(ctx, mutation, task, entry, actor) {
1177
1587
  mutation.history.push({
1178
1588
  _key: randomKey(),
1179
1589
  _type: "workflow.history.spawned",
1180
- at: (/* @__PURE__ */ new Date()).toISOString(),
1590
+ at: now,
1181
1591
  taskId: task.id,
1182
1592
  instanceRef: ref
1183
1593
  });
1184
1594
  if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
1185
1595
  const previousStatus = entry.status;
1186
- entry.status = "done", entry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1596
+ entry.status = "done", entry.completedAt = now, mutation.history.push(
1187
1597
  taskStatusChangedEntry({
1188
1598
  stageId: mutation.currentStageId,
1189
1599
  taskId: task.id,
1190
1600
  from: previousStatus,
1191
1601
  to: "done",
1602
+ at: now,
1192
1603
  ...actor !== void 0 ? { actor } : {}
1193
1604
  })
1194
1605
  );
@@ -1205,7 +1616,7 @@ async function buildStageTasks(ctx, stage) {
1205
1616
  status: inScope ? "pending" : "skipped",
1206
1617
  ...task.filter !== void 0 ? {
1207
1618
  filterEvaluation: {
1208
- at: (/* @__PURE__ */ new Date()).toISOString(),
1619
+ at: ctx.now,
1209
1620
  truthy: inScope
1210
1621
  }
1211
1622
  } : {}
@@ -1217,7 +1628,9 @@ function activatesOnStageEnter(task) {
1217
1628
  return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
1218
1629
  }
1219
1630
  async function setStage(args) {
1220
- const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId);
1631
+ const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId, {
1632
+ ...options?.clock ? { clock: options.clock } : {}
1633
+ });
1221
1634
  return commitSetStage(ctx, targetStageId, reason, options?.actor);
1222
1635
  }
1223
1636
  async function enterStage(ctx, mutation, nextStage, actor, at) {
@@ -1229,7 +1642,8 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1229
1642
  state: await resolveStageStateSlots({
1230
1643
  client: ctx.client,
1231
1644
  instance: ctx.instance,
1232
- stage: nextStage
1645
+ stage: nextStage,
1646
+ now: at
1233
1647
  }),
1234
1648
  tasks: await buildStageTasks(ctx, nextStage)
1235
1649
  };
@@ -1248,7 +1662,7 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1248
1662
  nextStage.kind === "terminal" && (mutation.completedAt = at);
1249
1663
  }
1250
1664
  async function beginStageMove(ctx, currentStage, actor) {
1251
- const mutation = startMutation(ctx.instance), at = (/* @__PURE__ */ new Date()).toISOString();
1665
+ const mutation = startMutation(ctx.instance), at = ctx.now;
1252
1666
  return await queueEffects(
1253
1667
  ctx,
1254
1668
  mutation,
@@ -1282,13 +1696,46 @@ async function commitSetStage(ctx, targetStageId, reason, actor) {
1282
1696
  };
1283
1697
  }
1284
1698
  async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1285
- await persist(ctx, mutation), await reconcileStageGuards({
1699
+ await persistThenDeploy(
1700
+ ctx,
1701
+ mutation,
1702
+ () => deployStageGuards({
1703
+ client: ctx.client,
1704
+ clientForGdr: ctx.clientForGdr,
1705
+ instance: materializeInstance(ctx.instance, mutation),
1706
+ definition: ctx.definition,
1707
+ stageId: enteredStageId,
1708
+ now: ctx.now
1709
+ })
1710
+ ), await retractStageGuards({
1286
1711
  client: ctx.client,
1287
1712
  clientForGdr: ctx.clientForGdr,
1288
1713
  instance: ctx.instance,
1289
1714
  definition: ctx.definition,
1290
- exitedStageId,
1291
- enteredStageId
1715
+ stageId: exitedStageId,
1716
+ now: ctx.now
1717
+ });
1718
+ }
1719
+ async function persistThenDeploy(ctx, mutation, deploy) {
1720
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1721
+ await deployOrRollback({
1722
+ client: ctx.client,
1723
+ instanceId: ctx.instance._id,
1724
+ committedRev: committed._rev,
1725
+ restore: instanceStateFields(ctx.instance),
1726
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1727
+ reversible: !spawned,
1728
+ deploy
1729
+ });
1730
+ }
1731
+ async function refreshStageGuards(ctx, mutation, stageId) {
1732
+ await deployStageGuards({
1733
+ client: ctx.client,
1734
+ clientForGdr: ctx.clientForGdr,
1735
+ instance: materializeInstance(ctx.instance, mutation),
1736
+ definition: ctx.definition,
1737
+ stageId,
1738
+ now: ctx.now
1292
1739
  });
1293
1740
  }
1294
1741
  async function commitTransition(ctx, action, actor) {
@@ -1330,26 +1777,29 @@ async function pickTransition(ctx, stage, action) {
1330
1777
  if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
1331
1778
  }
1332
1779
  async function evaluateAutoTransitions(args) {
1333
- const { client, instanceId, options, clientForGdr } = args, ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0);
1780
+ const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
1781
+ ...clientForGdr ? { clientForGdr } : {},
1782
+ ...clock ? { clock } : {},
1783
+ ...overlay ? { overlay } : {}
1784
+ });
1334
1785
  return commitTransition(ctx, void 0, options?.actor);
1335
1786
  }
1336
- async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1787
+ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
1337
1788
  const instance = await client.getDocument(instanceId);
1338
1789
  if (!instance || instance.stages.length > 0 || instance.pendingEffects.length > 0) return;
1339
1790
  const definition = JSON.parse(instance.definitionSnapshot), stage = definition.stages.find((s) => s.id === instance.currentStageId);
1340
1791
  if (stage === void 0) return;
1341
- const snapshot = await hydrateSnapshot({
1792
+ const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
1342
1793
  client,
1343
1794
  clientForGdr: clientForGdr ?? (() => client),
1344
- instance,
1345
- definition
1795
+ instance
1346
1796
  }), tasks = [];
1347
1797
  for (const task of stage.tasks ?? []) {
1348
1798
  const inScope = await evaluateFilter({
1349
1799
  filter: task.filter,
1350
1800
  definition,
1351
1801
  snapshot,
1352
- params: buildParams(instance)
1802
+ params: buildParams(instance, now)
1353
1803
  });
1354
1804
  tasks.push({
1355
1805
  _key: randomKey(),
@@ -1360,8 +1810,8 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1360
1810
  const initialStageEntry = {
1361
1811
  _key: randomKey(),
1362
1812
  id: stage.id,
1363
- enteredAt: (/* @__PURE__ */ new Date()).toISOString(),
1364
- state: await resolveStageStateSlots({ client, instance, stage }),
1813
+ enteredAt: now,
1814
+ state: await resolveStageStateSlots({ client, instance, stage, now }),
1365
1815
  tasks
1366
1816
  }, pendingEffects = [], newHistory = [...instance.history];
1367
1817
  for (const effect of stage.onEnter ?? []) {
@@ -1369,29 +1819,44 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1369
1819
  bindings: effect.bindings,
1370
1820
  staticInput: effect.input,
1371
1821
  instance,
1822
+ now,
1372
1823
  ...actor !== void 0 ? { actor } : {}
1373
1824
  }), { pending, history } = buildQueuedEffect(
1374
1825
  effect,
1375
1826
  { kind: "stageEnter", id: stage.id },
1376
1827
  params,
1377
- actor
1828
+ actor,
1829
+ now
1378
1830
  );
1379
1831
  pendingEffects.push(pending), newHistory.push(history);
1380
1832
  }
1381
- await client.patch(instance._id).set({
1833
+ const committed = await client.patch(instance._id).set({
1382
1834
  stages: [initialStageEntry],
1383
1835
  pendingEffects,
1384
1836
  history: newHistory,
1385
- lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1386
- }).ifRevisionId(instance._rev).commit(), await deployStageGuards({
1837
+ lastChangedAt: now
1838
+ }).ifRevisionId(instance._rev).commit();
1839
+ await deployOrRollback({
1387
1840
  client,
1388
- clientForGdr: clientForGdr ?? (() => client),
1389
- instance,
1390
- definition,
1391
- stageId: stage.id
1392
- }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr);
1841
+ instanceId: instance._id,
1842
+ committedRev: committed._rev,
1843
+ restore: {
1844
+ stages: instance.stages,
1845
+ pendingEffects: instance.pendingEffects,
1846
+ history: instance.history
1847
+ },
1848
+ reversible: !0,
1849
+ deploy: () => deployStageGuards({
1850
+ client,
1851
+ clientForGdr: clientForGdr ?? (() => client),
1852
+ instance,
1853
+ definition,
1854
+ stageId: stage.id,
1855
+ now
1856
+ })
1857
+ }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
1393
1858
  }
1394
- async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr) {
1859
+ async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
1395
1860
  const resolvedClientForGdr = clientForGdr ?? (() => client);
1396
1861
  for (const task of stage.tasks ?? []) {
1397
1862
  if (!activatesOnStageEnter(task)) continue;
@@ -1401,7 +1866,8 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
1401
1866
  client,
1402
1867
  clientForGdr: resolvedClientForGdr,
1403
1868
  instance: updated,
1404
- definition
1869
+ definition,
1870
+ ...clock ? { clock } : {}
1405
1871
  }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1406
1872
  mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
1407
1873
  }
@@ -1419,8 +1885,12 @@ async function gatherAutoResolveCandidates(ctx, tasks) {
1419
1885
  }
1420
1886
  return candidates;
1421
1887
  }
1422
- async function resolveCompleteWhen(client, instanceId, clientForGdr) {
1423
- const ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1888
+ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
1889
+ const ctx = await loadContext(client, instanceId, {
1890
+ ...clientForGdr ? { clientForGdr } : {},
1891
+ ...clock ? { clock } : {},
1892
+ ...overlay ? { overlay } : {}
1893
+ }), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1424
1894
  (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
1425
1895
  );
1426
1896
  if (tasksWithAutoPredicate.length === 0) return 0;
@@ -1434,302 +1904,480 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr) {
1434
1904
  const previousStatus = mutEntry.status, actor = {
1435
1905
  kind: "system",
1436
1906
  id: outcome === "failed" ? "engine.failWhen" : "engine.completeWhen"
1437
- };
1438
- mutEntry.status = outcome, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutEntry.completedBy = actor.id, mutation.history.push(
1907
+ }, now = ctx.now;
1908
+ mutEntry.status = outcome, mutEntry.completedAt = now, mutEntry.completedBy = actor.id, mutation.history.push(
1439
1909
  taskStatusChangedEntry({
1440
1910
  stageId: stage.id,
1441
1911
  taskId: task.id,
1442
1912
  from: previousStatus,
1443
1913
  to: outcome,
1914
+ at: now,
1444
1915
  actor
1445
1916
  })
1446
1917
  ), count++;
1447
1918
  }
1448
1919
  return count > 0 && await persist(ctx, mutation), count;
1449
1920
  }
1450
- async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr) {
1921
+ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock, overlay) {
1451
1922
  let count = 0;
1452
1923
  for (; ; ) {
1453
- if (await resolveCompleteWhen(client, instanceId, clientForGdr), !(await evaluateAutoTransitions({
1924
+ if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay), !(await evaluateAutoTransitions({
1454
1925
  client,
1455
1926
  instanceId,
1456
1927
  ...actor !== void 0 ? { options: { actor } } : {},
1457
- ...clientForGdr ? { clientForGdr } : {}
1928
+ ...clientForGdr ? { clientForGdr } : {},
1929
+ ...clock ? { clock } : {},
1930
+ ...overlay ? { overlay } : {}
1458
1931
  })).fired) return count;
1459
- if (count++, count > CASCADE_LIMIT)
1460
- throw new Error(
1461
- `Cascade did not stabilise after ${CASCADE_LIMIT} auto-transitions on ${instanceId} \u2014 possible infinite loop`
1462
- );
1932
+ if (count++, count >= CASCADE_LIMIT)
1933
+ throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
1463
1934
  }
1464
1935
  }
1465
- async function propagateToAncestors(client, instanceId, actor, clientForGdr) {
1466
- const instance = await client.getDocument(instanceId);
1467
- if (!instance || instance.ancestors.length === 0) return;
1468
- const parentRef = instance.ancestors[instance.ancestors.length - 1];
1936
+ async function findSatisfiedParentSpawn(client, child) {
1937
+ const parentRef = child.ancestors.at(-1);
1469
1938
  if (parentRef === void 0) return;
1470
- const parentId = gdrToBareId(parentRef.id), parent = await client.getDocument(parentId);
1471
- if (!parent || parent._type !== "workflow.instance" || typeof parent.definitionSnapshot != "string")
1472
- return;
1473
- const parentDefinition = JSON.parse(parent.definitionSnapshot), parentStage = parentDefinition.stages.find((s) => s.id === parent.currentStageId);
1474
- if (parentStage === void 0) return;
1475
- const parentCurrentTasks = findCurrentTasks(parent), parentTask = (parentStage.tasks ?? []).find((t) => t.spawns === void 0 ? !1 : parentCurrentTasks.find((e) => e.id === t.id)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === instance._id) ?? !1);
1476
- if (parentTask === void 0) return;
1477
- const parentEntry = parentCurrentTasks.find((e) => e.id === parentTask.id);
1478
- if (parentEntry === void 0 || parentEntry.status === "done" || !await checkSpawnCompletion(client, parentEntry, parentTask.spawns)) return;
1479
- const ctx = await buildEngineContext({
1939
+ const parent = await client.getDocument(gdrToBareId(parentRef.id));
1940
+ if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
1941
+ const definition = JSON.parse(parent.definitionSnapshot), stage = definition.stages.find((s) => s.id === parent.currentStageId);
1942
+ if (stage === void 0) return;
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);
1944
+ if (task?.spawns === void 0) return;
1945
+ const entry = parentCurrentTasks.find((e) => e.id === task.id);
1946
+ if (!(entry === void 0 || entry.status === "done") && await checkSpawnCompletion(client, entry, task.spawns))
1947
+ return { parent, definition, stage, task };
1948
+ }
1949
+ async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
1950
+ const instance = await client.getDocument(instanceId);
1951
+ if (!instance) return;
1952
+ const found = await findSatisfiedParentSpawn(client, instance);
1953
+ if (found === void 0) return;
1954
+ const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
1480
1955
  client,
1481
1956
  clientForGdr: clientForGdr ?? (() => client),
1482
1957
  instance: parent,
1483
- definition: parentDefinition
1484
- }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === parentTask.id);
1958
+ definition,
1959
+ ...clock ? { clock } : {}
1960
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === task.id);
1485
1961
  if (mutEntry === void 0) return;
1486
- const previousStatus = mutEntry.status;
1487
- mutEntry.status = "done", mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1962
+ const previousStatus = mutEntry.status, now = ctx.now;
1963
+ mutEntry.status = "done", mutEntry.completedAt = now, mutation.history.push(
1488
1964
  taskStatusChangedEntry({
1489
- stageId: parentStage.id,
1490
- taskId: parentTask.id,
1965
+ stageId: stage.id,
1966
+ taskId: task.id,
1491
1967
  from: previousStatus,
1492
1968
  to: "done",
1969
+ at: now,
1493
1970
  ...actor !== void 0 ? { actor } : {}
1494
1971
  })
1495
- ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parentId, actor, clientForGdr), await propagateToAncestors(client, parentId, actor, clientForGdr);
1496
- }
1497
- function startMutation(instance) {
1498
- return {
1499
- currentStageId: instance.currentStageId,
1500
- // Deep-ish copy. Per-scope state slot arrays need their own copies
1501
- // so ops can mutate without leaking into the source.
1502
- state: (instance.state ?? []).map((s) => ({ ...s })),
1503
- stages: instance.stages.map((s) => ({
1504
- ...s,
1505
- state: (s.state ?? []).map((slot) => ({ ...slot })),
1506
- tasks: s.tasks.map((t) => ({
1507
- ...t,
1508
- ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1509
- }))
1510
- })),
1511
- pendingEffects: [...instance.pendingEffects],
1512
- effectHistory: [...instance.effectHistory],
1513
- effectsContext: [...instance.effectsContext],
1514
- history: [...instance.history],
1515
- lastChangedAt: instance.lastChangedAt,
1516
- ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1517
- pendingCreates: []
1518
- };
1972
+ ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock);
1519
1973
  }
1520
- function taskStatusChangedEntry(args) {
1521
- return {
1522
- _key: randomKey(),
1523
- _type: "workflow.history.taskStatusChanged",
1524
- at: args.at ?? (/* @__PURE__ */ new Date()).toISOString(),
1525
- stageId: args.stageId,
1526
- taskId: args.taskId,
1527
- from: args.from,
1528
- to: args.to,
1529
- ...args.actor !== void 0 ? { actor: args.actor } : {}
1530
- };
1531
- }
1532
- function stageTransitionHistory(args) {
1533
- const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1534
- at,
1535
- ...transitionId !== void 0 ? { transitionId } : {},
1536
- ...actor !== void 0 ? { actor } : {},
1537
- ...via !== void 0 ? { via } : {},
1538
- ...reason !== void 0 ? { reason } : {}
1539
- };
1540
- return [
1541
- {
1542
- _key: randomKey(),
1543
- _type: "workflow.history.stageExited",
1544
- stageId: fromStageId,
1545
- toStageId,
1546
- ...shared
1547
- },
1548
- {
1549
- _key: randomKey(),
1550
- _type: "workflow.history.transitionFired",
1551
- fromStageId,
1552
- toStageId,
1553
- ...shared
1554
- },
1555
- {
1556
- _key: randomKey(),
1557
- _type: "workflow.history.stageEntered",
1558
- stageId: toStageId,
1559
- fromStageId,
1560
- ...shared
1561
- }
1562
- ];
1563
- }
1564
- async function persist(ctx, mutation) {
1565
- const set = {
1566
- currentStageId: mutation.currentStageId,
1567
- state: mutation.state,
1568
- stages: mutation.stages,
1569
- pendingEffects: mutation.pendingEffects,
1570
- effectHistory: mutation.effectHistory,
1571
- effectsContext: mutation.effectsContext,
1572
- history: mutation.history,
1573
- lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1574
- };
1575
- mutation.completedAt !== void 0 && (set.completedAt = mutation.completedAt);
1576
- const pendingCreates = mutation.pendingCreates;
1577
- if (pendingCreates.length === 0)
1578
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1579
- const tx = ctx.client.transaction();
1580
- for (const body of pendingCreates) tx.create(body);
1581
- tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1582
- const actorForPriming = void 0;
1583
- for (const body of pendingCreates)
1584
- await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr);
1585
- const reloaded = await ctx.client.getDocument(ctx.instance._id);
1586
- if (!reloaded)
1587
- throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1588
- return reloaded;
1589
- }
1590
- function findOpenStageEntry(host) {
1591
- return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
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;
1592
1983
  }
1593
- function currentStageEntry(mutation) {
1594
- const entry = findOpenStageEntry(mutation);
1595
- if (entry === void 0)
1596
- throw new Error(
1597
- `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
1598
- );
1599
- return entry;
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;
1600
1995
  }
1601
- function currentTasks(mutation) {
1602
- return currentStageEntry(mutation).tasks;
1996
+ async function fetchGrants(args) {
1997
+ const { client, resourcePath, signal } = args;
1998
+ return client.request({ url: resourcePath, ...signal ? { signal } : {} });
1603
1999
  }
1604
- function findTaskInCurrentStage(ctx, taskId) {
1605
- const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1606
- if (task === 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)
1607
2015
  throw new Error(
1608
- `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
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."
1609
2017
  );
1610
- return { stage, task };
1611
- }
1612
- function requireMutationTaskEntry(mutation, taskId) {
1613
- const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1614
- if (mutEntry === void 0)
1615
- throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1616
- return mutEntry;
2018
+ return { actor, ...grants !== void 0 ? { grants } : {} };
1617
2019
  }
1618
- function findCurrentStageEntry(instance) {
1619
- return findOpenStageEntry(instance);
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;
1620
2027
  }
1621
- function findCurrentTasks(instance) {
1622
- return findCurrentStageEntry(instance)?.tasks ?? [];
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);
1623
2030
  }
1624
- async function completeEffect(args) {
1625
- const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId);
1626
- return commitCompleteEffect(
1627
- ctx,
1628
- effectKey,
1629
- status,
1630
- outputs,
1631
- detail,
1632
- error,
1633
- durationMs,
1634
- options?.actor
1635
- );
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;
1636
2036
  }
1637
- function buildEffectHistoryEntry(pending, outcome) {
1638
- const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome;
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) ?? [];
1639
2052
  return {
1640
- _key: pending._key,
1641
- effectId: pending.effectId,
1642
- ...pending.title !== void 0 ? { title: pending.title } : {},
1643
- ...pending.description !== void 0 ? { description: pending.description } : {},
1644
- params: pending.params,
1645
- origin: pending.origin,
1646
- ...actor !== void 0 ? { actor } : pending.actor !== void 0 ? { actor: pending.actor } : {},
1647
- ranAt,
1648
- ...durationMs !== void 0 ? { durationMs } : {},
1649
- status,
1650
- ...detail !== void 0 ? { detail } : {},
1651
- ...error !== void 0 ? { error } : {},
1652
- ...outputs !== void 0 ? { outputs } : {}
2053
+ kind: "user",
2054
+ id: user.id,
2055
+ ...roleNames.length > 0 ? { roles: roleNames } : {}
1653
2056
  };
1654
2057
  }
1655
- function upsertEffectsContext(mutation, outputs) {
1656
- for (const [id, value] of Object.entries(outputs)) {
1657
- const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1658
- existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
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;
1659
2066
  }
1660
2067
  }
1661
- async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1662
- const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1663
- if (pending === void 0)
1664
- throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1665
- const mutation = startMutation(ctx.instance);
1666
- mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1667
- const ranAt = (/* @__PURE__ */ new Date()).toISOString();
1668
- return mutation.effectHistory.push(
1669
- buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1670
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1671
- _key: randomKey(),
1672
- _type: "workflow.history.effectCompleted",
1673
- at: ranAt,
1674
- effectKey,
1675
- effectId: pending.effectId,
1676
- status,
1677
- ...outputs !== void 0 ? { outputs } : {},
1678
- ...detail !== void 0 ? { detail } : {},
1679
- ...actor !== void 0 ? { actor } : {}
1680
- }), await persist(ctx, mutation), { effectKey, status };
1681
- }
1682
- function buildQueuedEffect(effect, origin, params, actor) {
1683
- const key = randomKey(), pending = {
1684
- _key: key,
1685
- _type: "workflow.pendingEffect",
1686
- effectId: effect.id,
1687
- ...effect.title !== void 0 ? { title: effect.title } : {},
1688
- ...effect.description !== void 0 ? { description: effect.description } : {},
1689
- ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1690
- params,
1691
- origin,
1692
- ...actor !== void 0 ? { actor } : {},
1693
- queuedAt: (/* @__PURE__ */ new Date()).toISOString()
1694
- }, history = {
1695
- _key: randomKey(),
1696
- _type: "workflow.history.effectQueued",
1697
- at: (/* @__PURE__ */ new Date()).toISOString(),
1698
- effectKey: key,
1699
- effectId: effect.id,
1700
- origin
1701
- };
1702
- return { pending, history };
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 } : {}
2088
+ });
1703
2089
  }
1704
- async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1705
- if (!effects) return;
1706
- const liveInstance = {
1707
- ...ctx.instance,
1708
- state: mutation.state,
1709
- stages: mutation.stages,
1710
- effectsContext: mutation.effectsContext
1711
- };
1712
- for (const effect of effects) {
1713
- const params = await resolveBindings({
1714
- bindings: effect.bindings,
1715
- staticInput: effect.input,
1716
- instance: liveInstance,
1717
- ...callerParams !== void 0 ? { params: callerParams } : {},
1718
- ...actor !== void 0 ? { actor } : {}
1719
- }), { pending, history } = buildQueuedEffect(effect, origin, params, actor);
1720
- mutation.pendingEffects.push(pending), mutation.history.push(history);
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));
2126
+ return {
2127
+ instance,
2128
+ definition,
2129
+ actor,
2130
+ currentStage,
2131
+ pendingOnYou,
2132
+ canInteract
2133
+ };
2134
+ }
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,
2154
+ status,
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
2160
+ };
2161
+ }
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 };
2167
+ }
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 };
2175
+ }
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 };
2182
+ }
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 };
2193
+ }
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)))
2211
+ return !0;
2212
+ return !1;
2213
+ }
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
+ );
1721
2222
  }
1722
2223
  }
1723
- class ActionParamsInvalidError extends Error {
1724
- action;
1725
- taskId;
1726
- issues;
1727
- constructor(args) {
1728
- const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1729
- `);
1730
- super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
1731
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
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 });
2227
+ }
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
+ )
2248
+ );
2249
+ return dedupById(perResource.flat());
2250
+ }
2251
+ function tryParseGdr(uri) {
2252
+ try {
2253
+ return parseGdr(uri);
2254
+ } catch {
2255
+ return;
2256
+ }
2257
+ }
2258
+ function dedupById(guards) {
2259
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2260
+ }
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;
1732
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);
1733
2381
  }
1734
2382
  function validateActionParams(action, taskId, callerParams) {
1735
2383
  const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
@@ -1766,7 +2414,7 @@ function checkParamType(value, decl) {
1766
2414
  case "doc.ref":
1767
2415
  return hasStringField(value, "id");
1768
2416
  case "doc.refs":
1769
- return Array.isArray(value) && value.every((v) => checkParamType(v, { paramType: "doc.ref" }));
2417
+ return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
1770
2418
  case "json":
1771
2419
  return !0;
1772
2420
  default:
@@ -1774,15 +2422,15 @@ function checkParamType(value, decl) {
1774
2422
  }
1775
2423
  }
1776
2424
  async function runActionOps(args) {
1777
- const { ops, mutation, stageId, taskId, actionName, params, actor } = args;
2425
+ const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
1778
2426
  if (ops === void 0 || ops.length === 0) return [];
1779
2427
  const summaries = [];
1780
2428
  for (const op of ops) {
1781
- const summary = applyOp(op, { mutation, stageId, taskId, params, actor });
2429
+ const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
1782
2430
  summaries.push(summary), mutation.history.push({
1783
2431
  _key: randomKey(),
1784
2432
  _type: "workflow.history.opApplied",
1785
- at: (/* @__PURE__ */ new Date()).toISOString(),
2433
+ at: now,
1786
2434
  stageId,
1787
2435
  taskId,
1788
2436
  actionId: actionName,
@@ -1841,8 +2489,8 @@ function applyStateUpdateWhere(op, ctx) {
1841
2489
  /*createIfMissing*/
1842
2490
  !1
1843
2491
  ), resolvedMerge = {};
1844
- for (const [k, v] of Object.entries(op.merge))
1845
- resolvedMerge[k] = resolveOpSource(v, ctx);
2492
+ for (const [k, v2] of Object.entries(op.merge))
2493
+ resolvedMerge[k] = resolveOpSource(v2, ctx);
1846
2494
  return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
1847
2495
  (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
1848
2496
  )), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
@@ -1885,18 +2533,11 @@ function locateSlot(ctx, target, createIfMissing) {
1885
2533
  }, host.push(slot)), slot;
1886
2534
  }
1887
2535
  function resolveOpSource(src, ctx) {
2536
+ const staticValue = resolveStaticSource(src, ctx);
2537
+ if (staticValue.handled) return staticValue.value;
1888
2538
  switch (src.source) {
1889
- case "literal":
1890
- return src.value;
1891
- case "param":
1892
- return ctx.params[src.paramId];
1893
- case "actor":
1894
- return ctx.actor;
1895
- case "now":
1896
- return (/* @__PURE__ */ new Date()).toISOString();
1897
2539
  case "self":
1898
- return;
1899
- // Not applicable inside ops; reserved for higher-level binding.
2540
+ return ctx.self;
1900
2541
  case "stageId":
1901
2542
  return ctx.stageId;
1902
2543
  case "stateRead": {
@@ -1906,8 +2547,8 @@ function resolveOpSource(src, ctx) {
1906
2547
  case "effectOutput": {
1907
2548
  const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
1908
2549
  if (entry === void 0) return null;
1909
- const v = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
1910
- return v === void 0 ? null : v;
2550
+ const v2 = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
2551
+ return v2 === void 0 ? null : v2;
1911
2552
  }
1912
2553
  case "object": {
1913
2554
  const out = {};
@@ -1915,6 +2556,11 @@ function resolveOpSource(src, ctx) {
1915
2556
  out[field] = resolveOpSource(fieldSrc, ctx);
1916
2557
  return out;
1917
2558
  }
2559
+ case "row":
2560
+ case "parentState":
2561
+ return;
2562
+ default:
2563
+ return;
1918
2564
  }
1919
2565
  }
1920
2566
  function slotValue(slots, slotId) {
@@ -1933,333 +2579,101 @@ function evalOpPredicate(p, row, ctx) {
1933
2579
  const target = resolveOpSource(p.equals, ctx);
1934
2580
  return row[p.field] === target;
1935
2581
  }
1936
- async function fireAction(args) {
1937
- const { client, instanceId, taskId, action, params, options } = args, ctx = await loadContext(client, instanceId);
1938
- return commitAction(ctx, taskId, action, params, options?.actor);
1939
- }
1940
- async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
1941
- const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
1942
- if (action === void 0)
1943
- throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
1944
- if (!await ctxEvaluateFilter(ctx, action.filter))
1945
- throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
1946
- const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
1947
- if (entry === void 0 || entry.status !== "active")
1948
- throw new Error(
1949
- `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
1950
- );
1951
- const params = validateActionParams(action, taskId, callerParams);
1952
- return { stage, task, action, params };
1953
- }
1954
- function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor) {
1955
- if (action.setStatus === void 0) return;
1956
- const initialStatus = mutEntry.status, newStatus = action.setStatus;
1957
- return mutEntry.status = newStatus, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), actor && (mutEntry.completedBy = actor.id), mutation.history.push(
1958
- taskStatusChangedEntry({
1959
- stageId,
1960
- taskId,
1961
- from: initialStatus,
1962
- to: newStatus,
1963
- ...actor !== void 0 ? { actor } : {}
1964
- })
1965
- ), newStatus;
1966
- }
1967
- async function commitAction(ctx, taskId, actionName, callerParams, actor) {
1968
- const { stage, task, action, params } = await resolveActionCommit(
1969
- ctx,
1970
- taskId,
1971
- actionName,
1972
- callerParams
1973
- ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1974
- mutation.history.push({
1975
- _key: randomKey(),
1976
- _type: "workflow.history.actionFired",
1977
- at: (/* @__PURE__ */ new Date()).toISOString(),
1978
- stageId: stage.id,
1979
- taskId,
1980
- actionId: actionName,
1981
- ...actor !== void 0 ? { actor } : {}
1982
- });
1983
- const ranOps = await runActionOps({
1984
- ops: action.ops,
1985
- mutation,
1986
- stageId: stage.id,
1987
- taskId,
1988
- actionName,
1989
- params,
1990
- actor
1991
- }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor);
1992
- return await queueEffects(
1993
- ctx,
1994
- mutation,
1995
- action.effects,
1996
- {
1997
- kind: "action",
1998
- id: `${task.id}:${actionName}`
1999
- },
2000
- actor,
2001
- params
2002
- ), await persist(ctx, mutation), {
2003
- fired: !0,
2004
- taskId,
2005
- action: actionName,
2006
- ...newStatus !== void 0 ? { newStatus } : {},
2007
- ...ranOps.length > 0 ? { ranOps } : {}
2008
- };
2009
- }
2010
- const parsedFilters = /* @__PURE__ */ new Map();
2011
- async function matchesFilter(args) {
2012
- const { document, filter, userId } = args;
2013
- parsedFilters.has(filter) || parsedFilters.set(filter, parse(`*[${filter}]`));
2014
- const ast = parsedFilters.get(filter), data = await (await evaluate(ast, {
2015
- dataset: [document],
2016
- ...userId !== void 0 ? { identity: userId } : {}
2017
- })).get();
2018
- return Array.isArray(data) && data.length === 1;
2019
- }
2020
- async function grantsPermissionOn(args) {
2021
- const { document, grants, permission, userId } = args;
2022
- if (!document || grants.length === 0) return !1;
2023
- for (const grant of grants)
2024
- if (grant.permissions.includes(permission) && await matchesFilter({
2025
- document,
2026
- filter: grant.filter,
2027
- ...userId !== void 0 ? { userId } : {}
2028
- }))
2029
- return !0;
2030
- return !1;
2031
- }
2032
- async function fetchGrants(args) {
2033
- const { client, resourcePath, signal } = args;
2034
- return client.request({ url: resourcePath, ...signal ? { signal } : {} });
2035
- }
2036
- const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
2037
- async function resolveAccess(client, args = {}) {
2038
- const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2039
- if (overrideActor !== void 0 && overrideGrants !== void 0)
2040
- return { actor: overrideActor, grants: overrideGrants };
2041
- const requestFn = client.request;
2042
- if (requestFn === void 0) {
2043
- if (overrideActor === void 0)
2044
- throw new Error(
2045
- "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)."
2046
- );
2047
- return { actor: overrideActor };
2048
- }
2049
- 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]);
2050
- if (actor === void 0)
2051
- throw new Error(
2052
- "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."
2053
- );
2054
- return { actor, ...grants !== void 0 ? { grants } : {} };
2055
- }
2056
- function cachedActor(client, requestFn) {
2057
- const cached = actorCache.get(client);
2058
- if (cached !== void 0) return cached;
2059
- const pending = fetchActor(requestFn).catch((err) => {
2060
- throw actorCache.get(client) === pending && actorCache.delete(client), err;
2061
- });
2062
- return actorCache.set(client, pending), pending;
2063
- }
2064
- function cachedGrants(client, requestFn, resourcePath) {
2065
- let byPath = grantsCache.get(client);
2066
- byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
2067
- let cached = byPath.get(resourcePath);
2068
- return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
2069
- }
2070
- async function fetchActor(requestFn) {
2071
- let user;
2072
- try {
2073
- user = await requestFn({
2074
- uri: "/users/me",
2075
- tag: "workflow.access.resolve-actor"
2076
- });
2077
- } catch (err) {
2078
- throw new Error(
2079
- '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.',
2080
- { cause: err }
2081
- );
2082
- }
2083
- if (!user || typeof user.id != "string" || user.id.length === 0) return;
2084
- const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
2085
- return {
2086
- kind: "user",
2087
- id: user.id,
2088
- ...roleNames.length > 0 ? { roles: roleNames } : {}
2089
- };
2090
- }
2091
- async function fetchGrantsCached(requestFn, resourcePath) {
2092
- try {
2093
- return await fetchGrants({ client: { request: requestFn }, resourcePath });
2094
- } catch (err) {
2095
- console.warn(
2096
- `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2097
- );
2098
- return;
2099
- }
2100
- }
2101
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
2102
- function validateTags(tags) {
2103
- if (tags.length === 0)
2104
- throw new Error("tags: must be a non-empty array");
2105
- for (const tag of tags)
2106
- if (!TAG_RE.test(tag))
2107
- throw new Error(
2108
- `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
2109
- );
2110
- }
2111
- function canonicalTag(tags) {
2112
- return tags[0];
2113
- }
2114
- const WILDCARD_ROLE = "*";
2115
- async function evaluateInstance(args) {
2116
- const { client, tags, instanceId, resourceClients } = args;
2117
- validateTags(tags);
2118
- const { actor, grants } = await resolveAccess(client, {
2119
- ...args.access !== void 0 ? { override: args.access } : {},
2120
- ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2121
- }), instance = await client.getDocument(instanceId);
2122
- if (!instance)
2123
- throw new Error(`Workflow instance "${instanceId}" not found`);
2124
- if (!tags.some((t) => instance.tags?.includes(t)))
2125
- throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2126
- const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2127
- if (stage === void 0)
2128
- throw new Error(
2129
- `Instance "${instanceId}" currentStageId "${instance.currentStageId}" not in definition`
2130
- );
2131
- const snapshot = await hydrateSnapshot({
2132
- client,
2133
- clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client,
2134
- instance,
2135
- definition
2136
- }), currentTaskEntries = instance.stages.find(
2137
- (s) => s.id === instance.currentStageId && s.exitedAt === void 0
2138
- )?.tasks ?? [], taskEvaluations = [];
2139
- for (const task of stage.tasks ?? [])
2140
- taskEvaluations.push(
2141
- await evaluateTask({
2142
- task,
2143
- statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2144
- instance,
2145
- definition,
2146
- actor,
2147
- grants,
2148
- snapshot
2149
- })
2150
- );
2151
- const transitionEvaluations = [];
2152
- for (const transition of stage.transitions ?? [])
2153
- transitionEvaluations.push({
2154
- transition,
2155
- filterSatisfied: await evaluateFilter({
2156
- filter: transition.filter,
2157
- definition,
2158
- snapshot,
2159
- params: buildParams(instance)
2160
- })
2161
- });
2162
- const currentStage = {
2163
- stage,
2164
- tasks: taskEvaluations,
2165
- transitions: transitionEvaluations
2166
- }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2167
- return {
2168
- instance,
2169
- definition,
2170
- actor,
2171
- currentStage,
2172
- pendingOnYou,
2173
- canInteract
2174
- };
2175
- }
2176
- async function evaluateTask(args) {
2177
- const { task, statusEntry, instance, definition, actor, grants, snapshot } = args, status = statusEntry?.status ?? "pending", actions = [];
2178
- for (const action of task.actions ?? [])
2179
- actions.push(
2180
- await evaluateAction({
2181
- action,
2182
- task,
2183
- ...statusEntry !== void 0 ? { statusEntry } : {},
2184
- status,
2185
- instance,
2186
- definition,
2187
- actor,
2188
- ...grants !== void 0 ? { grants } : {},
2189
- snapshot
2190
- })
2191
- );
2192
- return {
2193
- task,
2194
- status,
2195
- // "pending on actor" treats both `pending` and `active` as waiting on
2196
- // the assignee — pending tasks just haven't been auto-invoked yet, but
2197
- // they're still inbox items.
2198
- pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2199
- actions
2200
- };
2201
- }
2202
- async function evaluateAction(args) {
2203
- const { action, task, status, instance, definition, actor, grants, snapshot } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2204
- if (syncReason !== void 0) return disabled(action, syncReason);
2205
- const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot);
2206
- return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
2207
- }
2208
- function lifecycleReason(instance, definition, status) {
2209
- if (instance.completedAt !== void 0)
2210
- return { kind: "instance-completed", completedAt: instance.completedAt };
2211
- if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2212
- return { kind: "stage-terminal", stageId: instance.currentStageId };
2213
- if (status === "done" || status === "skipped" || status === "failed")
2214
- return { kind: "task-not-active", status };
2215
- }
2216
- function actorReason(action, task, actor) {
2217
- const actorRoles = actor.roles ?? [];
2218
- if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2219
- return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2220
- if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2221
- return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
2222
- }
2223
- async function permissionReason(action, instance, actor, grants) {
2224
- if (grants === void 0) return;
2225
- const permission = action.requiredPermission ?? "update";
2226
- if (!await grantsPermissionOn({
2227
- document: instance,
2228
- grants,
2229
- permission,
2230
- userId: actor.id
2231
- }))
2232
- return { kind: "permission-denied", permission, matchingGrants: grants.length };
2233
- }
2234
- async function filterReason(action, instance, definition, snapshot) {
2235
- if (!(action.filter === void 0 || await evaluateFilter({
2236
- filter: action.filter,
2237
- definition,
2238
- snapshot,
2239
- params: buildParams(instance)
2240
- })))
2241
- return { kind: "filter-failed", filter: action.filter };
2242
- }
2243
- function disabled(action, reason) {
2244
- return { action, allowed: !1, disabledReason: reason };
2245
- }
2246
- function actorIsAssignee(actor, assignees) {
2247
- if (assignees.length === 0) return !1;
2248
- const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2249
- for (const assignee of assignees)
2250
- if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
2251
- return !0;
2252
- return !1;
2582
+ async function fireAction(args) {
2583
+ const { client, instanceId, taskId, action, params, options } = args;
2584
+ for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2585
+ const ctx = await loadContext(client, instanceId, {
2586
+ ...options?.clock ? { clock: options.clock } : {}
2587
+ });
2588
+ try {
2589
+ return await commitAction(ctx, taskId, action, params, options?.actor);
2590
+ } catch (error) {
2591
+ if (!isRevisionConflict(error)) throw error;
2592
+ }
2593
+ }
2594
+ throw new ConcurrentFireActionError({
2595
+ instanceId,
2596
+ taskId,
2597
+ action,
2598
+ attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2599
+ });
2253
2600
  }
2254
- function parseDefinitionSnapshot(instance) {
2255
- try {
2256
- return JSON.parse(instance.definitionSnapshot);
2257
- } catch (err) {
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")
2258
2609
  throw new Error(
2259
- `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2260
- { cause: err }
2610
+ `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2261
2611
  );
2262
- }
2612
+ const params = validateActionParams(action, taskId, callerParams);
2613
+ return { stage, task, action, params };
2614
+ }
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
+ };
2263
2677
  }
2264
2678
  class ActionDisabledError extends Error {
2265
2679
  reason;
@@ -2283,121 +2697,6 @@ function formatDisabledReason(taskId, action, reason) {
2283
2697
  const detail = disabledReasonDetail[reason.kind](reason);
2284
2698
  return `Action "${taskId}:${action}" is not allowed: ${detail}`;
2285
2699
  }
2286
- function definitionDocId(_workflowResource, prefix, workflowId, version) {
2287
- return `${prefix}.${workflowId}.v${version}`;
2288
- }
2289
- async function sortByDependencies(client, definitions, tags, workflowResource) {
2290
- const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2291
- for (const def of definitions) {
2292
- const list = byWorkflowId.get(def.workflowId) ?? [];
2293
- list.push(def), byWorkflowId.set(def.workflowId, list);
2294
- }
2295
- const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2296
- return await assertCrossBatchRefsDeployed(client, definitions, {
2297
- tags,
2298
- workflowResource,
2299
- prefix,
2300
- findInBatch
2301
- }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2302
- }
2303
- function findRefInBatch(byWorkflowId, ref) {
2304
- const candidates = byWorkflowId.get(ref.workflowId);
2305
- if (!(candidates === void 0 || candidates.length === 0))
2306
- return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2307
- (best, c) => best === void 0 || c.version > best.version ? c : best,
2308
- void 0
2309
- );
2310
- }
2311
- async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2312
- const missing = [];
2313
- for (const def of definitions) {
2314
- const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2315
- for (const ref of refsOf(def)) {
2316
- if (ctx.findInBatch(ref) !== void 0) continue;
2317
- const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2318
- label !== void 0 && missing.push({ from: fromId, ref: label });
2319
- }
2320
- }
2321
- if (missing.length === 0) return;
2322
- const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
2323
- throw new Error(
2324
- `workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
2325
- ` + lines.join(`
2326
- `)
2327
- );
2328
- }
2329
- async function resolveDeployedRefLabel(client, ref, tags) {
2330
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
2331
- if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2332
- definitionLookupGroq(wantsExplicit),
2333
- params
2334
- ))
2335
- return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2336
- }
2337
- function topoSortDefinitions(definitions, ctx) {
2338
- const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2339
- const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2340
- if (!visited.has(id)) {
2341
- if (visiting.has(id))
2342
- throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
2343
- visiting.add(id);
2344
- for (const ref of refsOf(def)) {
2345
- const child = ctx.findInBatch(ref);
2346
- child !== void 0 && visit(child);
2347
- }
2348
- visiting.delete(id), visited.add(id), ordered.push(def);
2349
- }
2350
- };
2351
- for (const def of definitions) visit(def);
2352
- return ordered;
2353
- }
2354
- function definitionLookupGroq(wantsExplicit) {
2355
- 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]';
2356
- }
2357
- function refsOf(def) {
2358
- const out = [];
2359
- for (const stage of def.stages)
2360
- for (const task of stage.tasks ?? []) {
2361
- const ref = task.spawns?.definitionRef;
2362
- ref !== void 0 && out.push({
2363
- workflowId: ref.workflowId,
2364
- ...ref.version !== void 0 ? { version: ref.version } : {}
2365
- });
2366
- }
2367
- return out;
2368
- }
2369
- function isDefinitionUnchanged(existing, expected) {
2370
- return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
2371
- }
2372
- function stripSystemFields(doc) {
2373
- const out = {};
2374
- for (const [k, v] of Object.entries(doc))
2375
- k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v);
2376
- return out;
2377
- }
2378
- function stableStringify(value) {
2379
- return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2380
- ([a], [b]) => a.localeCompare(b)
2381
- ).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
2382
- }
2383
- async function loadDefinition(client, workflowId, version, tags) {
2384
- if (version !== void 0) {
2385
- const doc = await client.fetch(
2386
- "*[_type == 'workflow.definition' && workflowId == $id && version == $v && count(tags[@ in $engineTags]) > 0][0]",
2387
- { id: workflowId, v: version, engineTags: tags }
2388
- );
2389
- if (!doc)
2390
- throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2391
- return doc;
2392
- }
2393
- const latest = await client.fetch(
2394
- "*[_type == 'workflow.definition' && workflowId == $id && count(tags[@ in $engineTags]) > 0] | order(version desc)[0]",
2395
- { id: workflowId, engineTags: tags }
2396
- );
2397
- if (!latest)
2398
- throw new Error(`No deployed definition for workflow ${workflowId}`);
2399
- return latest;
2400
- }
2401
2700
  function bareIdFromSpawnRef(uri) {
2402
2701
  if (!uri.includes(":")) return [uri];
2403
2702
  try {
@@ -2418,9 +2717,16 @@ async function resolveOperationContext(args) {
2418
2717
  clientForGdr: buildClientForGdr(args.client, args.resourceClients)
2419
2718
  };
2420
2719
  }
2421
- async function cascade(client, instanceId, actor, clientForGdr) {
2422
- const count = await cascadeAutoTransitions(client, instanceId, actor, clientForGdr);
2423
- return await propagateToAncestors(client, instanceId, actor, clientForGdr), count;
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
+ );
2729
+ return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
2424
2730
  }
2425
2731
  function buildClientForGdr(defaultClient, resolver) {
2426
2732
  return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
@@ -2438,6 +2744,22 @@ function intersectsTags(docTags, engineTags) {
2438
2744
  function toEffectsContextEntries(ctx) {
2439
2745
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2440
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
+ }
2441
2763
  const workflow = {
2442
2764
  /**
2443
2765
  * Persist a workflow definition to the lake. Stored as a
@@ -2470,7 +2792,7 @@ const workflow = {
2470
2792
  const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
2471
2793
  ...def,
2472
2794
  _id: id,
2473
- _type: "workflow.definition",
2795
+ _type: WORKFLOW_DEFINITION_TYPE,
2474
2796
  tags
2475
2797
  }, existing = await client.getDocument(id);
2476
2798
  if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
@@ -2512,16 +2834,17 @@ const workflow = {
2512
2834
  effectsContext,
2513
2835
  instanceId,
2514
2836
  perspective
2515
- } = args, { actor, clientForGdr } = await resolveOperationContext(args), definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2837
+ } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2516
2838
  if (definition.stages.find((s) => s.id === definition.initialStageId) === void 0)
2517
2839
  throw new Error(
2518
2840
  `Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
2519
2841
  );
2520
- const now = (/* @__PURE__ */ new Date()).toISOString(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2842
+ const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2521
2843
  slotDefs: definition.state ?? [],
2522
2844
  initialState: initialState ?? [],
2523
2845
  ctx: {
2524
2846
  client,
2847
+ now,
2525
2848
  selfId: id,
2526
2849
  tags,
2527
2850
  workflowResource,
@@ -2543,7 +2866,7 @@ const workflow = {
2543
2866
  initialStageId: definition.initialStageId,
2544
2867
  actor
2545
2868
  });
2546
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr), await cascade(client, id, actor, clientForGdr), reload(client, id, tags);
2869
+ return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
2547
2870
  },
2548
2871
  /**
2549
2872
  * Fire an action against a task. If the task is pending, it is
@@ -2565,43 +2888,32 @@ const workflow = {
2565
2888
  params,
2566
2889
  idempotent,
2567
2890
  resourceClients
2568
- } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), before = await reload(client, instanceId, tags), taskEntry = before.stages.find(
2569
- (s) => s.id === before.currentStageId && s.exitedAt === void 0
2570
- )?.tasks.find((t) => t.id === taskId);
2571
- 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)
2572
2893
  return { instance: before, cascaded: 0, fired: !1 };
2573
- const actionEval = (await evaluateInstance({
2894
+ const evaluation = await evaluateInstance({
2574
2895
  client,
2575
2896
  tags,
2576
2897
  instanceId,
2577
2898
  access,
2899
+ clock,
2578
2900
  ...resourceClients !== void 0 ? { resourceClients } : {}
2579
- })).currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2580
- if (actionEval !== void 0 && !actionEval.allowed && actionEval.disabledReason)
2581
- throw new ActionDisabledError({
2582
- taskId,
2583
- action,
2584
- reason: actionEval.disabledReason
2585
- });
2586
- taskEntry?.status === "pending" && await invokeTask({
2587
- client,
2588
- instanceId,
2589
- taskId,
2590
- options: { actor }
2591
2901
  });
2592
- const actionResult = await fireAction({
2902
+ assertActionAllowed(evaluation, taskId, action);
2903
+ const ranOps = await applyAction({
2593
2904
  client,
2594
- instanceId,
2905
+ instance: before,
2595
2906
  taskId,
2596
2907
  action,
2597
- ...params !== void 0 ? { params } : {},
2598
- options: { actor }
2599
- }), cascaded = await cascade(client, instanceId, actor, clientForGdr);
2908
+ actor,
2909
+ clock,
2910
+ ...params !== void 0 ? { params } : {}
2911
+ }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2600
2912
  return {
2601
2913
  instance: await reload(client, instanceId, tags),
2602
2914
  cascaded,
2603
2915
  fired: !0,
2604
- ...actionResult.ranOps !== void 0 ? { ranOps: actionResult.ranOps } : {}
2916
+ ...ranOps !== void 0 ? { ranOps } : {}
2605
2917
  };
2606
2918
  },
2607
2919
  /**
@@ -2612,7 +2924,7 @@ const workflow = {
2612
2924
  * reference them. Cascades after.
2613
2925
  */
2614
2926
  completeEffect: async (args) => {
2615
- const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2927
+ const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2616
2928
  await completeEffect({
2617
2929
  client,
2618
2930
  instanceId,
@@ -2622,9 +2934,9 @@ const workflow = {
2622
2934
  ...detail !== void 0 ? { detail } : {},
2623
2935
  ...error !== void 0 ? { error } : {},
2624
2936
  ...durationMs !== void 0 ? { durationMs } : {},
2625
- ...actor !== void 0 ? { options: { actor } } : {}
2937
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
2626
2938
  });
2627
- const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2939
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2628
2940
  return {
2629
2941
  instance: await reload(client, instanceId, tags),
2630
2942
  cascaded,
@@ -2641,9 +2953,9 @@ const workflow = {
2641
2953
  * affected instances and the engine re-evaluates.
2642
2954
  */
2643
2955
  tick: async (args) => {
2644
- const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2956
+ const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2645
2957
  await reload(client, instanceId, tags);
2646
- const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2958
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2647
2959
  return {
2648
2960
  instance: await reload(client, instanceId, tags),
2649
2961
  cascaded,
@@ -2656,15 +2968,15 @@ const workflow = {
2656
2968
  * upstream; this verb performs the mechanical move.
2657
2969
  */
2658
2970
  setStage: async (args) => {
2659
- const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2971
+ const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2660
2972
  await reload(client, instanceId, tags);
2661
2973
  const result = await setStage({
2662
2974
  client,
2663
2975
  instanceId,
2664
2976
  targetStageId,
2665
2977
  ...reason !== void 0 ? { reason } : {},
2666
- ...actor !== void 0 ? { options: { actor } } : {}
2667
- }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr) : 0;
2978
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
2979
+ }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
2668
2980
  return {
2669
2981
  instance: await reload(client, instanceId, tags),
2670
2982
  cascaded,
@@ -2680,6 +2992,16 @@ const workflow = {
2680
2992
  const { client, tags, instanceId } = args;
2681
2993
  return validateTags(tags), reload(client, instanceId, tags);
2682
2994
  },
2995
+ guardsForInstance: async (args) => {
2996
+ const { client, tags, instanceId, resourceClients } = args;
2997
+ validateTags(tags);
2998
+ const instance = await reload(client, instanceId, tags);
2999
+ return guardsForInstance({
3000
+ client,
3001
+ clientForGdr: buildClientForGdr(client, resourceClients),
3002
+ instance
3003
+ });
3004
+ },
2683
3005
  /**
2684
3006
  * Run a caller-supplied GROQ query with the engine's tags bound as
2685
3007
  * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
@@ -2714,9 +3036,9 @@ const workflow = {
2714
3036
  * without re-implementing hydration. Pure read — never writes.
2715
3037
  */
2716
3038
  queryInScope: async (args) => {
2717
- const { client, tags, instanceId, groq, params, resourceClients } = args;
3039
+ const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
2718
3040
  validateTags(tags);
2719
- 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), 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 } });
2720
3042
  return await (await evaluate(tree, {
2721
3043
  dataset: snapshot.docs,
2722
3044
  params: { ...reserved, ...params }
@@ -2759,7 +3081,7 @@ const workflow = {
2759
3081
  validateTags(tags);
2760
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));
2761
3083
  return ids.length === 0 ? [] : client.fetch(
2762
- "*[_id in $ids && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)",
3084
+ `*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
2763
3085
  { ids, engineTags: tags }
2764
3086
  );
2765
3087
  },
@@ -2778,7 +3100,7 @@ async function verifyDeployedDefinitionsInternal(args) {
2778
3100
  const { client, tags, effectHandlers, missingHandler, logger } = args;
2779
3101
  validateTags(tags);
2780
3102
  const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
2781
- '*[_type == "workflow.definition" && count(tags[@ in $engineTags]) > 0] | order(workflowId asc, version asc)',
3103
+ `*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(workflowId asc, version asc)`,
2782
3104
  { engineTags: tags }
2783
3105
  ), seen = [], missingByName = /* @__PURE__ */ new Map();
2784
3106
  for (const def of definitions)
@@ -2900,7 +3222,8 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
2900
3222
  };
2901
3223
  try {
2902
3224
  return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
2903
- } catch {
3225
+ } catch (error) {
3226
+ if (!isRevisionConflict(error)) throw error;
2904
3227
  return !1;
2905
3228
  }
2906
3229
  }
@@ -2928,6 +3251,89 @@ async function applyMissingHandler(policy, info, log) {
2928
3251
  return "fail";
2929
3252
  }
2930
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
+ }
2931
3337
  const defaultLoggerFactory = (name) => ({
2932
3338
  // info/warn/error all go to stderr — these are diagnostic, not
2933
3339
  // program output. Keeps stdout reserved for the caller's own data
@@ -2945,13 +3351,14 @@ const defaultLoggerFactory = (name) => ({
2945
3351
  }
2946
3352
  };
2947
3353
  function createEngine(args) {
2948
- const { client, workflowResource, tags, resourceClients } = args;
3354
+ const { client, workflowResource, tags, resourceClients, clock } = args;
2949
3355
  validateTags(tags);
2950
3356
  const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
2951
3357
  client,
2952
3358
  tags,
2953
3359
  workflowResource,
2954
3360
  ...resourceClients !== void 0 ? { resourceClients } : {},
3361
+ ...clock !== void 0 ? { clock } : {},
2955
3362
  ...rest
2956
3363
  });
2957
3364
  return {
@@ -2969,6 +3376,23 @@ function createEngine(args) {
2969
3376
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
2970
3377
  setStage: (rest) => workflow.setStage(bind(rest)),
2971
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
+ }),
3389
+ guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
3390
+ client,
3391
+ tags,
3392
+ workflowResource,
3393
+ instanceId,
3394
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3395
+ }),
2972
3396
  children: ({ instanceId, taskId }) => workflow.children({
2973
3397
  client,
2974
3398
  tags,
@@ -2989,7 +3413,8 @@ function createEngine(args) {
2989
3413
  workflowResource,
2990
3414
  instanceId,
2991
3415
  groq,
2992
- ...params !== void 0 ? { params } : {}
3416
+ ...params !== void 0 ? { params } : {},
3417
+ ...clock !== void 0 ? { clock } : {}
2993
3418
  }),
2994
3419
  listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tags, workflowResource, instanceId }),
2995
3420
  findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
@@ -3020,15 +3445,6 @@ function createEngine(args) {
3020
3445
  })
3021
3446
  };
3022
3447
  }
3023
- class MutationGuardDeniedError extends Error {
3024
- denied;
3025
- documentId;
3026
- action;
3027
- constructor(args) {
3028
- const ids = args.denied.map((d) => d.guardId).join(", ");
3029
- 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;
3030
- }
3031
- }
3032
3448
  const HISTORY_DISPLAY = {
3033
3449
  "workflow.history.stageEntered": {
3034
3450
  title: "Stage entered",
@@ -3224,41 +3640,58 @@ export {
3224
3640
  AUTHORING_DISPLAY,
3225
3641
  ActionDisabledError,
3226
3642
  ActionParamsInvalidError,
3643
+ CascadeLimitError,
3644
+ ConcurrentFireActionError,
3227
3645
  DISPLAY,
3228
3646
  EFFECTS_CONTEXT_DISPLAY,
3647
+ GUARD_DOC_TYPE,
3229
3648
  GUARD_LIFTED_PREDICATE,
3230
3649
  GUARD_OWNER,
3231
3650
  HISTORY_DISPLAY,
3232
3651
  MutationGuardDeniedError,
3233
3652
  OP_DISPLAY,
3234
3653
  STATE_SLOT_DISPLAY,
3654
+ WORKFLOW_DEFINITION_TYPE,
3655
+ WORKFLOW_INSTANCE_TYPE,
3656
+ WorkflowStateDivergedError,
3657
+ buildSnapshot,
3235
3658
  canonicalTag,
3236
3659
  compileGuard,
3660
+ contentReleaseName,
3237
3661
  createEngine,
3238
3662
  defaultLoggerFactory,
3239
3663
  denyingGuards,
3240
3664
  deployStageGuards,
3241
3665
  displayDescription,
3242
3666
  displayTitle,
3667
+ evaluateFromSnapshot,
3243
3668
  evaluateMutationGuard,
3244
3669
  extractDocumentId,
3245
3670
  gdrFromResource,
3246
3671
  gdrRef,
3247
3672
  gdrUri,
3248
3673
  guardMatches,
3674
+ guardsForInstance,
3675
+ guardsForResource,
3676
+ isDefinitionUnchanged,
3249
3677
  isGdr,
3250
3678
  lakeGuardId,
3251
3679
  parseGdr,
3252
- reconcileStageGuards,
3680
+ readsRaw,
3253
3681
  refCanvas,
3254
3682
  refDashboard,
3255
3683
  refDataset,
3256
3684
  refMediaLibrary,
3257
3685
  resolveAccess,
3686
+ resourceFromParsed,
3258
3687
  retractStageGuards,
3259
3688
  silentLogger,
3689
+ stripSystemFields,
3690
+ subscriptionDocumentsForInstance,
3691
+ tagScopeFilter,
3260
3692
  validateDefinition,
3261
3693
  validateTags,
3694
+ wallClock,
3262
3695
  workflow
3263
3696
  };
3264
3697
  //# sourceMappingURL=index.js.map