@sanity/workflow-engine 0.1.0 → 0.2.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,17 @@
1
1
  import { parse, evaluate } from "groq-js";
2
- import { z } from "zod";
2
+ import * as v from "valibot";
3
3
  function validateDefinition(definition) {
4
- const v = createDefinitionValidator();
4
+ const v2 = createDefinitionValidator();
5
5
  for (const slot of definition.state ?? [])
6
- v.checkSlot(slot, "workflow.state", `workflow.state "${slot.id}"`);
6
+ v2.checkSlot(slot, `workflow.state "${slot.id}"`);
7
7
  for (const p of definition.predicates ?? [])
8
- p.groq && v.checkFilter(p.groq, `predicate "${p.id}"`);
8
+ p.groq && v2.checkFilter(p.groq, `predicate "${p.id}"`);
9
9
  for (const stage of definition.stages)
10
- validateStage(v, stage);
11
- if (v.errors.length > 0)
10
+ validateStage(v2, stage);
11
+ if (v2.errors.length > 0)
12
12
  throw new Error(
13
- `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v.errors.length} validation error${v.errors.length === 1 ? "" : "s"}:
14
- ` + v.errors.join(`
13
+ `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
14
+ ` + v2.errors.join(`
15
15
  `)
16
16
  );
17
17
  }
@@ -30,31 +30,29 @@ function createDefinitionValidator() {
30
30
  };
31
31
  return { errors, tryParse, checkFilter: (groq, where) => {
32
32
  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`);
33
+ }, checkSlot: (slot, slotLabel) => {
34
+ slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
37
35
  } };
38
36
  }
39
- function validateStage(v, stage) {
37
+ function validateStage(v2, stage) {
40
38
  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`);
39
+ v2.checkSlot(slot, `stage "${stage.id}".state "${slot.id}"`);
40
+ typeof stage.completion == "string" && v2.tryParse(stage.completion, `stage "${stage.id}".completion`);
43
41
  for (const t of stage.transitions ?? [])
44
- typeof t.filter == "string" && v.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
42
+ typeof t.filter == "string" && v2.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
45
43
  for (const task of stage.tasks ?? [])
46
- validateTask(v, stage.id, task);
44
+ validateTask(v2, stage.id, task);
47
45
  }
48
- function validateTask(v, stageId, task) {
46
+ function validateTask(v2, stageId, task) {
49
47
  const where = `stage "${stageId}" task "${task.id}"`;
50
48
  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`);
49
+ v2.checkSlot(slot, `${where}.state "${slot.id}"`);
50
+ typeof task.filter == "string" && v2.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v2.checkFilter(task.completeWhen, `${where}.completeWhen`);
53
51
  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`);
52
+ typeof a.filter == "string" && v2.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
53
+ task.spawns?.forEach?.groq && v2.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
56
54
  }
57
- const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
55
+ const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
58
56
  function parseGdr(uri) {
59
57
  const colon = uri.indexOf(":");
60
58
  if (colon < 0)
@@ -121,6 +119,9 @@ function gdrFromResource(res, documentId) {
121
119
  }
122
120
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
123
121
  }
122
+ function selfGdr(doc) {
123
+ return gdrFromResource(doc.workflowResource, doc._id);
124
+ }
124
125
  function isGdrUri(value) {
125
126
  if (typeof value != "string") return !1;
126
127
  try {
@@ -135,15 +136,15 @@ function gdrRef(res, documentId, type) {
135
136
  function isGdr(value) {
136
137
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
137
138
  }
138
- function buildParams(instance, extra) {
139
+ function buildParams(instance, now, extra) {
139
140
  return {
140
- self: gdrFromResource(instance.workflowResource, instance._id),
141
+ self: selfGdr(instance),
141
142
  state: stateMap(instance.state ?? []),
142
143
  parent: instance.ancestors.at(-1)?.id ?? null,
143
144
  ancestors: instance.ancestors.map((a) => a.id),
144
145
  /** Current stage id — bind for filters that scope to the current stage. */
145
146
  stage: instance.currentStageId,
146
- now: (/* @__PURE__ */ new Date()).toISOString(),
147
+ now,
147
148
  ...extra
148
149
  };
149
150
  }
@@ -167,8 +168,8 @@ function stripStateForLake(value) {
167
168
  if (Array.isArray(value)) return value.map(stripStateForLake);
168
169
  if (typeof value == "object") {
169
170
  const out = {};
170
- for (const [k, v] of Object.entries(value))
171
- out[k] = stripStateForLake(v);
171
+ for (const [k, v2] of Object.entries(value))
172
+ out[k] = stripStateForLake(v2);
172
173
  return out;
173
174
  }
174
175
  return value;
@@ -226,7 +227,7 @@ function matchesParamType(type, value) {
226
227
  case "boolean":
227
228
  return typeof value == "boolean";
228
229
  case "string[]":
229
- return Array.isArray(value) && value.every((v) => typeof v == "string");
230
+ return Array.isArray(value) && value.every((v2) => typeof v2 == "string");
230
231
  case "reference":
231
232
  return isGdr(value);
232
233
  }
@@ -247,11 +248,11 @@ function restampForSnapshot(doc, gdrUri2, resource) {
247
248
  }
248
249
  function rewriteRefsRecursive(value, resource) {
249
250
  if (Array.isArray(value))
250
- return value.map((v) => rewriteRefsRecursive(v, resource));
251
+ return value.map((v2) => rewriteRefsRecursive(v2, resource));
251
252
  if (value === null || typeof value != "object") return value;
252
253
  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);
254
+ for (const [k, v2] of Object.entries(obj))
255
+ k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
255
256
  return out;
256
257
  }
257
258
  async function hydrateSnapshot(args) {
@@ -260,7 +261,7 @@ async function hydrateSnapshot(args) {
260
261
  const fetched = await loadByGdr(client, clientForGdr, instance.workflowResource, uri);
261
262
  fetched && (loaded.push(fetched), visited.add(uri));
262
263
  };
263
- loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(gdrFromResource(instance.workflowResource, instance._id));
264
+ loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
264
265
  for (const ancestor of instance.ancestors)
265
266
  await loadInto(ancestor.id);
266
267
  for (const uri of collectSlotDocUris(instance.state))
@@ -295,12 +296,12 @@ function slotDocUris(slot) {
295
296
  if (!slot || typeof slot != "object") return [];
296
297
  const s = slot;
297
298
  if (s._type === "workflow.state.doc.ref") {
298
- const v = s.value, id = refId(v);
299
+ const v2 = s.value, id = refId(v2);
299
300
  return id !== void 0 ? [id] : [];
300
301
  }
301
302
  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) : [];
303
+ const v2 = s.value;
304
+ return Array.isArray(v2) ? v2.map(refId).filter((id) => id !== void 0) : [];
304
305
  }
305
306
  return [];
306
307
  }
@@ -324,50 +325,61 @@ class SlotValueShapeError extends Error {
324
325
  super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
325
326
  }
326
327
  }
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
- })
328
+ const GdrShape = v.looseObject({
329
+ id: v.pipe(
330
+ v.string(),
331
+ v.check((s) => isGdrUri(s), "must be a GDR URI")
332
+ ),
333
+ type: v.pipe(v.string(), v.minLength(1))
334
+ }), ReleaseRefShape = v.looseObject({
335
+ id: v.pipe(
336
+ v.string(),
337
+ v.check((s) => isGdrUri(s), "must be a GDR URI")
338
+ ),
339
+ type: v.literal("system.release"),
340
+ releaseName: v.pipe(v.string(), v.minLength(1))
341
+ }), ActorShape = v.looseObject({
342
+ kind: v.picklist(["user", "ai", "system"]),
343
+ id: v.pipe(v.string(), v.minLength(1)),
344
+ roles: v.optional(v.array(v.string())),
345
+ onBehalfOf: v.optional(v.string())
346
+ }), AssigneeShape = v.union([
347
+ v.looseObject({ kind: v.literal("user"), id: v.pipe(v.string(), v.minLength(1)) }),
348
+ v.looseObject({ kind: v.literal("role"), role: v.pipe(v.string(), v.minLength(1)) })
349
+ ]), ChecklistItemShape = v.looseObject({
350
+ label: v.pipe(v.string(), v.minLength(1)),
351
+ done: v.boolean(),
352
+ doneBy: v.optional(v.string()),
353
+ doneAt: v.optional(v.string()),
354
+ _key: v.optional(v.pipe(v.string(), v.minLength(1)))
355
+ }), NoteItemShape = v.pipe(
356
+ v.looseObject({
357
+ _key: v.optional(v.pipe(v.string(), v.minLength(1)))
358
+ }),
359
+ v.check(
360
+ (val) => typeof val == "object" && val !== null && !Array.isArray(val),
361
+ "must be an object"
362
+ )
363
+ ), NullableString = v.union([v.null(), v.string()]), NullableNumber = v.union([v.null(), v.number()]), NullableBoolean = v.union([v.null(), v.boolean()]), NullableDateTime = v.union([
364
+ v.null(),
365
+ v.pipe(
366
+ v.string(),
367
+ v.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
368
+ )
357
369
  ]), 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(),
370
+ "workflow.state.doc.ref": v.union([v.null(), GdrShape]),
371
+ "workflow.state.doc.refs": v.array(GdrShape),
372
+ "workflow.state.release.ref": v.union([v.null(), ReleaseRefShape]),
373
+ "workflow.state.query": v.any(),
362
374
  "workflow.state.value.string": NullableString,
363
375
  "workflow.state.value.url": NullableUrl,
364
376
  "workflow.state.value.number": NullableNumber,
365
377
  "workflow.state.value.boolean": NullableBoolean,
366
378
  "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)
379
+ "workflow.state.value.actor": v.union([v.null(), ActorShape]),
380
+ "workflow.state.checklist": v.array(ChecklistItemShape),
381
+ "workflow.state.notes": v.array(NoteItemShape),
382
+ "workflow.state.assignees": v.array(AssigneeShape)
371
383
  }, itemSchemas = {
372
384
  "workflow.state.doc.refs": GdrShape,
373
385
  "workflow.state.checklist": ChecklistItemShape,
@@ -386,12 +398,12 @@ function validateSlotValue(args) {
386
398
  issues: [`unknown slot type ${args.slotType}`],
387
399
  mode: "value"
388
400
  });
389
- const result = schema.safeParse(args.value);
401
+ const result = v.safeParse(schema, args.value);
390
402
  if (!result.success)
391
403
  throw new SlotValueShapeError({
392
404
  slotType: args.slotType,
393
405
  slotId: args.slotId,
394
- issues: formatIssues(result.error.issues),
406
+ issues: formatIssues(result.issues),
395
407
  mode: "value"
396
408
  });
397
409
  }
@@ -403,17 +415,20 @@ function validateSlotAppendItem(args) {
403
415
  issues: [`slot type ${args.slotType} does not support append`],
404
416
  mode: "item"
405
417
  });
406
- const result = itemSchemas[args.slotType].safeParse(args.item);
418
+ const schema = itemSchemas[args.slotType], result = v.safeParse(schema, args.item);
407
419
  if (!result.success)
408
420
  throw new SlotValueShapeError({
409
421
  slotType: args.slotType,
410
422
  slotId: args.slotId,
411
- issues: formatIssues(result.error.issues),
423
+ issues: formatIssues(result.issues),
412
424
  mode: "item"
413
425
  });
414
426
  }
415
427
  function formatIssues(issues) {
416
- return issues.map((i) => `${i.path.length > 0 ? `at ${i.path.join(".")}: ` : ""}${i.message}`);
428
+ return issues.map((i) => {
429
+ const keys = i.path?.map((p) => p.key) ?? [];
430
+ return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
431
+ });
417
432
  }
418
433
  function derivePerspectiveFromState(slots) {
419
434
  if (slots !== void 0) {
@@ -458,7 +473,7 @@ async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
458
473
  state: stateMapFromResolved(ctx.resolvedState ?? []),
459
474
  stage: ctx.stageId ?? null,
460
475
  task: ctx.taskId ?? null,
461
- now: (/* @__PURE__ */ new Date()).toISOString(),
476
+ now: ctx.now,
462
477
  engineTags: ctx.tags ?? []
463
478
  });
464
479
  try {
@@ -475,7 +490,7 @@ async function resolveSlotValue(slot, initialState, ctx, defaultValue) {
475
490
  const source = slot.source;
476
491
  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
492
  }
478
- function buildResolvedSlot(slot, value, _key) {
493
+ function buildResolvedSlot(slot, value, _key, now) {
479
494
  const titleProp = slot.title !== void 0 ? { title: slot.title } : {}, descriptionProp = slot.description !== void 0 ? { description: slot.description } : {};
480
495
  return slot.type === "workflow.state.query" ? {
481
496
  _key,
@@ -484,7 +499,7 @@ function buildResolvedSlot(slot, value, _key) {
484
499
  ...titleProp,
485
500
  ...descriptionProp,
486
501
  value,
487
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
502
+ resolvedAt: now
488
503
  } : {
489
504
  _key,
490
505
  _type: slot.type,
@@ -496,7 +511,7 @@ function buildResolvedSlot(slot, value, _key) {
496
511
  }
497
512
  async function resolveOneSlot(slot, initialState, ctx, randomKey2) {
498
513
  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());
514
+ return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2(), ctx.now);
500
515
  }
501
516
  function stateMapFromResolved(slots) {
502
517
  const out = {};
@@ -510,10 +525,10 @@ function assertInitValueShape(slot, value) {
510
525
  }
511
526
  if (slot.type === "workflow.state.release.ref") {
512
527
  assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
513
- const v = value;
514
- if (typeof v.releaseName != "string" || v.releaseName.length === 0)
528
+ const v2 = value;
529
+ if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
515
530
  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)}.`
531
+ `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
532
  );
518
533
  return;
519
534
  }
@@ -531,18 +546,18 @@ function assertGdrShape(value, context) {
531
546
  throw new Error(
532
547
  `Invalid GDR for ${context}: expected { id: "<scheme>:...", type: "<schema>" }, got ${typeof value}.`
533
548
  );
534
- const v = value;
535
- if (typeof v.id != "string" || !isGdrUri(v.id))
549
+ const v2 = value;
550
+ if (typeof v2.id != "string" || !isGdrUri(v2.id))
536
551
  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.`
552
+ `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
553
  );
539
- if (typeof v.type != "string" || v.type.length === 0)
554
+ if (typeof v2.type != "string" || v2.type.length === 0)
540
555
  throw new Error(
541
- `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v.type)}.`
556
+ `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
542
557
  );
543
558
  }
544
559
  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;
560
+ 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
561
  }
547
562
  function toGdrUri(docId, workflowResource) {
548
563
  return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
@@ -567,22 +582,24 @@ async function loadContext(client, instanceId, options) {
567
582
  const instance = await client.getDocument(instanceId);
568
583
  if (!instance)
569
584
  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 };
585
+ const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition });
586
+ return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
572
587
  }
573
588
  function ctxEvaluateFilter(ctx, filter) {
574
589
  return evaluateFilter({
575
590
  filter,
576
591
  definition: ctx.definition,
577
592
  snapshot: ctx.snapshot,
578
- params: buildParams(ctx.instance)
593
+ params: buildParams(ctx.instance, ctx.now)
579
594
  });
580
595
  }
581
596
  async function buildEngineContext(args) {
582
- const { client, clientForGdr, instance, definition } = args;
597
+ const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
583
598
  return {
584
599
  client,
585
600
  clientForGdr,
601
+ clock,
602
+ now: clock(),
586
603
  instance,
587
604
  definition,
588
605
  snapshot: await hydrateSnapshot({ client, clientForGdr, instance, definition })
@@ -595,12 +612,13 @@ function findStage(definition, stageId) {
595
612
  return stage;
596
613
  }
597
614
  async function resolveStageStateSlots(args) {
598
- const { client, instance, stage, initialState } = args;
615
+ const { client, instance, stage, now, initialState } = args;
599
616
  return resolveDeclaredState({
600
617
  slotDefs: stage.state,
601
618
  initialState: initialState ?? [],
602
619
  ctx: {
603
620
  client,
621
+ now,
604
622
  selfId: instance._id,
605
623
  tags: instance.tags ?? [],
606
624
  stageId: stage.id,
@@ -615,12 +633,13 @@ async function resolveStageStateSlots(args) {
615
633
  });
616
634
  }
617
635
  async function resolveTaskStateSlots(args) {
618
- const { client, instance, stage, task } = args;
636
+ const { client, instance, stage, task, now } = args;
619
637
  return resolveDeclaredState({
620
638
  slotDefs: task.state,
621
639
  initialState: [],
622
640
  ctx: {
623
641
  client,
642
+ now,
624
643
  selfId: instance._id,
625
644
  tags: instance.tags ?? [],
626
645
  stageId: stage.id,
@@ -648,16 +667,24 @@ function slotValue$1(slot) {
648
667
  if (slot != null && typeof slot == "object")
649
668
  return slot.value;
650
669
  }
651
- function resolveSource(src, ctx) {
670
+ function resolveStaticSource(src, ctx) {
652
671
  switch (src.source) {
653
672
  case "literal":
654
- return src.value;
673
+ return { handled: !0, value: src.value };
655
674
  case "param":
656
- return ctx.params?.[src.paramId];
675
+ return { handled: !0, value: ctx.params?.[src.paramId] };
657
676
  case "actor":
658
- return ctx.actor;
677
+ return { handled: !0, value: ctx.actor };
659
678
  case "now":
660
- return (/* @__PURE__ */ new Date()).toISOString();
679
+ return { handled: !0, value: ctx.now };
680
+ default:
681
+ return { handled: !1 };
682
+ }
683
+ }
684
+ function resolveSource(src, ctx) {
685
+ const staticValue = resolveStaticSource(src, ctx);
686
+ if (staticValue.handled) return staticValue.value;
687
+ switch (src.source) {
661
688
  case "self":
662
689
  return ctx.instance._id;
663
690
  case "stageId":
@@ -668,6 +695,11 @@ function resolveSource(src, ctx) {
668
695
  return resolveEffectOutput(src, ctx);
669
696
  case "object":
670
697
  return resolveObject(src, ctx);
698
+ case "row":
699
+ case "parentState":
700
+ return;
701
+ default:
702
+ return;
671
703
  }
672
704
  }
673
705
  function resolveStateRead(src, ctx) {
@@ -687,10 +719,11 @@ function resolveObject(src, ctx) {
687
719
  return out;
688
720
  }
689
721
  async function resolveBindings(args) {
690
- const { bindings, staticInput, instance, params, actor } = args, resolved = {};
722
+ const { bindings, staticInput, instance, now, params, actor } = args, resolved = {};
691
723
  if (bindings) {
692
724
  const ctx = {
693
725
  instance,
726
+ now,
694
727
  ...params !== void 0 ? { params } : {},
695
728
  ...actor !== void 0 ? { actor } : {}
696
729
  };
@@ -737,16 +770,29 @@ function buildInstanceBase(args) {
737
770
  lastChangedAt: now
738
771
  };
739
772
  }
773
+ const GUARD_DOC_TYPE = "temp.system.guard";
774
+ class MutationGuardDeniedError extends Error {
775
+ denied;
776
+ documentId;
777
+ action;
778
+ constructor(args) {
779
+ const ids = args.denied.map((d) => d.guardId).join(", ");
780
+ super(`Mutation on "${args.documentId}" (${args.action}) denied by guard(s) [${ids}]`), this.name = "MutationGuardDeniedError", this.denied = args.denied, this.documentId = args.documentId, this.action = args.action;
781
+ }
782
+ }
740
783
  function lakeGuardId(args) {
741
- return `temp.system.guard.${args.instanceDocId}.${args.stageId}.${args.index}`;
784
+ return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
742
785
  }
743
786
  function compileGuard(args) {
744
787
  return {
745
788
  _id: args.id,
746
- _type: "temp.system.guard",
789
+ _type: GUARD_DOC_TYPE,
747
790
  resourceType: args.resourceType,
748
791
  resourceId: args.resourceId,
749
792
  owner: args.owner,
793
+ sourceInstanceId: args.sourceInstanceId,
794
+ sourceDefinitionId: args.sourceDefinitionId,
795
+ sourceStageId: args.sourceStageId,
750
796
  ...args.name !== void 0 ? { name: args.name } : {},
751
797
  ...args.description !== void 0 ? { description: args.description } : {},
752
798
  match: args.match,
@@ -802,9 +848,9 @@ function resolveIdRefTarget(src, ctx) {
802
848
  if (typeof value == "string" && isGdrUri(value))
803
849
  return { parsed: parseGdr(value) };
804
850
  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) };
851
+ const v2 = value;
852
+ if (typeof v2.id == "string" && isGdrUri(v2.id))
853
+ return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
808
854
  }
809
855
  return null;
810
856
  }
@@ -843,8 +889,8 @@ function resolveMetadata(metadata, ctx) {
843
889
  return out;
844
890
  }
845
891
  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);
892
+ function resolveGuard(guard, index, instance, stageId, now) {
893
+ const ctx = { instance, stageId, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
848
894
  if (targets === null) return null;
849
895
  const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
850
896
  return { doc: compileGuard({
@@ -852,6 +898,9 @@ function resolveGuard(guard, index, instance, stageId) {
852
898
  resourceType: resource.type,
853
899
  resourceId: resource.id,
854
900
  owner: GUARD_OWNER,
901
+ sourceInstanceId: instance._id,
902
+ sourceDefinitionId: instance.workflowId,
903
+ sourceStageId: stageId,
855
904
  ...guard.name !== void 0 ? { name: guard.name } : {},
856
905
  ...guard.description !== void 0 ? { description: guard.description } : {},
857
906
  match: {
@@ -865,47 +914,108 @@ function resolveGuard(guard, index, instance, stageId) {
865
914
  }), routeGdr: targets[0].parsed };
866
915
  }
867
916
  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
875
- };
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);
917
+ if (!await client.getDocument(doc._id)) {
918
+ await client.create(doc);
919
+ return;
920
+ }
921
+ const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
922
+ await client.patch(doc._id).set(body).commit();
877
923
  }
878
924
  function resolvedStageGuards(args) {
879
925
  const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
880
926
  for (const [index, guard] of (stage?.guards ?? []).entries()) {
881
- const resolved = resolveGuard(guard, index, args.instance, args.stageId);
927
+ const resolved = resolveGuard(guard, index, args.instance, args.stageId, args.now);
882
928
  resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
883
929
  }
884
930
  return out;
885
931
  }
932
+ async function committedStageId(args) {
933
+ return (await args.client.getDocument(args.instance._id))?.currentStageId;
934
+ }
886
935
  async function deployStageGuards(args) {
887
- for (const { client, doc } of resolvedStageGuards(args))
888
- await upsertGuard(client, doc);
936
+ if (await committedStageId(args) === args.stageId)
937
+ for (const { client, doc } of resolvedStageGuards(args))
938
+ await upsertGuard(client, doc);
889
939
  }
890
940
  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();
941
+ const live = await committedStageId(args);
942
+ if (!(live === void 0 || live === args.stageId))
943
+ for (const { client, doc } of resolvedStageGuards(args))
944
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
893
945
  }
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
- });
946
+ class ActionParamsInvalidError extends Error {
947
+ action;
948
+ taskId;
949
+ issues;
950
+ constructor(args) {
951
+ const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
952
+ `);
953
+ super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
954
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
955
+ }
956
+ }
957
+ class WorkflowStateDivergedError extends Error {
958
+ instanceId;
959
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
960
+ guardError;
961
+ /** The rollback failure, when rollback was attempted and itself failed. */
962
+ rollbackError;
963
+ constructor(args) {
964
+ super(
965
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
966
+ { cause: args.guardError }
967
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
968
+ }
969
+ }
970
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
971
+ class ConcurrentFireActionError extends Error {
972
+ instanceId;
973
+ taskId;
974
+ action;
975
+ attempts;
976
+ constructor(args) {
977
+ super(
978
+ `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.`
979
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.taskId = args.taskId, this.action = args.action, this.attempts = args.attempts;
980
+ }
981
+ }
982
+ function isRevisionConflict(error) {
983
+ if (typeof error != "object" || error === null) return !1;
984
+ const { statusCode, message } = error;
985
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
986
+ }
987
+ class CascadeLimitError extends Error {
988
+ instanceId;
989
+ limit;
990
+ constructor(args) {
991
+ super(
992
+ `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.`
993
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
994
+ }
995
+ }
996
+ async function deployOrRollback(args) {
997
+ try {
998
+ await args.deploy();
999
+ } catch (guardError) {
1000
+ if (!args.reversible)
1001
+ throw new WorkflowStateDivergedError({
1002
+ instanceId: args.instanceId,
1003
+ guardError,
1004
+ reason: "the move created child instances a parent-only rollback cannot undo"
1005
+ });
1006
+ try {
1007
+ let patch = args.client.patch(args.instanceId).set(args.restore);
1008
+ args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1009
+ } catch (rollbackError) {
1010
+ throw new WorkflowStateDivergedError({
1011
+ instanceId: args.instanceId,
1012
+ guardError,
1013
+ rollbackError,
1014
+ reason: "rollback of the committed move failed"
1015
+ });
1016
+ }
1017
+ throw guardError;
1018
+ }
909
1019
  }
910
1020
  async function discoverItems(args) {
911
1021
  const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
@@ -922,31 +1032,29 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
922
1032
  const MAX_SPAWN_DEPTH = 6;
923
1033
  async function spawnChildren(ctx, mutation, task, spawn, actor) {
924
1034
  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 ");
1035
+ const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
929
1036
  throw new Error(
930
1037
  `Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
931
1038
  );
932
1039
  }
933
- const rows = await resolveSpawnRows(ctx, spawn), refs = [];
1040
+ const now = ctx.now, rows = await resolveSpawnRows(ctx, spawn), refs = [];
934
1041
  for (const row of rows) {
935
- const projected = projectSpawnRow(ctx.instance, spawn, row), { ref, body } = await prepareChildInstance({
1042
+ const projected = projectSpawnRow(ctx.instance, spawn, row, now), { ref, body } = await prepareChildInstance({
936
1043
  client: ctx.client,
937
1044
  parent: ctx.instance,
938
1045
  task,
939
1046
  spawn,
940
1047
  initialState: projected.initialState,
941
1048
  effectsContext: projected.effectsContext,
942
- actor
1049
+ actor,
1050
+ now
943
1051
  });
944
1052
  refs.push(ref), mutation.pendingCreates.push(body);
945
1053
  }
946
1054
  return refs;
947
1055
  }
948
1056
  async function resolveSpawnRows(ctx, spawn) {
949
- const params = buildParams(ctx.instance), groq = spawn.forEach.groq;
1057
+ const params = buildParams(ctx.instance, ctx.now), groq = spawn.forEach.groq;
950
1058
  let value;
951
1059
  if (groq.includes("*")) {
952
1060
  const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
@@ -968,7 +1076,7 @@ function firstDocRefGdrUri(slots) {
968
1076
  if (s._type === "workflow.state.doc.ref" && s.value !== null) return s.value.id;
969
1077
  }
970
1078
  }
971
- function resolveSpawnSource(source, parent, row) {
1079
+ function resolveSpawnSource(source, parent, row, now) {
972
1080
  switch (source.source) {
973
1081
  case "literal":
974
1082
  return source.value;
@@ -979,15 +1087,15 @@ function resolveSpawnSource(source, parent, row) {
979
1087
  return slot === void 0 ? null : source.path !== void 0 ? getPath(slot, source.path) : slot;
980
1088
  }
981
1089
  case "self":
982
- return gdrFromResource(parent.workflowResource, parent._id);
1090
+ return selfGdr(parent);
983
1091
  case "stageId":
984
1092
  return parent.currentStageId;
985
1093
  case "now":
986
- return (/* @__PURE__ */ new Date()).toISOString();
1094
+ return now;
987
1095
  case "object": {
988
1096
  const out = {};
989
- for (const [k, v] of Object.entries(source.fields))
990
- out[k] = resolveSpawnSource(v, parent, row);
1097
+ for (const [k, v2] of Object.entries(source.fields))
1098
+ out[k] = resolveSpawnSource(v2, parent, row, now);
991
1099
  return out;
992
1100
  }
993
1101
  case "actor":
@@ -997,10 +1105,10 @@ function resolveSpawnSource(source, parent, row) {
997
1105
  return null;
998
1106
  }
999
1107
  }
1000
- function projectSpawnRow(parent, spawn, row) {
1108
+ function projectSpawnRow(parent, spawn, row, now) {
1001
1109
  const initialState = [];
1002
1110
  for (const entry of spawn.forEach.as.initialState ?? []) {
1003
- const raw = resolveSpawnSource(entry.value, parent, row), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1111
+ const raw = resolveSpawnSource(entry.value, parent, row, now), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1004
1112
  initialState.push({
1005
1113
  type: entry.type,
1006
1114
  id: entry.id,
@@ -1009,13 +1117,13 @@ function projectSpawnRow(parent, spawn, row) {
1009
1117
  }
1010
1118
  const effectsContext = {};
1011
1119
  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);
1120
+ const v2 = resolveSpawnSource(src, parent, row, now);
1121
+ v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (effectsContext[k] = v2);
1014
1122
  }
1015
1123
  return { initialState, effectsContext };
1016
1124
  }
1017
1125
  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;
1126
+ 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
1127
  }
1020
1128
  function coerceSpawnGdr(raw, workflowResource) {
1021
1129
  const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
@@ -1038,7 +1146,7 @@ async function resolveLogicalDefinition(client, ref, tags) {
1038
1146
  return await client.fetch(groq, params) ?? null;
1039
1147
  }
1040
1148
  async function prepareChildInstance(args) {
1041
- const { client, parent, task, spawn, initialState, effectsContext, actor } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1149
+ const { client, parent, task, spawn, initialState, effectsContext, actor, now } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1042
1150
  if (definition === null) {
1043
1151
  const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
1044
1152
  throw new Error(
@@ -1048,7 +1156,7 @@ async function prepareChildInstance(args) {
1048
1156
  const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: "workflow.instance" }, ancestors = [
1049
1157
  ...parent.ancestors,
1050
1158
  {
1051
- id: gdrFromResource(parent.workflowResource, parent._id),
1159
+ id: selfGdr(parent),
1052
1160
  type: "workflow.instance"
1053
1161
  }
1054
1162
  ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
@@ -1056,6 +1164,7 @@ async function prepareChildInstance(args) {
1056
1164
  initialState,
1057
1165
  ctx: {
1058
1166
  client,
1167
+ now,
1059
1168
  selfId: childDocId,
1060
1169
  tags: childTags,
1061
1170
  workflowResource,
@@ -1075,7 +1184,7 @@ async function prepareChildInstance(args) {
1075
1184
  }
1076
1185
  ), base = buildInstanceBase({
1077
1186
  id: childDocId,
1078
- now: (/* @__PURE__ */ new Date()).toISOString(),
1187
+ now,
1079
1188
  tags: childTags,
1080
1189
  workflowResource,
1081
1190
  workflowId: definition.workflowId,
@@ -1111,7 +1220,9 @@ async function checkSpawnCompletion(client, entry, spawn) {
1111
1220
  }
1112
1221
  }
1113
1222
  async function invokeTask(args) {
1114
- const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId);
1223
+ const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId, {
1224
+ ...options?.clock ? { clock: options.clock } : {}
1225
+ });
1115
1226
  return commitInvoke(ctx, taskId, options?.actor);
1116
1227
  }
1117
1228
  async function commitInvoke(ctx, taskId, actor) {
@@ -1123,41 +1234,41 @@ async function commitInvoke(ctx, taskId, actor) {
1123
1234
  const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1124
1235
  return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, taskId };
1125
1236
  }
1126
- async function autoResolveOnActivate(ctx, mutation, task, entry) {
1237
+ async function autoResolveOnActivate(ctx, mutation, task, entry, now) {
1127
1238
  const checks = [
1128
1239
  { filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
1129
1240
  { filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
1130
1241
  ];
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
- }
1242
+ for (const { filter, outcome, systemId } of checks)
1243
+ if (filter !== void 0 && await ctxEvaluateFilter(ctx, filter))
1244
+ return entry.status = outcome, entry.completedAt = now, entry.completedBy = systemId, mutation.history.push(
1245
+ taskStatusChangedEntry({
1246
+ stageId: mutation.currentStageId,
1247
+ taskId: task.id,
1248
+ from: "active",
1249
+ to: outcome,
1250
+ at: now,
1251
+ actor: { kind: "system", id: systemId }
1252
+ })
1253
+ ), !0;
1145
1254
  return !1;
1146
1255
  }
1147
1256
  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) {
1257
+ const now = ctx.now;
1258
+ if (entry.status = "active", entry.startedAt = now, task.state !== void 0 && task.state.length > 0) {
1149
1259
  const stage = findStage(ctx.definition, mutation.currentStageId);
1150
1260
  entry.state = await resolveTaskStateSlots({
1151
1261
  client: ctx.client,
1152
1262
  instance: ctx.instance,
1153
1263
  stage,
1154
- task
1264
+ task,
1265
+ now
1155
1266
  });
1156
1267
  }
1157
- if (!await autoResolveOnActivate(ctx, mutation, task, entry) && (mutation.history.push({
1268
+ if (!await autoResolveOnActivate(ctx, mutation, task, entry, now) && (mutation.history.push({
1158
1269
  _key: randomKey(),
1159
1270
  _type: "workflow.history.taskActivated",
1160
- at: (/* @__PURE__ */ new Date()).toISOString(),
1271
+ at: now,
1161
1272
  stageId: mutation.currentStageId,
1162
1273
  taskId: task.id,
1163
1274
  ...actor !== void 0 ? { actor } : {}
@@ -1177,18 +1288,19 @@ async function activateTask(ctx, mutation, task, entry, actor) {
1177
1288
  mutation.history.push({
1178
1289
  _key: randomKey(),
1179
1290
  _type: "workflow.history.spawned",
1180
- at: (/* @__PURE__ */ new Date()).toISOString(),
1291
+ at: now,
1181
1292
  taskId: task.id,
1182
1293
  instanceRef: ref
1183
1294
  });
1184
1295
  if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
1185
1296
  const previousStatus = entry.status;
1186
- entry.status = "done", entry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1297
+ entry.status = "done", entry.completedAt = now, mutation.history.push(
1187
1298
  taskStatusChangedEntry({
1188
1299
  stageId: mutation.currentStageId,
1189
1300
  taskId: task.id,
1190
1301
  from: previousStatus,
1191
1302
  to: "done",
1303
+ at: now,
1192
1304
  ...actor !== void 0 ? { actor } : {}
1193
1305
  })
1194
1306
  );
@@ -1205,7 +1317,7 @@ async function buildStageTasks(ctx, stage) {
1205
1317
  status: inScope ? "pending" : "skipped",
1206
1318
  ...task.filter !== void 0 ? {
1207
1319
  filterEvaluation: {
1208
- at: (/* @__PURE__ */ new Date()).toISOString(),
1320
+ at: ctx.now,
1209
1321
  truthy: inScope
1210
1322
  }
1211
1323
  } : {}
@@ -1217,7 +1329,9 @@ function activatesOnStageEnter(task) {
1217
1329
  return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
1218
1330
  }
1219
1331
  async function setStage(args) {
1220
- const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId);
1332
+ const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId, {
1333
+ ...options?.clock ? { clock: options.clock } : {}
1334
+ });
1221
1335
  return commitSetStage(ctx, targetStageId, reason, options?.actor);
1222
1336
  }
1223
1337
  async function enterStage(ctx, mutation, nextStage, actor, at) {
@@ -1229,7 +1343,8 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1229
1343
  state: await resolveStageStateSlots({
1230
1344
  client: ctx.client,
1231
1345
  instance: ctx.instance,
1232
- stage: nextStage
1346
+ stage: nextStage,
1347
+ now: at
1233
1348
  }),
1234
1349
  tasks: await buildStageTasks(ctx, nextStage)
1235
1350
  };
@@ -1248,7 +1363,7 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1248
1363
  nextStage.kind === "terminal" && (mutation.completedAt = at);
1249
1364
  }
1250
1365
  async function beginStageMove(ctx, currentStage, actor) {
1251
- const mutation = startMutation(ctx.instance), at = (/* @__PURE__ */ new Date()).toISOString();
1366
+ const mutation = startMutation(ctx.instance), at = ctx.now;
1252
1367
  return await queueEffects(
1253
1368
  ctx,
1254
1369
  mutation,
@@ -1282,13 +1397,46 @@ async function commitSetStage(ctx, targetStageId, reason, actor) {
1282
1397
  };
1283
1398
  }
1284
1399
  async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1285
- await persist(ctx, mutation), await reconcileStageGuards({
1400
+ await persistThenDeploy(
1401
+ ctx,
1402
+ mutation,
1403
+ () => deployStageGuards({
1404
+ client: ctx.client,
1405
+ clientForGdr: ctx.clientForGdr,
1406
+ instance: materializeInstance(ctx.instance, mutation),
1407
+ definition: ctx.definition,
1408
+ stageId: enteredStageId,
1409
+ now: ctx.now
1410
+ })
1411
+ ), await retractStageGuards({
1286
1412
  client: ctx.client,
1287
1413
  clientForGdr: ctx.clientForGdr,
1288
1414
  instance: ctx.instance,
1289
1415
  definition: ctx.definition,
1290
- exitedStageId,
1291
- enteredStageId
1416
+ stageId: exitedStageId,
1417
+ now: ctx.now
1418
+ });
1419
+ }
1420
+ async function persistThenDeploy(ctx, mutation, deploy) {
1421
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1422
+ await deployOrRollback({
1423
+ client: ctx.client,
1424
+ instanceId: ctx.instance._id,
1425
+ committedRev: committed._rev,
1426
+ restore: instanceStateFields(ctx.instance),
1427
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1428
+ reversible: !spawned,
1429
+ deploy
1430
+ });
1431
+ }
1432
+ async function refreshStageGuards(ctx, mutation, stageId) {
1433
+ await deployStageGuards({
1434
+ client: ctx.client,
1435
+ clientForGdr: ctx.clientForGdr,
1436
+ instance: materializeInstance(ctx.instance, mutation),
1437
+ definition: ctx.definition,
1438
+ stageId,
1439
+ now: ctx.now
1292
1440
  });
1293
1441
  }
1294
1442
  async function commitTransition(ctx, action, actor) {
@@ -1330,15 +1478,18 @@ async function pickTransition(ctx, stage, action) {
1330
1478
  if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
1331
1479
  }
1332
1480
  async function evaluateAutoTransitions(args) {
1333
- const { client, instanceId, options, clientForGdr } = args, ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0);
1481
+ const { client, instanceId, options, clientForGdr, clock } = args, ctx = await loadContext(client, instanceId, {
1482
+ ...clientForGdr ? { clientForGdr } : {},
1483
+ ...clock ? { clock } : {}
1484
+ });
1334
1485
  return commitTransition(ctx, void 0, options?.actor);
1335
1486
  }
1336
- async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1487
+ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
1337
1488
  const instance = await client.getDocument(instanceId);
1338
1489
  if (!instance || instance.stages.length > 0 || instance.pendingEffects.length > 0) return;
1339
1490
  const definition = JSON.parse(instance.definitionSnapshot), stage = definition.stages.find((s) => s.id === instance.currentStageId);
1340
1491
  if (stage === void 0) return;
1341
- const snapshot = await hydrateSnapshot({
1492
+ const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
1342
1493
  client,
1343
1494
  clientForGdr: clientForGdr ?? (() => client),
1344
1495
  instance,
@@ -1349,7 +1500,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1349
1500
  filter: task.filter,
1350
1501
  definition,
1351
1502
  snapshot,
1352
- params: buildParams(instance)
1503
+ params: buildParams(instance, now)
1353
1504
  });
1354
1505
  tasks.push({
1355
1506
  _key: randomKey(),
@@ -1360,8 +1511,8 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1360
1511
  const initialStageEntry = {
1361
1512
  _key: randomKey(),
1362
1513
  id: stage.id,
1363
- enteredAt: (/* @__PURE__ */ new Date()).toISOString(),
1364
- state: await resolveStageStateSlots({ client, instance, stage }),
1514
+ enteredAt: now,
1515
+ state: await resolveStageStateSlots({ client, instance, stage, now }),
1365
1516
  tasks
1366
1517
  }, pendingEffects = [], newHistory = [...instance.history];
1367
1518
  for (const effect of stage.onEnter ?? []) {
@@ -1369,29 +1520,44 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1369
1520
  bindings: effect.bindings,
1370
1521
  staticInput: effect.input,
1371
1522
  instance,
1523
+ now,
1372
1524
  ...actor !== void 0 ? { actor } : {}
1373
1525
  }), { pending, history } = buildQueuedEffect(
1374
1526
  effect,
1375
1527
  { kind: "stageEnter", id: stage.id },
1376
1528
  params,
1377
- actor
1529
+ actor,
1530
+ now
1378
1531
  );
1379
1532
  pendingEffects.push(pending), newHistory.push(history);
1380
1533
  }
1381
- await client.patch(instance._id).set({
1534
+ const committed = await client.patch(instance._id).set({
1382
1535
  stages: [initialStageEntry],
1383
1536
  pendingEffects,
1384
1537
  history: newHistory,
1385
- lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1386
- }).ifRevisionId(instance._rev).commit(), await deployStageGuards({
1538
+ lastChangedAt: now
1539
+ }).ifRevisionId(instance._rev).commit();
1540
+ await deployOrRollback({
1387
1541
  client,
1388
- clientForGdr: clientForGdr ?? (() => client),
1389
- instance,
1390
- definition,
1391
- stageId: stage.id
1392
- }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr);
1542
+ instanceId: instance._id,
1543
+ committedRev: committed._rev,
1544
+ restore: {
1545
+ stages: instance.stages,
1546
+ pendingEffects: instance.pendingEffects,
1547
+ history: instance.history
1548
+ },
1549
+ reversible: !0,
1550
+ deploy: () => deployStageGuards({
1551
+ client,
1552
+ clientForGdr: clientForGdr ?? (() => client),
1553
+ instance,
1554
+ definition,
1555
+ stageId: stage.id,
1556
+ now
1557
+ })
1558
+ }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
1393
1559
  }
1394
- async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr) {
1560
+ async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
1395
1561
  const resolvedClientForGdr = clientForGdr ?? (() => client);
1396
1562
  for (const task of stage.tasks ?? []) {
1397
1563
  if (!activatesOnStageEnter(task)) continue;
@@ -1401,7 +1567,8 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
1401
1567
  client,
1402
1568
  clientForGdr: resolvedClientForGdr,
1403
1569
  instance: updated,
1404
- definition
1570
+ definition,
1571
+ ...clock ? { clock } : {}
1405
1572
  }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1406
1573
  mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
1407
1574
  }
@@ -1419,8 +1586,11 @@ async function gatherAutoResolveCandidates(ctx, tasks) {
1419
1586
  }
1420
1587
  return candidates;
1421
1588
  }
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(
1589
+ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock) {
1590
+ const ctx = await loadContext(client, instanceId, {
1591
+ ...clientForGdr ? { clientForGdr } : {},
1592
+ ...clock ? { clock } : {}
1593
+ }), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1424
1594
  (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
1425
1595
  );
1426
1596
  if (tasksWithAutoPredicate.length === 0) return 0;
@@ -1434,65 +1604,71 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr) {
1434
1604
  const previousStatus = mutEntry.status, actor = {
1435
1605
  kind: "system",
1436
1606
  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(
1607
+ }, now = ctx.now;
1608
+ mutEntry.status = outcome, mutEntry.completedAt = now, mutEntry.completedBy = actor.id, mutation.history.push(
1439
1609
  taskStatusChangedEntry({
1440
1610
  stageId: stage.id,
1441
1611
  taskId: task.id,
1442
1612
  from: previousStatus,
1443
1613
  to: outcome,
1614
+ at: now,
1444
1615
  actor
1445
1616
  })
1446
1617
  ), count++;
1447
1618
  }
1448
1619
  return count > 0 && await persist(ctx, mutation), count;
1449
1620
  }
1450
- async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr) {
1621
+ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock) {
1451
1622
  let count = 0;
1452
1623
  for (; ; ) {
1453
- if (await resolveCompleteWhen(client, instanceId, clientForGdr), !(await evaluateAutoTransitions({
1624
+ if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock), !(await evaluateAutoTransitions({
1454
1625
  client,
1455
1626
  instanceId,
1456
1627
  ...actor !== void 0 ? { options: { actor } } : {},
1457
- ...clientForGdr ? { clientForGdr } : {}
1628
+ ...clientForGdr ? { clientForGdr } : {},
1629
+ ...clock ? { clock } : {}
1458
1630
  })).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
- );
1631
+ if (count++, count >= CASCADE_LIMIT)
1632
+ throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
1463
1633
  }
1464
1634
  }
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];
1635
+ async function findSatisfiedParentSpawn(client, child) {
1636
+ const parentRef = child.ancestors.at(-1);
1469
1637
  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({
1638
+ const parent = await client.getDocument(gdrToBareId(parentRef.id));
1639
+ if (!parent || parent._type !== "workflow.instance" || typeof parent.definitionSnapshot != "string") return;
1640
+ const definition = JSON.parse(parent.definitionSnapshot), stage = definition.stages.find((s) => s.id === parent.currentStageId);
1641
+ if (stage === void 0) return;
1642
+ 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);
1643
+ if (task?.spawns === void 0) return;
1644
+ const entry = parentCurrentTasks.find((e) => e.id === task.id);
1645
+ if (!(entry === void 0 || entry.status === "done") && await checkSpawnCompletion(client, entry, task.spawns))
1646
+ return { parent, definition, stage, task };
1647
+ }
1648
+ async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
1649
+ const instance = await client.getDocument(instanceId);
1650
+ if (!instance) return;
1651
+ const found = await findSatisfiedParentSpawn(client, instance);
1652
+ if (found === void 0) return;
1653
+ const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
1480
1654
  client,
1481
1655
  clientForGdr: clientForGdr ?? (() => client),
1482
1656
  instance: parent,
1483
- definition: parentDefinition
1484
- }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === parentTask.id);
1657
+ definition,
1658
+ ...clock ? { clock } : {}
1659
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === task.id);
1485
1660
  if (mutEntry === void 0) return;
1486
- const previousStatus = mutEntry.status;
1487
- mutEntry.status = "done", mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1661
+ const previousStatus = mutEntry.status, now = ctx.now;
1662
+ mutEntry.status = "done", mutEntry.completedAt = now, mutation.history.push(
1488
1663
  taskStatusChangedEntry({
1489
- stageId: parentStage.id,
1490
- taskId: parentTask.id,
1664
+ stageId: stage.id,
1665
+ taskId: task.id,
1491
1666
  from: previousStatus,
1492
1667
  to: "done",
1668
+ at: now,
1493
1669
  ...actor !== void 0 ? { actor } : {}
1494
1670
  })
1495
- ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parentId, actor, clientForGdr), await propagateToAncestors(client, parentId, actor, clientForGdr);
1671
+ ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock);
1496
1672
  }
1497
1673
  function startMutation(instance) {
1498
1674
  return {
@@ -1521,7 +1697,7 @@ function taskStatusChangedEntry(args) {
1521
1697
  return {
1522
1698
  _key: randomKey(),
1523
1699
  _type: "workflow.history.taskStatusChanged",
1524
- at: args.at ?? (/* @__PURE__ */ new Date()).toISOString(),
1700
+ at: args.at,
1525
1701
  stageId: args.stageId,
1526
1702
  taskId: args.taskId,
1527
1703
  from: args.from,
@@ -1561,19 +1737,31 @@ function stageTransitionHistory(args) {
1561
1737
  }
1562
1738
  ];
1563
1739
  }
1564
- async function persist(ctx, mutation) {
1565
- const set = {
1740
+ function instanceStateFields(src) {
1741
+ const fields = {
1742
+ currentStageId: src.currentStageId,
1743
+ state: src.state,
1744
+ stages: src.stages,
1745
+ pendingEffects: src.pendingEffects,
1746
+ effectHistory: src.effectHistory,
1747
+ effectsContext: src.effectsContext,
1748
+ history: src.history
1749
+ };
1750
+ return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
1751
+ }
1752
+ function materializeInstance(base, mutation) {
1753
+ return {
1754
+ ...base,
1566
1755
  currentStageId: mutation.currentStageId,
1567
1756
  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()
1757
+ stages: mutation.stages
1574
1758
  };
1575
- mutation.completedAt !== void 0 && (set.completedAt = mutation.completedAt);
1576
- const pendingCreates = mutation.pendingCreates;
1759
+ }
1760
+ async function persist(ctx, mutation) {
1761
+ const set = {
1762
+ ...instanceStateFields(mutation),
1763
+ lastChangedAt: ctx.now
1764
+ }, pendingCreates = mutation.pendingCreates;
1577
1765
  if (pendingCreates.length === 0)
1578
1766
  return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1579
1767
  const tx = ctx.client.transaction();
@@ -1581,7 +1769,7 @@ async function persist(ctx, mutation) {
1581
1769
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1582
1770
  const actorForPriming = void 0;
1583
1771
  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);
1772
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1585
1773
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1586
1774
  if (!reloaded)
1587
1775
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1622,7 +1810,9 @@ function findCurrentTasks(instance) {
1622
1810
  return findCurrentStageEntry(instance)?.tasks ?? [];
1623
1811
  }
1624
1812
  async function completeEffect(args) {
1625
- const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId);
1813
+ const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1814
+ ...options?.clock ? { clock: options.clock } : {}
1815
+ });
1626
1816
  return commitCompleteEffect(
1627
1817
  ctx,
1628
1818
  effectKey,
@@ -1664,7 +1854,7 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1664
1854
  throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1665
1855
  const mutation = startMutation(ctx.instance);
1666
1856
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1667
- const ranAt = (/* @__PURE__ */ new Date()).toISOString();
1857
+ const ranAt = ctx.now;
1668
1858
  return mutation.effectHistory.push(
1669
1859
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1670
1860
  ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
@@ -1679,7 +1869,7 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1679
1869
  ...actor !== void 0 ? { actor } : {}
1680
1870
  }), await persist(ctx, mutation), { effectKey, status };
1681
1871
  }
1682
- function buildQueuedEffect(effect, origin, params, actor) {
1872
+ function buildQueuedEffect(effect, origin, params, actor, now) {
1683
1873
  const key = randomKey(), pending = {
1684
1874
  _key: key,
1685
1875
  _type: "workflow.pendingEffect",
@@ -1690,11 +1880,11 @@ function buildQueuedEffect(effect, origin, params, actor) {
1690
1880
  params,
1691
1881
  origin,
1692
1882
  ...actor !== void 0 ? { actor } : {},
1693
- queuedAt: (/* @__PURE__ */ new Date()).toISOString()
1883
+ queuedAt: now
1694
1884
  }, history = {
1695
1885
  _key: randomKey(),
1696
1886
  _type: "workflow.history.effectQueued",
1697
- at: (/* @__PURE__ */ new Date()).toISOString(),
1887
+ at: now,
1698
1888
  effectKey: key,
1699
1889
  effectId: effect.id,
1700
1890
  origin
@@ -1703,7 +1893,7 @@ function buildQueuedEffect(effect, origin, params, actor) {
1703
1893
  }
1704
1894
  async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1705
1895
  if (!effects) return;
1706
- const liveInstance = {
1896
+ const now = ctx.now, liveInstance = {
1707
1897
  ...ctx.instance,
1708
1898
  state: mutation.state,
1709
1899
  stages: mutation.stages,
@@ -1714,22 +1904,21 @@ async function queueEffects(ctx, mutation, effects, origin, actor, callerParams)
1714
1904
  bindings: effect.bindings,
1715
1905
  staticInput: effect.input,
1716
1906
  instance: liveInstance,
1907
+ now,
1717
1908
  ...callerParams !== void 0 ? { params: callerParams } : {},
1718
1909
  ...actor !== void 0 ? { actor } : {}
1719
- }), { pending, history } = buildQueuedEffect(effect, origin, params, actor);
1910
+ }), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
1720
1911
  mutation.pendingEffects.push(pending), mutation.history.push(history);
1721
1912
  }
1722
1913
  }
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;
1732
- }
1914
+ const STATE_OP_TYPES = [
1915
+ "workflow.op.state.set",
1916
+ "workflow.op.state.append",
1917
+ "workflow.op.state.updateWhere",
1918
+ "workflow.op.state.removeWhere"
1919
+ ];
1920
+ function isStateOp(summary) {
1921
+ return STATE_OP_TYPES.includes(summary.opType);
1733
1922
  }
1734
1923
  function validateActionParams(action, taskId, callerParams) {
1735
1924
  const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
@@ -1766,7 +1955,7 @@ function checkParamType(value, decl) {
1766
1955
  case "doc.ref":
1767
1956
  return hasStringField(value, "id");
1768
1957
  case "doc.refs":
1769
- return Array.isArray(value) && value.every((v) => checkParamType(v, { paramType: "doc.ref" }));
1958
+ return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
1770
1959
  case "json":
1771
1960
  return !0;
1772
1961
  default:
@@ -1774,15 +1963,15 @@ function checkParamType(value, decl) {
1774
1963
  }
1775
1964
  }
1776
1965
  async function runActionOps(args) {
1777
- const { ops, mutation, stageId, taskId, actionName, params, actor } = args;
1966
+ const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
1778
1967
  if (ops === void 0 || ops.length === 0) return [];
1779
1968
  const summaries = [];
1780
1969
  for (const op of ops) {
1781
- const summary = applyOp(op, { mutation, stageId, taskId, params, actor });
1970
+ const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
1782
1971
  summaries.push(summary), mutation.history.push({
1783
1972
  _key: randomKey(),
1784
1973
  _type: "workflow.history.opApplied",
1785
- at: (/* @__PURE__ */ new Date()).toISOString(),
1974
+ at: now,
1786
1975
  stageId,
1787
1976
  taskId,
1788
1977
  actionId: actionName,
@@ -1841,8 +2030,8 @@ function applyStateUpdateWhere(op, ctx) {
1841
2030
  /*createIfMissing*/
1842
2031
  !1
1843
2032
  ), resolvedMerge = {};
1844
- for (const [k, v] of Object.entries(op.merge))
1845
- resolvedMerge[k] = resolveOpSource(v, ctx);
2033
+ for (const [k, v2] of Object.entries(op.merge))
2034
+ resolvedMerge[k] = resolveOpSource(v2, ctx);
1846
2035
  return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
1847
2036
  (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
1848
2037
  )), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
@@ -1885,18 +2074,11 @@ function locateSlot(ctx, target, createIfMissing) {
1885
2074
  }, host.push(slot)), slot;
1886
2075
  }
1887
2076
  function resolveOpSource(src, ctx) {
2077
+ const staticValue = resolveStaticSource(src, ctx);
2078
+ if (staticValue.handled) return staticValue.value;
1888
2079
  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
2080
  case "self":
1898
- return;
1899
- // Not applicable inside ops; reserved for higher-level binding.
2081
+ return ctx.self;
1900
2082
  case "stageId":
1901
2083
  return ctx.stageId;
1902
2084
  case "stateRead": {
@@ -1906,8 +2088,8 @@ function resolveOpSource(src, ctx) {
1906
2088
  case "effectOutput": {
1907
2089
  const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
1908
2090
  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;
2091
+ const v2 = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
2092
+ return v2 === void 0 ? null : v2;
1911
2093
  }
1912
2094
  case "object": {
1913
2095
  const out = {};
@@ -1915,6 +2097,11 @@ function resolveOpSource(src, ctx) {
1915
2097
  out[field] = resolveOpSource(fieldSrc, ctx);
1916
2098
  return out;
1917
2099
  }
2100
+ case "row":
2101
+ case "parentState":
2102
+ return;
2103
+ default:
2104
+ return;
1918
2105
  }
1919
2106
  }
1920
2107
  function slotValue(slots, slotId) {
@@ -1934,8 +2121,23 @@ function evalOpPredicate(p, row, ctx) {
1934
2121
  return row[p.field] === target;
1935
2122
  }
1936
2123
  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);
2124
+ const { client, instanceId, taskId, action, params, options } = args;
2125
+ for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2126
+ const ctx = await loadContext(client, instanceId, {
2127
+ ...options?.clock ? { clock: options.clock } : {}
2128
+ });
2129
+ try {
2130
+ return await commitAction(ctx, taskId, action, params, options?.actor);
2131
+ } catch (error) {
2132
+ if (!isRevisionConflict(error)) throw error;
2133
+ }
2134
+ }
2135
+ throw new ConcurrentFireActionError({
2136
+ instanceId,
2137
+ taskId,
2138
+ action,
2139
+ attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2140
+ });
1939
2141
  }
1940
2142
  async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
1941
2143
  const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
@@ -1951,15 +2153,16 @@ async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
1951
2153
  const params = validateActionParams(action, taskId, callerParams);
1952
2154
  return { stage, task, action, params };
1953
2155
  }
1954
- function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor) {
2156
+ function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor, now) {
1955
2157
  if (action.setStatus === void 0) return;
1956
2158
  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(
2159
+ return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
1958
2160
  taskStatusChangedEntry({
1959
2161
  stageId,
1960
2162
  taskId,
1961
2163
  from: initialStatus,
1962
2164
  to: newStatus,
2165
+ at: now,
1963
2166
  ...actor !== void 0 ? { actor } : {}
1964
2167
  })
1965
2168
  ), newStatus;
@@ -1970,11 +2173,11 @@ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
1970
2173
  taskId,
1971
2174
  actionName,
1972
2175
  callerParams
1973
- ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
2176
+ ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
1974
2177
  mutation.history.push({
1975
2178
  _key: randomKey(),
1976
2179
  _type: "workflow.history.actionFired",
1977
- at: (/* @__PURE__ */ new Date()).toISOString(),
2180
+ at: now,
1978
2181
  stageId: stage.id,
1979
2182
  taskId,
1980
2183
  actionId: actionName,
@@ -1987,8 +2190,10 @@ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
1987
2190
  taskId,
1988
2191
  actionName,
1989
2192
  params,
1990
- actor
1991
- }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor);
2193
+ actor,
2194
+ self: selfGdr(ctx.instance),
2195
+ now
2196
+ }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor, now);
1992
2197
  return await queueEffects(
1993
2198
  ctx,
1994
2199
  mutation,
@@ -1999,7 +2204,11 @@ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
1999
2204
  },
2000
2205
  actor,
2001
2206
  params
2002
- ), await persist(ctx, mutation), {
2207
+ ), await persistThenDeploy(
2208
+ ctx,
2209
+ mutation,
2210
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
2211
+ ), {
2003
2212
  fired: !0,
2004
2213
  taskId,
2005
2214
  action: actionName,
@@ -2038,7 +2247,7 @@ async function resolveAccess(client, args = {}) {
2038
2247
  const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2039
2248
  if (overrideActor !== void 0 && overrideGrants !== void 0)
2040
2249
  return { actor: overrideActor, grants: overrideGrants };
2041
- const requestFn = client.request;
2250
+ const requestFn = client.request === void 0 ? void 0 : (opts) => client.request(opts);
2042
2251
  if (requestFn === void 0) {
2043
2252
  if (overrideActor === void 0)
2044
2253
  throw new Error(
@@ -2113,7 +2322,7 @@ function canonicalTag(tags) {
2113
2322
  }
2114
2323
  const WILDCARD_ROLE = "*";
2115
2324
  async function evaluateInstance(args) {
2116
- const { client, tags, instanceId, resourceClients } = args;
2325
+ const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2117
2326
  validateTags(tags);
2118
2327
  const { actor, grants } = await resolveAccess(client, {
2119
2328
  ...args.access !== void 0 ? { override: args.access } : {},
@@ -2145,7 +2354,8 @@ async function evaluateInstance(args) {
2145
2354
  definition,
2146
2355
  actor,
2147
2356
  grants,
2148
- snapshot
2357
+ snapshot,
2358
+ now
2149
2359
  })
2150
2360
  );
2151
2361
  const transitionEvaluations = [];
@@ -2156,7 +2366,7 @@ async function evaluateInstance(args) {
2156
2366
  filter: transition.filter,
2157
2367
  definition,
2158
2368
  snapshot,
2159
- params: buildParams(instance)
2369
+ params: buildParams(instance, now)
2160
2370
  })
2161
2371
  });
2162
2372
  const currentStage = {
@@ -2174,7 +2384,7 @@ async function evaluateInstance(args) {
2174
2384
  };
2175
2385
  }
2176
2386
  async function evaluateTask(args) {
2177
- const { task, statusEntry, instance, definition, actor, grants, snapshot } = args, status = statusEntry?.status ?? "pending", actions = [];
2387
+ const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
2178
2388
  for (const action of task.actions ?? [])
2179
2389
  actions.push(
2180
2390
  await evaluateAction({
@@ -2186,7 +2396,8 @@ async function evaluateTask(args) {
2186
2396
  definition,
2187
2397
  actor,
2188
2398
  ...grants !== void 0 ? { grants } : {},
2189
- snapshot
2399
+ snapshot,
2400
+ now
2190
2401
  })
2191
2402
  );
2192
2403
  return {
@@ -2200,9 +2411,9 @@ async function evaluateTask(args) {
2200
2411
  };
2201
2412
  }
2202
2413
  async function evaluateAction(args) {
2203
- const { action, task, status, instance, definition, actor, grants, snapshot } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2414
+ const { action, task, status, instance, definition, actor, grants, snapshot, now } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2204
2415
  if (syncReason !== void 0) return disabled(action, syncReason);
2205
- const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot);
2416
+ const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot, now);
2206
2417
  return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
2207
2418
  }
2208
2419
  function lifecycleReason(instance, definition, status) {
@@ -2231,12 +2442,12 @@ async function permissionReason(action, instance, actor, grants) {
2231
2442
  }))
2232
2443
  return { kind: "permission-denied", permission, matchingGrants: grants.length };
2233
2444
  }
2234
- async function filterReason(action, instance, definition, snapshot) {
2445
+ async function filterReason(action, instance, definition, snapshot, now) {
2235
2446
  if (!(action.filter === void 0 || await evaluateFilter({
2236
2447
  filter: action.filter,
2237
2448
  definition,
2238
2449
  snapshot,
2239
- params: buildParams(instance)
2450
+ params: buildParams(instance, now)
2240
2451
  })))
2241
2452
  return { kind: "filter-failed", filter: action.filter };
2242
2453
  }
@@ -2261,6 +2472,43 @@ function parseDefinitionSnapshot(instance) {
2261
2472
  );
2262
2473
  }
2263
2474
  }
2475
+ const resourceKey = (r) => `${r.type}.${r.id}`;
2476
+ function guardsForResource(client) {
2477
+ return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2478
+ }
2479
+ async function guardsForInstance(args) {
2480
+ const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
2481
+ [resourceKey(instance.workflowResource), client]
2482
+ ]), stateUris = [
2483
+ ...collectSlotDocUris(instance.state),
2484
+ ...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
2485
+ ];
2486
+ for (const uri of stateUris) {
2487
+ const parsed = tryParseGdr(uri);
2488
+ if (parsed === void 0) continue;
2489
+ const key = resourceKey(resourceFromParsed(parsed));
2490
+ clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
2491
+ }
2492
+ const perResource = await Promise.all(
2493
+ [...clientsByResource.values()].map(
2494
+ (c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
2495
+ t: GUARD_DOC_TYPE,
2496
+ id: instance._id
2497
+ })
2498
+ )
2499
+ );
2500
+ return dedupById(perResource.flat());
2501
+ }
2502
+ function tryParseGdr(uri) {
2503
+ try {
2504
+ return parseGdr(uri);
2505
+ } catch {
2506
+ return;
2507
+ }
2508
+ }
2509
+ function dedupById(guards) {
2510
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2511
+ }
2264
2512
  class ActionDisabledError extends Error {
2265
2513
  reason;
2266
2514
  taskId;
@@ -2371,14 +2619,14 @@ function isDefinitionUnchanged(existing, expected) {
2371
2619
  }
2372
2620
  function stripSystemFields(doc) {
2373
2621
  const out = {};
2374
- for (const [k, v] of Object.entries(doc))
2375
- k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v);
2622
+ for (const [k, v2] of Object.entries(doc))
2623
+ k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v2);
2376
2624
  return out;
2377
2625
  }
2378
2626
  function stableStringify(value) {
2379
2627
  return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2380
2628
  ([a], [b]) => a.localeCompare(b)
2381
- ).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
2629
+ ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2382
2630
  }
2383
2631
  async function loadDefinition(client, workflowId, version, tags) {
2384
2632
  if (version !== void 0) {
@@ -2418,9 +2666,9 @@ async function resolveOperationContext(args) {
2418
2666
  clientForGdr: buildClientForGdr(args.client, args.resourceClients)
2419
2667
  };
2420
2668
  }
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;
2669
+ async function cascade(client, instanceId, actor, clientForGdr, clock) {
2670
+ const count = await cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock);
2671
+ return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
2424
2672
  }
2425
2673
  function buildClientForGdr(defaultClient, resolver) {
2426
2674
  return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
@@ -2512,16 +2760,17 @@ const workflow = {
2512
2760
  effectsContext,
2513
2761
  instanceId,
2514
2762
  perspective
2515
- } = args, { actor, clientForGdr } = await resolveOperationContext(args), definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2763
+ } = 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
2764
  if (definition.stages.find((s) => s.id === definition.initialStageId) === void 0)
2517
2765
  throw new Error(
2518
2766
  `Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
2519
2767
  );
2520
- const now = (/* @__PURE__ */ new Date()).toISOString(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2768
+ const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2521
2769
  slotDefs: definition.state ?? [],
2522
2770
  initialState: initialState ?? [],
2523
2771
  ctx: {
2524
2772
  client,
2773
+ now,
2525
2774
  selfId: id,
2526
2775
  tags,
2527
2776
  workflowResource,
@@ -2543,7 +2792,7 @@ const workflow = {
2543
2792
  initialStageId: definition.initialStageId,
2544
2793
  actor
2545
2794
  });
2546
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr), await cascade(client, id, actor, clientForGdr), reload(client, id, tags);
2795
+ return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
2547
2796
  },
2548
2797
  /**
2549
2798
  * Fire an action against a task. If the task is pending, it is
@@ -2565,7 +2814,7 @@ const workflow = {
2565
2814
  params,
2566
2815
  idempotent,
2567
2816
  resourceClients
2568
- } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), before = await reload(client, instanceId, tags), taskEntry = before.stages.find(
2817
+ } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags), taskEntry = before.stages.find(
2569
2818
  (s) => s.id === before.currentStageId && s.exitedAt === void 0
2570
2819
  )?.tasks.find((t) => t.id === taskId);
2571
2820
  if (taskEntry === void 0 && idempotent === !0)
@@ -2575,6 +2824,7 @@ const workflow = {
2575
2824
  tags,
2576
2825
  instanceId,
2577
2826
  access,
2827
+ clock,
2578
2828
  ...resourceClients !== void 0 ? { resourceClients } : {}
2579
2829
  })).currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2580
2830
  if (actionEval !== void 0 && !actionEval.allowed && actionEval.disabledReason)
@@ -2587,7 +2837,7 @@ const workflow = {
2587
2837
  client,
2588
2838
  instanceId,
2589
2839
  taskId,
2590
- options: { actor }
2840
+ options: { actor, clock }
2591
2841
  });
2592
2842
  const actionResult = await fireAction({
2593
2843
  client,
@@ -2595,8 +2845,8 @@ const workflow = {
2595
2845
  taskId,
2596
2846
  action,
2597
2847
  ...params !== void 0 ? { params } : {},
2598
- options: { actor }
2599
- }), cascaded = await cascade(client, instanceId, actor, clientForGdr);
2848
+ options: { actor, clock }
2849
+ }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2600
2850
  return {
2601
2851
  instance: await reload(client, instanceId, tags),
2602
2852
  cascaded,
@@ -2612,7 +2862,7 @@ const workflow = {
2612
2862
  * reference them. Cascades after.
2613
2863
  */
2614
2864
  completeEffect: async (args) => {
2615
- const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2865
+ const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2616
2866
  await completeEffect({
2617
2867
  client,
2618
2868
  instanceId,
@@ -2622,9 +2872,9 @@ const workflow = {
2622
2872
  ...detail !== void 0 ? { detail } : {},
2623
2873
  ...error !== void 0 ? { error } : {},
2624
2874
  ...durationMs !== void 0 ? { durationMs } : {},
2625
- ...actor !== void 0 ? { options: { actor } } : {}
2875
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
2626
2876
  });
2627
- const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2877
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2628
2878
  return {
2629
2879
  instance: await reload(client, instanceId, tags),
2630
2880
  cascaded,
@@ -2641,9 +2891,9 @@ const workflow = {
2641
2891
  * affected instances and the engine re-evaluates.
2642
2892
  */
2643
2893
  tick: async (args) => {
2644
- const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2894
+ const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2645
2895
  await reload(client, instanceId, tags);
2646
- const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2896
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2647
2897
  return {
2648
2898
  instance: await reload(client, instanceId, tags),
2649
2899
  cascaded,
@@ -2656,15 +2906,15 @@ const workflow = {
2656
2906
  * upstream; this verb performs the mechanical move.
2657
2907
  */
2658
2908
  setStage: async (args) => {
2659
- const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2909
+ const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2660
2910
  await reload(client, instanceId, tags);
2661
2911
  const result = await setStage({
2662
2912
  client,
2663
2913
  instanceId,
2664
2914
  targetStageId,
2665
2915
  ...reason !== void 0 ? { reason } : {},
2666
- ...actor !== void 0 ? { options: { actor } } : {}
2667
- }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr) : 0;
2916
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
2917
+ }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
2668
2918
  return {
2669
2919
  instance: await reload(client, instanceId, tags),
2670
2920
  cascaded,
@@ -2680,6 +2930,16 @@ const workflow = {
2680
2930
  const { client, tags, instanceId } = args;
2681
2931
  return validateTags(tags), reload(client, instanceId, tags);
2682
2932
  },
2933
+ guardsForInstance: async (args) => {
2934
+ const { client, tags, instanceId, resourceClients } = args;
2935
+ validateTags(tags);
2936
+ const instance = await reload(client, instanceId, tags);
2937
+ return guardsForInstance({
2938
+ client,
2939
+ clientForGdr: buildClientForGdr(client, resourceClients),
2940
+ instance
2941
+ });
2942
+ },
2683
2943
  /**
2684
2944
  * Run a caller-supplied GROQ query with the engine's tags bound as
2685
2945
  * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
@@ -2714,9 +2974,9 @@ const workflow = {
2714
2974
  * without re-implementing hydration. Pure read — never writes.
2715
2975
  */
2716
2976
  queryInScope: async (args) => {
2717
- const { client, tags, instanceId, groq, params, resourceClients } = args;
2977
+ const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
2718
2978
  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 } });
2979
+ const instance = await reload(client, instanceId, tags), definition = JSON.parse(instance.definitionSnapshot), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition }), reserved = buildParams(instance, clock()), tree = parse(groq, { params: { ...reserved, ...params } });
2720
2980
  return await (await evaluate(tree, {
2721
2981
  dataset: snapshot.docs,
2722
2982
  params: { ...reserved, ...params }
@@ -2900,7 +3160,8 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
2900
3160
  };
2901
3161
  try {
2902
3162
  return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
2903
- } catch {
3163
+ } catch (error) {
3164
+ if (!isRevisionConflict(error)) throw error;
2904
3165
  return !1;
2905
3166
  }
2906
3167
  }
@@ -2945,13 +3206,14 @@ const defaultLoggerFactory = (name) => ({
2945
3206
  }
2946
3207
  };
2947
3208
  function createEngine(args) {
2948
- const { client, workflowResource, tags, resourceClients } = args;
3209
+ const { client, workflowResource, tags, resourceClients, clock } = args;
2949
3210
  validateTags(tags);
2950
3211
  const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
2951
3212
  client,
2952
3213
  tags,
2953
3214
  workflowResource,
2954
3215
  ...resourceClients !== void 0 ? { resourceClients } : {},
3216
+ ...clock !== void 0 ? { clock } : {},
2955
3217
  ...rest
2956
3218
  });
2957
3219
  return {
@@ -2969,6 +3231,13 @@ function createEngine(args) {
2969
3231
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
2970
3232
  setStage: (rest) => workflow.setStage(bind(rest)),
2971
3233
  getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
3234
+ guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
3235
+ client,
3236
+ tags,
3237
+ workflowResource,
3238
+ instanceId,
3239
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3240
+ }),
2972
3241
  children: ({ instanceId, taskId }) => workflow.children({
2973
3242
  client,
2974
3243
  tags,
@@ -2989,7 +3258,8 @@ function createEngine(args) {
2989
3258
  workflowResource,
2990
3259
  instanceId,
2991
3260
  groq,
2992
- ...params !== void 0 ? { params } : {}
3261
+ ...params !== void 0 ? { params } : {},
3262
+ ...clock !== void 0 ? { clock } : {}
2993
3263
  }),
2994
3264
  listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tags, workflowResource, instanceId }),
2995
3265
  findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
@@ -3020,15 +3290,6 @@ function createEngine(args) {
3020
3290
  })
3021
3291
  };
3022
3292
  }
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
3293
  const HISTORY_DISPLAY = {
3033
3294
  "workflow.history.stageEntered": {
3034
3295
  title: "Stage entered",
@@ -3224,14 +3485,18 @@ export {
3224
3485
  AUTHORING_DISPLAY,
3225
3486
  ActionDisabledError,
3226
3487
  ActionParamsInvalidError,
3488
+ CascadeLimitError,
3489
+ ConcurrentFireActionError,
3227
3490
  DISPLAY,
3228
3491
  EFFECTS_CONTEXT_DISPLAY,
3492
+ GUARD_DOC_TYPE,
3229
3493
  GUARD_LIFTED_PREDICATE,
3230
3494
  GUARD_OWNER,
3231
3495
  HISTORY_DISPLAY,
3232
3496
  MutationGuardDeniedError,
3233
3497
  OP_DISPLAY,
3234
3498
  STATE_SLOT_DISPLAY,
3499
+ WorkflowStateDivergedError,
3235
3500
  canonicalTag,
3236
3501
  compileGuard,
3237
3502
  createEngine,
@@ -3246,10 +3511,12 @@ export {
3246
3511
  gdrRef,
3247
3512
  gdrUri,
3248
3513
  guardMatches,
3514
+ guardsForInstance,
3515
+ guardsForResource,
3516
+ isDefinitionUnchanged,
3249
3517
  isGdr,
3250
3518
  lakeGuardId,
3251
3519
  parseGdr,
3252
- reconcileStageGuards,
3253
3520
  refCanvas,
3254
3521
  refDashboard,
3255
3522
  refDataset,
@@ -3257,8 +3524,10 @@ export {
3257
3524
  resolveAccess,
3258
3525
  retractStageGuards,
3259
3526
  silentLogger,
3527
+ stripSystemFields,
3260
3528
  validateDefinition,
3261
3529
  validateTags,
3530
+ wallClock,
3262
3531
  workflow
3263
3532
  };
3264
3533
  //# sourceMappingURL=index.js.map