@sanity/workflow-engine 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,18 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: !0 });
3
- var groqJs = require("groq-js"), zod = require("zod");
3
+ var groqJs = require("groq-js"), schema = require("./_chunks-cjs/schema.cjs"), v = require("valibot");
4
+ function _interopNamespaceCompat(e) {
5
+ if (e && typeof e == "object" && "default" in e) return e;
6
+ var n = /* @__PURE__ */ Object.create(null);
7
+ return e && Object.keys(e).forEach(function(k) {
8
+ if (k !== "default") {
9
+ var d = Object.getOwnPropertyDescriptor(e, k);
10
+ Object.defineProperty(n, k, d.get ? d : {
11
+ enumerable: !0,
12
+ get: function() {
13
+ return e[k];
14
+ }
15
+ });
16
+ }
17
+ }), n.default = e, Object.freeze(n);
18
+ }
19
+ var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
4
20
  function validateDefinition(definition) {
5
- const v = createDefinitionValidator();
21
+ const v2 = createDefinitionValidator();
6
22
  for (const slot of definition.state ?? [])
7
- v.checkSlot(slot, "workflow.state", `workflow.state "${slot.id}"`);
23
+ v2.checkSlot(slot, `workflow.state "${slot.id}"`);
8
24
  for (const p of definition.predicates ?? [])
9
- p.groq && v.checkFilter(p.groq, `predicate "${p.id}"`);
25
+ p.groq && v2.checkFilter(p.groq, `predicate "${p.id}"`);
10
26
  for (const stage of definition.stages)
11
- validateStage(v, stage);
12
- if (v.errors.length > 0)
27
+ validateStage(v2, stage);
28
+ if (v2.errors.length > 0)
13
29
  throw new Error(
14
- `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v.errors.length} validation error${v.errors.length === 1 ? "" : "s"}:
15
- ` + v.errors.join(`
30
+ `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
31
+ ` + v2.errors.join(`
16
32
  `)
17
33
  );
18
34
  }
@@ -31,31 +47,29 @@ function createDefinitionValidator() {
31
47
  };
32
48
  return { errors, tryParse, checkFilter: (groq, where) => {
33
49
  tryParse(groq, where), rejectTypeScan(groq, where);
34
- }, checkSlot: (slot, scopeLabel, slotLabel) => {
35
- slot.source.kind === "computed" && errors.push(
36
- ` \xB7 ${scopeLabel} slot "${slot.id}": source.kind="computed" is reserved (not implemented in v0.1)`
37
- ), slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
50
+ }, checkSlot: (slot, slotLabel) => {
51
+ slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
38
52
  } };
39
53
  }
40
- function validateStage(v, stage) {
54
+ function validateStage(v2, stage) {
41
55
  for (const slot of stage.state ?? [])
42
- v.checkSlot(slot, `stage "${stage.id}".state`, `stage "${stage.id}".state "${slot.id}"`);
43
- typeof stage.completion == "string" && v.tryParse(stage.completion, `stage "${stage.id}".completion`);
56
+ v2.checkSlot(slot, `stage "${stage.id}".state "${slot.id}"`);
57
+ typeof stage.completion == "string" && v2.tryParse(stage.completion, `stage "${stage.id}".completion`);
44
58
  for (const t of stage.transitions ?? [])
45
- typeof t.filter == "string" && v.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
59
+ typeof t.filter == "string" && v2.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
46
60
  for (const task of stage.tasks ?? [])
47
- validateTask(v, stage.id, task);
61
+ validateTask(v2, stage.id, task);
48
62
  }
49
- function validateTask(v, stageId, task) {
63
+ function validateTask(v2, stageId, task) {
50
64
  const where = `stage "${stageId}" task "${task.id}"`;
51
65
  for (const slot of task.state ?? [])
52
- v.checkSlot(slot, `${where}.state`, `${where}.state "${slot.id}"`);
53
- typeof task.filter == "string" && v.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v.checkFilter(task.completeWhen, `${where}.completeWhen`);
66
+ v2.checkSlot(slot, `${where}.state "${slot.id}"`);
67
+ typeof task.filter == "string" && v2.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v2.checkFilter(task.completeWhen, `${where}.completeWhen`);
54
68
  for (const a of task.actions ?? [])
55
- typeof a.filter == "string" && v.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
56
- task.spawns?.forEach?.groq && v.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
69
+ typeof a.filter == "string" && v2.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
70
+ task.spawns?.forEach?.groq && v2.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
57
71
  }
58
- const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
72
+ const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
59
73
  function parseGdr(uri) {
60
74
  const colon = uri.indexOf(":");
61
75
  if (colon < 0)
@@ -122,6 +136,9 @@ function gdrFromResource(res, documentId) {
122
136
  }
123
137
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
124
138
  }
139
+ function selfGdr(doc) {
140
+ return gdrFromResource(doc.workflowResource, doc._id);
141
+ }
125
142
  function isGdrUri(value) {
126
143
  if (typeof value != "string") return !1;
127
144
  try {
@@ -136,15 +153,15 @@ function gdrRef(res, documentId, type) {
136
153
  function isGdr(value) {
137
154
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
138
155
  }
139
- function buildParams(instance, extra) {
156
+ function buildParams(instance, now, extra) {
140
157
  return {
141
- self: gdrFromResource(instance.workflowResource, instance._id),
158
+ self: selfGdr(instance),
142
159
  state: stateMap(instance.state ?? []),
143
160
  parent: instance.ancestors.at(-1)?.id ?? null,
144
161
  ancestors: instance.ancestors.map((a) => a.id),
145
162
  /** Current stage id — bind for filters that scope to the current stage. */
146
163
  stage: instance.currentStageId,
147
- now: (/* @__PURE__ */ new Date()).toISOString(),
164
+ now,
148
165
  ...extra
149
166
  };
150
167
  }
@@ -168,8 +185,8 @@ function stripStateForLake(value) {
168
185
  if (Array.isArray(value)) return value.map(stripStateForLake);
169
186
  if (typeof value == "object") {
170
187
  const out = {};
171
- for (const [k, v] of Object.entries(value))
172
- out[k] = stripStateForLake(v);
188
+ for (const [k, v2] of Object.entries(value))
189
+ out[k] = stripStateForLake(v2);
173
190
  return out;
174
191
  }
175
192
  return value;
@@ -177,9 +194,94 @@ function stripStateForLake(value) {
177
194
  function bareId(id) {
178
195
  return isGdrUri(id) ? extractDocumentId(id) : id;
179
196
  }
180
- function randomKey(length = 12) {
181
- const bytes = new Uint8Array(length);
182
- return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
197
+ function getPath(value, path) {
198
+ let current = value;
199
+ for (const part of path.split(".")) {
200
+ if (current == null || typeof current != "object") return;
201
+ current = current[part];
202
+ }
203
+ return current;
204
+ }
205
+ function readStateSlot(instance, scope, slotId, stageId, taskId) {
206
+ if (scope === "workflow")
207
+ return slotValue$1(instance.state?.find((s) => s.id === slotId));
208
+ const stageEntry = instance.stages.find(
209
+ (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
210
+ );
211
+ if (stageEntry === void 0) return;
212
+ if (scope === "stage")
213
+ return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
214
+ const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
215
+ return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
216
+ }
217
+ function slotValue$1(slot) {
218
+ if (slot != null && typeof slot == "object")
219
+ return slot.value;
220
+ }
221
+ function resolveStaticSource(src, ctx) {
222
+ switch (src.source) {
223
+ case "literal":
224
+ return { handled: !0, value: src.value };
225
+ case "param":
226
+ return { handled: !0, value: ctx.params?.[src.paramId] };
227
+ case "actor":
228
+ return { handled: !0, value: ctx.actor };
229
+ case "now":
230
+ return { handled: !0, value: ctx.now };
231
+ default:
232
+ return { handled: !1 };
233
+ }
234
+ }
235
+ function resolveSource(src, ctx) {
236
+ const staticValue = resolveStaticSource(src, ctx);
237
+ if (staticValue.handled) return staticValue.value;
238
+ switch (src.source) {
239
+ case "self":
240
+ return ctx.instance._id;
241
+ case "stageId":
242
+ return ctx.stageId ?? ctx.instance.currentStageId;
243
+ case "stateRead":
244
+ return resolveStateRead(src, ctx);
245
+ case "effectOutput":
246
+ return resolveEffectOutput(src, ctx);
247
+ case "object":
248
+ return resolveObject(src, ctx);
249
+ case "row":
250
+ case "parentState":
251
+ return;
252
+ default:
253
+ return;
254
+ }
255
+ }
256
+ function resolveStateRead(src, ctx) {
257
+ const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
258
+ return src.path !== void 0 ? getPath(value, src.path) : value;
259
+ }
260
+ function resolveEffectOutput(src, ctx) {
261
+ const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
262
+ if (entry === void 0) return null;
263
+ const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
264
+ return value === void 0 ? null : value;
265
+ }
266
+ function resolveObject(src, ctx) {
267
+ const out = {};
268
+ for (const [field, fieldSrc] of Object.entries(src.fields))
269
+ out[field] = resolveSource(fieldSrc, ctx);
270
+ return out;
271
+ }
272
+ async function resolveBindings(args) {
273
+ const { bindings, staticInput, instance, now, params, actor } = args, resolved = {};
274
+ if (bindings) {
275
+ const ctx = {
276
+ instance,
277
+ now,
278
+ ...params !== void 0 ? { params } : {},
279
+ ...actor !== void 0 ? { actor } : {}
280
+ };
281
+ for (const [key, src] of Object.entries(bindings))
282
+ resolved[key] = resolveSource(src, ctx);
283
+ }
284
+ return { ...resolved, ...staticInput };
183
285
  }
184
286
  async function evaluateFilter(args) {
185
287
  const { filter, definition, snapshot, params } = args;
@@ -227,7 +329,7 @@ function matchesParamType(type, value) {
227
329
  case "boolean":
228
330
  return typeof value == "boolean";
229
331
  case "string[]":
230
- return Array.isArray(value) && value.every((v) => typeof v == "string");
332
+ return Array.isArray(value) && value.every((v2) => typeof v2 == "string");
231
333
  case "reference":
232
334
  return isGdr(value);
233
335
  }
@@ -235,186 +337,403 @@ function matchesParamType(type, value) {
235
337
  function describeValue(value) {
236
338
  return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
237
339
  }
238
- function buildSnapshot(args) {
239
- const docs = [], knownIds = /* @__PURE__ */ new Set();
240
- for (const { doc, resource } of args.docs) {
241
- const uri = gdrFromResource(resource, doc._id), restamped = restampForSnapshot(doc, uri, resource);
242
- docs.push(restamped), knownIds.add(uri);
340
+ const GUARD_DOC_TYPE = "temp.system.guard";
341
+ class MutationGuardDeniedError extends Error {
342
+ denied;
343
+ documentId;
344
+ action;
345
+ constructor(args) {
346
+ const ids = args.denied.map((d) => d.guardId).join(", ");
347
+ super(`Mutation on "${args.documentId}" (${args.action}) denied by guard(s) [${ids}]`), this.name = "MutationGuardDeniedError", this.denied = args.denied, this.documentId = args.documentId, this.action = args.action;
243
348
  }
244
- return { docs, knownIds };
245
- }
246
- function restampForSnapshot(doc, gdrUri2, resource) {
247
- return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
248
349
  }
249
- function rewriteRefsRecursive(value, resource) {
250
- if (Array.isArray(value))
251
- return value.map((v) => rewriteRefsRecursive(v, resource));
252
- if (value === null || typeof value != "object") return value;
253
- const obj = value, out = {};
254
- for (const [k, v] of Object.entries(obj))
255
- k === "_ref" && typeof v == "string" ? out[k] = v.includes(":") ? v : gdrFromResource(resource, v) : out[k] = rewriteRefsRecursive(v, resource);
256
- return out;
350
+ function lakeGuardId(args) {
351
+ return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
257
352
  }
258
- async function hydrateSnapshot(args) {
259
- const { client, clientForGdr, instance, definition } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri) => {
260
- if (visited.has(uri)) return;
261
- const fetched = await loadByGdr(client, clientForGdr, instance.workflowResource, uri);
262
- fetched && (loaded.push(fetched), visited.add(uri));
353
+ function compileGuard(args) {
354
+ return {
355
+ _id: args.id,
356
+ _type: GUARD_DOC_TYPE,
357
+ resourceType: args.resourceType,
358
+ resourceId: args.resourceId,
359
+ owner: args.owner,
360
+ sourceInstanceId: args.sourceInstanceId,
361
+ sourceDefinitionId: args.sourceDefinitionId,
362
+ sourceStageId: args.sourceStageId,
363
+ ...args.name !== void 0 ? { name: args.name } : {},
364
+ ...args.description !== void 0 ? { description: args.description } : {},
365
+ match: args.match,
366
+ predicate: args.predicate,
367
+ metadata: args.metadata
263
368
  };
264
- loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(gdrFromResource(instance.workflowResource, instance._id));
265
- for (const ancestor of instance.ancestors)
266
- await loadInto(ancestor.id);
267
- for (const uri of collectSlotDocUris(instance.state))
268
- await loadInto(uri);
269
- if (definition) {
270
- const stageEntry = instance.stages.find(
271
- (s) => s.id === instance.currentStageId && s.exitedAt === void 0
272
- );
273
- for (const uri of collectSlotDocUris(stageEntry?.state))
274
- await loadInto(uri);
369
+ }
370
+ function toGroqJsPredicate(predicate) {
371
+ return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
372
+ }
373
+ function globMatch(pattern, value) {
374
+ if (!pattern.includes("*")) return pattern === value;
375
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
376
+ return new RegExp(`^${escaped}$`).test(value);
377
+ }
378
+ function guardMatches(guard, doc, action) {
379
+ const m = guard.match;
380
+ if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
381
+ return !1;
382
+ if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
383
+ const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
384
+ if (!byRef && !byPattern) return !1;
275
385
  }
276
- return buildSnapshot({ docs: loaded });
386
+ return !0;
277
387
  }
278
- async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri) {
279
- let parsed;
388
+ async function evaluateMutationGuard(args) {
389
+ const { guard, context } = args;
390
+ if (guard.predicate === "") return !1;
391
+ const params = { guard, mutation: { action: context.action } };
280
392
  try {
281
- parsed = parseGdr(uri);
393
+ const tree = groqJs.parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
394
+ return await (await groqJs.evaluate(tree, {
395
+ before: context.before,
396
+ after: context.after,
397
+ params,
398
+ ...context.identity !== void 0 ? { identity: context.identity } : {}
399
+ })).get() === !0;
282
400
  } catch {
283
- const doc2 = await defaultClient.getDocument(uri);
284
- return doc2 ? { doc: doc2, resource: defaultResource } : null;
401
+ return !1;
285
402
  }
286
- const doc = await clientForGdr(parsed).getDocument(parsed.documentId);
287
- return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
288
403
  }
289
- function resourceFromParsed(parsed) {
290
- return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
404
+ async function denyingGuards(args) {
405
+ const { guards, doc, context } = args, denied = [];
406
+ for (const guard of guards)
407
+ guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
408
+ return denied;
291
409
  }
292
- function collectSlotDocUris(resolvedStateSlots) {
293
- return Array.isArray(resolvedStateSlots) ? resolvedStateSlots.flatMap(slotDocUris) : [];
410
+ function resourceOf(p) {
411
+ return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
294
412
  }
295
- function slotDocUris(slot) {
296
- if (!slot || typeof slot != "object") return [];
297
- const s = slot;
298
- if (s._type === "workflow.state.doc.ref") {
299
- const v = s.value, id = refId(v);
300
- return id !== void 0 ? [id] : [];
301
- }
302
- if (s._type === "workflow.state.doc.refs") {
303
- const v = s.value;
304
- return Array.isArray(v) ? v.map(refId).filter((id) => id !== void 0) : [];
413
+ function resolveIdRefTarget(src, ctx) {
414
+ const value = resolveSource(src, ctx);
415
+ if (typeof value == "string" && isGdrUri(value))
416
+ return { parsed: parseGdr(value) };
417
+ if (value && typeof value == "object") {
418
+ const v2 = value;
419
+ if (typeof v2.id == "string" && isGdrUri(v2.id))
420
+ return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
305
421
  }
306
- return [];
307
- }
308
- function refId(ref) {
309
- return ref && typeof ref.id == "string" ? ref.id : void 0;
422
+ return null;
310
423
  }
311
- function getPath(value, path) {
312
- let current = value;
313
- for (const part of path.split(".")) {
314
- if (current == null || typeof current != "object") return;
315
- current = current[part];
424
+ function resolveIdRefTargets(idRefs, ctx) {
425
+ const targets = [];
426
+ for (const src of idRefs ?? []) {
427
+ const target = resolveIdRefTarget(src, ctx);
428
+ if (target === null) return null;
429
+ targets.push(target);
316
430
  }
317
- return current;
431
+ return targets.length === 0 ? null : targets;
318
432
  }
319
- class SlotValueShapeError extends Error {
320
- slotType;
321
- slotId;
322
- issues;
323
- constructor(args) {
324
- const issueText = args.issues.join("; ");
325
- super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
433
+ function assertSingleResource(targets) {
434
+ const resource = resourceOf(targets[0].parsed);
435
+ for (const g of targets) {
436
+ const r = resourceOf(g.parsed);
437
+ if (r.type !== resource.type || r.id !== resource.id)
438
+ throw new Error(
439
+ `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
440
+ );
326
441
  }
442
+ return resource;
327
443
  }
328
- const GdrShape = zod.z.object({
329
- id: zod.z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
330
- type: zod.z.string().min(1)
331
- }).passthrough(), ReleaseRefShape = zod.z.object({
332
- id: zod.z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
333
- type: zod.z.literal("system.release"),
334
- releaseName: zod.z.string().min(1)
335
- }).passthrough(), ActorShape = zod.z.object({
336
- kind: zod.z.enum(["user", "ai", "system"]),
337
- id: zod.z.string().min(1),
338
- roles: zod.z.array(zod.z.string()).optional(),
339
- onBehalfOf: zod.z.string().optional()
340
- }).passthrough(), AssigneeShape = zod.z.union([
341
- zod.z.object({ kind: zod.z.literal("user"), id: zod.z.string().min(1) }).passthrough(),
342
- zod.z.object({ kind: zod.z.literal("role"), role: zod.z.string().min(1) }).passthrough()
343
- ]), ChecklistItemShape = zod.z.object({
344
- label: zod.z.string().min(1),
345
- done: zod.z.boolean(),
346
- doneBy: zod.z.string().optional(),
347
- doneAt: zod.z.string().optional(),
348
- _key: zod.z.string().min(1).optional()
349
- }).passthrough(), NoteItemShape = zod.z.object({
350
- _key: zod.z.string().min(1).optional()
351
- }).passthrough().refine((v) => typeof v == "object" && v !== null && !Array.isArray(v), {
352
- message: "must be an object"
353
- }), NullableString = zod.z.union([zod.z.null(), zod.z.string()]), NullableNumber = zod.z.union([zod.z.null(), zod.z.number()]), NullableBoolean = zod.z.union([zod.z.null(), zod.z.boolean()]), NullableDateTime = zod.z.union([
354
- zod.z.null(),
355
- zod.z.string().refine((s) => !Number.isNaN(Date.parse(s)), {
356
- message: "must be an ISO-8601 datetime string"
357
- })
358
- ]), NullableUrl = NullableString, valueSchemas = {
359
- "workflow.state.doc.ref": zod.z.union([zod.z.null(), GdrShape]),
360
- "workflow.state.doc.refs": zod.z.array(GdrShape),
361
- "workflow.state.release.ref": zod.z.union([zod.z.null(), ReleaseRefShape]),
362
- "workflow.state.query": zod.z.any(),
363
- "workflow.state.value.string": NullableString,
364
- "workflow.state.value.url": NullableUrl,
365
- "workflow.state.value.number": NullableNumber,
366
- "workflow.state.value.boolean": NullableBoolean,
367
- "workflow.state.value.dateTime": NullableDateTime,
368
- "workflow.state.value.actor": zod.z.union([zod.z.null(), ActorShape]),
369
- "workflow.state.checklist": zod.z.array(ChecklistItemShape),
370
- "workflow.state.notes": zod.z.array(NoteItemShape),
371
- "workflow.state.assignees": zod.z.array(AssigneeShape)
372
- }, itemSchemas = {
373
- "workflow.state.doc.refs": GdrShape,
374
- "workflow.state.checklist": ChecklistItemShape,
375
- "workflow.state.notes": NoteItemShape,
376
- "workflow.state.assignees": AssigneeShape
377
- };
378
- function isAppendable(slotType) {
379
- return slotType in itemSchemas;
444
+ function bareIdRefs(targets) {
445
+ return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
380
446
  }
381
- function validateSlotValue(args) {
382
- const schema = valueSchemas[args.slotType];
383
- if (schema === void 0)
384
- throw new SlotValueShapeError({
385
- slotType: args.slotType,
386
- slotId: args.slotId,
387
- issues: [`unknown slot type ${args.slotType}`],
388
- mode: "value"
389
- });
390
- const result = schema.safeParse(args.value);
391
- if (!result.success)
392
- throw new SlotValueShapeError({
393
- slotType: args.slotType,
394
- slotId: args.slotId,
395
- issues: formatIssues(result.error.issues),
396
- mode: "value"
397
- });
447
+ function resolveMatchTypes(targets, authorTypes) {
448
+ if (authorTypes !== void 0) return authorTypes;
449
+ const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
450
+ return inferred.length > 0 ? inferred : void 0;
398
451
  }
399
- function validateSlotAppendItem(args) {
400
- if (!isAppendable(args.slotType))
401
- throw new SlotValueShapeError({
402
- slotType: args.slotType,
403
- slotId: args.slotId,
404
- issues: [`slot type ${args.slotType} does not support append`],
405
- mode: "item"
406
- });
407
- const result = itemSchemas[args.slotType].safeParse(args.item);
408
- if (!result.success)
409
- throw new SlotValueShapeError({
410
- slotType: args.slotType,
411
- slotId: args.slotId,
412
- issues: formatIssues(result.error.issues),
413
- mode: "item"
414
- });
452
+ function resolveMetadata(metadata, ctx) {
453
+ const out = {};
454
+ for (const [k, src] of Object.entries(metadata ?? {}))
455
+ out[k] = resolveSource(src, ctx);
456
+ return out;
457
+ }
458
+ const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
459
+ function resolveGuard(guard, index, instance, stageId, now) {
460
+ const ctx = { instance, stageId, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
461
+ if (targets === null) return null;
462
+ const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
463
+ return { doc: compileGuard({
464
+ id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
465
+ resourceType: resource.type,
466
+ resourceId: resource.id,
467
+ owner: GUARD_OWNER,
468
+ sourceInstanceId: instance._id,
469
+ sourceDefinitionId: instance.workflowId,
470
+ sourceStageId: stageId,
471
+ ...guard.name !== void 0 ? { name: guard.name } : {},
472
+ ...guard.description !== void 0 ? { description: guard.description } : {},
473
+ match: {
474
+ ...types !== void 0 ? { types } : {},
475
+ idRefs: bareIdRefs(targets),
476
+ ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
477
+ actions: guard.match.actions
478
+ },
479
+ predicate: guard.predicate ?? "",
480
+ metadata: resolveMetadata(guard.metadata, ctx)
481
+ }), routeGdr: targets[0].parsed };
482
+ }
483
+ async function upsertGuard(client, doc) {
484
+ if (!await client.getDocument(doc._id)) {
485
+ await client.create(doc);
486
+ return;
487
+ }
488
+ const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
489
+ await client.patch(doc._id).set(body).commit();
490
+ }
491
+ function resolvedStageGuards(args) {
492
+ const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
493
+ for (const [index, guard] of (stage?.guards ?? []).entries()) {
494
+ const resolved = resolveGuard(guard, index, args.instance, args.stageId, args.now);
495
+ resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
496
+ }
497
+ return out;
498
+ }
499
+ async function committedStageId(args) {
500
+ return (await args.client.getDocument(args.instance._id))?.currentStageId;
501
+ }
502
+ async function deployStageGuards(args) {
503
+ if (await committedStageId(args) === args.stageId)
504
+ for (const { client, doc } of resolvedStageGuards(args))
505
+ await upsertGuard(client, doc);
506
+ }
507
+ async function retractStageGuards(args) {
508
+ const live = await committedStageId(args);
509
+ if (!(live === void 0 || live === args.stageId))
510
+ for (const { client, doc } of resolvedStageGuards(args))
511
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
512
+ }
513
+ function randomKey(length = 12) {
514
+ const bytes = new Uint8Array(length);
515
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
516
+ }
517
+ function buildSnapshot(args) {
518
+ const docs = [], knownIds = /* @__PURE__ */ new Set();
519
+ for (const { doc, resource } of args.docs) {
520
+ const uri = gdrFromResource(resource, doc._id), restamped = restampForSnapshot(doc, uri, resource);
521
+ docs.push(restamped), knownIds.add(uri);
522
+ }
523
+ return { docs, knownIds };
524
+ }
525
+ function restampForSnapshot(doc, gdrUri2, resource) {
526
+ return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
527
+ }
528
+ function rewriteRefsRecursive(value, resource) {
529
+ if (Array.isArray(value))
530
+ return value.map((v2) => rewriteRefsRecursive(v2, resource));
531
+ if (value === null || typeof value != "object") return value;
532
+ const obj = value, out = {};
533
+ for (const [k, v2] of Object.entries(obj))
534
+ k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
535
+ return out;
536
+ }
537
+ function findOpenStageEntry(host) {
538
+ return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
539
+ }
540
+ function collectWatchRefs(instance) {
541
+ const stage = findOpenStageEntry(instance);
542
+ return [
543
+ gdrRef(instance.workflowResource, instance._id, instance._type),
544
+ ...instance.ancestors,
545
+ ...slotDocRefs(instance.state),
546
+ ...slotDocRefs(stage?.state),
547
+ ...slotReleaseRefs(instance.state),
548
+ ...slotReleaseRefs(stage?.state)
549
+ ];
550
+ }
551
+ function readsRaw(ref) {
552
+ return ref.type === "workflow.instance" || ref.type === "system.release";
553
+ }
554
+ function contentReleaseName(args) {
555
+ const { ref, perspective } = args;
556
+ if (!readsRaw(ref) && Array.isArray(perspective))
557
+ return perspective.find((entry) => entry !== "drafts" && entry !== "published" && entry !== "raw");
558
+ }
559
+ function subscriptionDocumentsForInstance(instance) {
560
+ const seen = /* @__PURE__ */ new Set(), documents = [];
561
+ for (const ref of collectWatchRefs(instance))
562
+ !isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push({ ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type }));
563
+ return {
564
+ documents,
565
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
566
+ };
567
+ }
568
+ function slotDocRefs(slots) {
569
+ return slotEntries(slots).flatMap((slot) => slot._type === "workflow.state.doc.ref" ? isGdr(slot.value) ? [slot.value] : [] : slot._type === "workflow.state.doc.refs" ? Array.isArray(slot.value) ? slot.value.filter(isGdr) : [] : []);
570
+ }
571
+ function slotReleaseRefs(slots) {
572
+ return slotEntries(slots).flatMap(
573
+ (slot) => slot._type === "workflow.state.release.ref" && isGdr(slot.value) ? [slot.value] : []
574
+ );
575
+ }
576
+ function slotEntries(slots) {
577
+ return Array.isArray(slots) ? slots.filter(
578
+ (slot) => !!slot && typeof slot == "object"
579
+ ) : [];
580
+ }
581
+ async function hydrateSnapshot(args) {
582
+ const { client, clientForGdr, instance, overlay } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri, perspective) => {
583
+ if (visited.has(uri)) return;
584
+ const held = overlay?.get(uri);
585
+ if (held !== void 0) {
586
+ loaded.push(held), visited.add(uri);
587
+ return;
588
+ }
589
+ const fetched = await loadByGdr(
590
+ client,
591
+ clientForGdr,
592
+ instance.workflowResource,
593
+ uri,
594
+ perspective
595
+ );
596
+ fetched && (loaded.push(fetched), visited.add(uri));
597
+ };
598
+ loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
599
+ for (const ref of collectWatchRefs(instance))
600
+ await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
601
+ return buildSnapshot({ docs: loaded });
602
+ }
603
+ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
604
+ let parsed;
605
+ try {
606
+ parsed = parseGdr(uri);
607
+ } catch {
608
+ const doc2 = await readDoc(defaultClient, uri, perspective);
609
+ return doc2 ? { doc: doc2, resource: defaultResource } : null;
610
+ }
611
+ const routed = clientForGdr(parsed), doc = await readDoc(routed, parsed.documentId, perspective);
612
+ return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
613
+ }
614
+ async function readDoc(client, id, perspective) {
615
+ return perspective === void 0 ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
616
+ }
617
+ function resourceFromParsed(parsed) {
618
+ return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
619
+ }
620
+ function collectSlotDocUris(resolvedStateSlots) {
621
+ return slotDocRefs(resolvedStateSlots).map((ref) => ref.id);
622
+ }
623
+ const WORKFLOW_INSTANCE_TYPE = "workflow.instance";
624
+ class SlotValueShapeError extends Error {
625
+ slotType;
626
+ slotId;
627
+ issues;
628
+ constructor(args) {
629
+ const issueText = args.issues.join("; ");
630
+ super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
631
+ }
632
+ }
633
+ const GdrShape = v__namespace.looseObject({
634
+ id: v__namespace.pipe(
635
+ v__namespace.string(),
636
+ v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
637
+ ),
638
+ type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
639
+ }), ReleaseRefShape = v__namespace.looseObject({
640
+ id: v__namespace.pipe(
641
+ v__namespace.string(),
642
+ v__namespace.check((s) => isGdrUri(s), "must be a GDR URI")
643
+ ),
644
+ type: v__namespace.literal("system.release"),
645
+ releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
646
+ }), ActorShape = v__namespace.looseObject({
647
+ kind: v__namespace.picklist(["user", "ai", "system"]),
648
+ id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
649
+ roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
650
+ onBehalfOf: v__namespace.optional(v__namespace.string())
651
+ }), AssigneeShape = v__namespace.union([
652
+ v__namespace.looseObject({ kind: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
653
+ v__namespace.looseObject({ kind: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
654
+ ]), ChecklistItemShape = v__namespace.looseObject({
655
+ label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
656
+ done: v__namespace.boolean(),
657
+ doneBy: v__namespace.optional(v__namespace.string()),
658
+ doneAt: v__namespace.optional(v__namespace.string()),
659
+ _key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
660
+ }), NoteItemShape = v__namespace.pipe(
661
+ v__namespace.looseObject({
662
+ _key: v__namespace.optional(v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)))
663
+ }),
664
+ v__namespace.check(
665
+ (val) => typeof val == "object" && val !== null && !Array.isArray(val),
666
+ "must be an object"
667
+ )
668
+ ), NullableString = v__namespace.union([v__namespace.null(), v__namespace.string()]), NullableNumber = v__namespace.union([v__namespace.null(), v__namespace.number()]), NullableBoolean = v__namespace.union([v__namespace.null(), v__namespace.boolean()]), NullableDateTime = v__namespace.union([
669
+ v__namespace.null(),
670
+ v__namespace.pipe(
671
+ v__namespace.string(),
672
+ v__namespace.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
673
+ )
674
+ ]), NullableUrl = NullableString, valueSchemas = {
675
+ "workflow.state.doc.ref": v__namespace.union([v__namespace.null(), GdrShape]),
676
+ "workflow.state.doc.refs": v__namespace.array(GdrShape),
677
+ "workflow.state.release.ref": v__namespace.union([v__namespace.null(), ReleaseRefShape]),
678
+ "workflow.state.query": v__namespace.any(),
679
+ "workflow.state.value.string": NullableString,
680
+ "workflow.state.value.url": NullableUrl,
681
+ "workflow.state.value.number": NullableNumber,
682
+ "workflow.state.value.boolean": NullableBoolean,
683
+ "workflow.state.value.dateTime": NullableDateTime,
684
+ "workflow.state.value.actor": v__namespace.union([v__namespace.null(), ActorShape]),
685
+ "workflow.state.checklist": v__namespace.array(ChecklistItemShape),
686
+ "workflow.state.notes": v__namespace.array(NoteItemShape),
687
+ "workflow.state.assignees": v__namespace.array(AssigneeShape)
688
+ }, itemSchemas = {
689
+ "workflow.state.doc.refs": GdrShape,
690
+ "workflow.state.checklist": ChecklistItemShape,
691
+ "workflow.state.notes": NoteItemShape,
692
+ "workflow.state.assignees": AssigneeShape
693
+ };
694
+ function isAppendable(slotType) {
695
+ return slotType in itemSchemas;
696
+ }
697
+ function validateSlotValue(args) {
698
+ const schema2 = valueSchemas[args.slotType];
699
+ if (schema2 === void 0)
700
+ throw new SlotValueShapeError({
701
+ slotType: args.slotType,
702
+ slotId: args.slotId,
703
+ issues: [`unknown slot type ${args.slotType}`],
704
+ mode: "value"
705
+ });
706
+ const result = v__namespace.safeParse(schema2, args.value);
707
+ if (!result.success)
708
+ throw new SlotValueShapeError({
709
+ slotType: args.slotType,
710
+ slotId: args.slotId,
711
+ issues: formatIssues(result.issues),
712
+ mode: "value"
713
+ });
714
+ }
715
+ function validateSlotAppendItem(args) {
716
+ if (!isAppendable(args.slotType))
717
+ throw new SlotValueShapeError({
718
+ slotType: args.slotType,
719
+ slotId: args.slotId,
720
+ issues: [`slot type ${args.slotType} does not support append`],
721
+ mode: "item"
722
+ });
723
+ const schema2 = itemSchemas[args.slotType], result = v__namespace.safeParse(schema2, args.item);
724
+ if (!result.success)
725
+ throw new SlotValueShapeError({
726
+ slotType: args.slotType,
727
+ slotId: args.slotId,
728
+ issues: formatIssues(result.issues),
729
+ mode: "item"
730
+ });
415
731
  }
416
732
  function formatIssues(issues) {
417
- return issues.map((i) => `${i.path.length > 0 ? `at ${i.path.join(".")}: ` : ""}${i.message}`);
733
+ return issues.map((i) => {
734
+ const keys = i.path?.map((p) => p.key) ?? [];
735
+ return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
736
+ });
418
737
  }
419
738
  function derivePerspectiveFromState(slots) {
420
739
  if (slots !== void 0) {
@@ -459,7 +778,7 @@ async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
459
778
  state: stateMapFromResolved(ctx.resolvedState ?? []),
460
779
  stage: ctx.stageId ?? null,
461
780
  task: ctx.taskId ?? null,
462
- now: (/* @__PURE__ */ new Date()).toISOString(),
781
+ now: ctx.now,
463
782
  engineTags: ctx.tags ?? []
464
783
  });
465
784
  try {
@@ -476,7 +795,7 @@ async function resolveSlotValue(slot, initialState, ctx, defaultValue) {
476
795
  const source = slot.source;
477
796
  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;
478
797
  }
479
- function buildResolvedSlot(slot, value, _key) {
798
+ function buildResolvedSlot(slot, value, _key, now) {
480
799
  const titleProp = slot.title !== void 0 ? { title: slot.title } : {}, descriptionProp = slot.description !== void 0 ? { description: slot.description } : {};
481
800
  return slot.type === "workflow.state.query" ? {
482
801
  _key,
@@ -485,7 +804,7 @@ function buildResolvedSlot(slot, value, _key) {
485
804
  ...titleProp,
486
805
  ...descriptionProp,
487
806
  value,
488
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
807
+ resolvedAt: now
489
808
  } : {
490
809
  _key,
491
810
  _type: slot.type,
@@ -497,7 +816,7 @@ function buildResolvedSlot(slot, value, _key) {
497
816
  }
498
817
  async function resolveOneSlot(slot, initialState, ctx, randomKey2) {
499
818
  const defaultValue = defaultSlotValue(slot.type), value = await resolveSlotValue(slot, initialState, ctx, defaultValue);
500
- return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2());
819
+ return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2(), ctx.now);
501
820
  }
502
821
  function stateMapFromResolved(slots) {
503
822
  const out = {};
@@ -511,10 +830,10 @@ function assertInitValueShape(slot, value) {
511
830
  }
512
831
  if (slot.type === "workflow.state.release.ref") {
513
832
  assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
514
- const v = value;
515
- if (typeof v.releaseName != "string" || v.releaseName.length === 0)
833
+ const v2 = value;
834
+ if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
516
835
  throw new Error(
517
- `Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v.releaseName)}.`
836
+ `Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
518
837
  );
519
838
  return;
520
839
  }
@@ -532,18 +851,18 @@ function assertGdrShape(value, context) {
532
851
  throw new Error(
533
852
  `Invalid GDR for ${context}: expected { id: "<scheme>:...", type: "<schema>" }, got ${typeof value}.`
534
853
  );
535
- const v = value;
536
- if (typeof v.id != "string" || !isGdrUri(v.id))
854
+ const v2 = value;
855
+ if (typeof v2.id != "string" || !isGdrUri(v2.id))
537
856
  throw new Error(
538
- `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.`
857
+ `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.`
539
858
  );
540
- if (typeof v.type != "string" || v.type.length === 0)
859
+ if (typeof v2.type != "string" || v2.type.length === 0)
541
860
  throw new Error(
542
- `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v.type)}.`
861
+ `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
543
862
  );
544
863
  }
545
864
  function normalizeQueryResult(slotType, raw, workflowResource) {
546
- 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;
865
+ 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;
547
866
  }
548
867
  function toGdrUri(docId, workflowResource) {
549
868
  return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
@@ -568,25 +887,32 @@ async function loadContext(client, instanceId, options) {
568
887
  const instance = await client.getDocument(instanceId);
569
888
  if (!instance)
570
889
  throw new Error(`Workflow instance ${instanceId} not found`);
571
- const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition });
572
- return { client, clientForGdr, instance, definition, snapshot };
890
+ const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
891
+ client,
892
+ clientForGdr,
893
+ instance,
894
+ ...options?.overlay !== void 0 ? { overlay: options.overlay } : {}
895
+ });
896
+ return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
573
897
  }
574
898
  function ctxEvaluateFilter(ctx, filter) {
575
899
  return evaluateFilter({
576
900
  filter,
577
901
  definition: ctx.definition,
578
902
  snapshot: ctx.snapshot,
579
- params: buildParams(ctx.instance)
903
+ params: buildParams(ctx.instance, ctx.now)
580
904
  });
581
905
  }
582
906
  async function buildEngineContext(args) {
583
- const { client, clientForGdr, instance, definition } = args;
907
+ const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
584
908
  return {
585
909
  client,
586
910
  clientForGdr,
911
+ clock,
912
+ now: clock(),
587
913
  instance,
588
914
  definition,
589
- snapshot: await hydrateSnapshot({ client, clientForGdr, instance, definition })
915
+ snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
590
916
  };
591
917
  }
592
918
  function findStage(definition, stageId) {
@@ -596,12 +922,13 @@ function findStage(definition, stageId) {
596
922
  return stage;
597
923
  }
598
924
  async function resolveStageStateSlots(args) {
599
- const { client, instance, stage, initialState } = args;
925
+ const { client, instance, stage, now, initialState } = args;
600
926
  return resolveDeclaredState({
601
927
  slotDefs: stage.state,
602
928
  initialState: initialState ?? [],
603
929
  ctx: {
604
930
  client,
931
+ now,
605
932
  selfId: instance._id,
606
933
  tags: instance.tags ?? [],
607
934
  stageId: stage.id,
@@ -616,12 +943,13 @@ async function resolveStageStateSlots(args) {
616
943
  });
617
944
  }
618
945
  async function resolveTaskStateSlots(args) {
619
- const { client, instance, stage, task } = args;
946
+ const { client, instance, stage, task, now } = args;
620
947
  return resolveDeclaredState({
621
948
  slotDefs: task.state,
622
949
  initialState: [],
623
950
  ctx: {
624
951
  client,
952
+ now,
625
953
  selfId: instance._id,
626
954
  tags: instance.tags ?? [],
627
955
  stageId: stage.id,
@@ -633,73 +961,6 @@ async function resolveTaskStateSlots(args) {
633
961
  randomKey
634
962
  });
635
963
  }
636
- function readStateSlot(instance, scope, slotId, stageId, taskId) {
637
- if (scope === "workflow")
638
- return slotValue$1(instance.state?.find((s) => s.id === slotId));
639
- const stageEntry = instance.stages.find(
640
- (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
641
- );
642
- if (stageEntry === void 0) return;
643
- if (scope === "stage")
644
- return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
645
- const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
646
- return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
647
- }
648
- function slotValue$1(slot) {
649
- if (slot != null && typeof slot == "object")
650
- return slot.value;
651
- }
652
- function resolveSource(src, ctx) {
653
- switch (src.source) {
654
- case "literal":
655
- return src.value;
656
- case "param":
657
- return ctx.params?.[src.paramId];
658
- case "actor":
659
- return ctx.actor;
660
- case "now":
661
- return (/* @__PURE__ */ new Date()).toISOString();
662
- case "self":
663
- return ctx.instance._id;
664
- case "stageId":
665
- return ctx.stageId ?? ctx.instance.currentStageId;
666
- case "stateRead":
667
- return resolveStateRead(src, ctx);
668
- case "effectOutput":
669
- return resolveEffectOutput(src, ctx);
670
- case "object":
671
- return resolveObject(src, ctx);
672
- }
673
- }
674
- function resolveStateRead(src, ctx) {
675
- const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
676
- return src.path !== void 0 ? getPath(value, src.path) : value;
677
- }
678
- function resolveEffectOutput(src, ctx) {
679
- const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
680
- if (entry === void 0) return null;
681
- const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
682
- return value === void 0 ? null : value;
683
- }
684
- function resolveObject(src, ctx) {
685
- const out = {};
686
- for (const [field, fieldSrc] of Object.entries(src.fields))
687
- out[field] = resolveSource(fieldSrc, ctx);
688
- return out;
689
- }
690
- async function resolveBindings(args) {
691
- const { bindings, staticInput, instance, params, actor } = args, resolved = {};
692
- if (bindings) {
693
- const ctx = {
694
- instance,
695
- ...params !== void 0 ? { params } : {},
696
- ...actor !== void 0 ? { actor } : {}
697
- };
698
- for (const [key, src] of Object.entries(bindings))
699
- resolved[key] = resolveSource(src, ctx);
700
- }
701
- return { ...resolved, ...staticInput };
702
- }
703
964
  function effectsContextEntry(id, value) {
704
965
  const _key = randomKey();
705
966
  return typeof value == "string" ? { _key, _type: "effectsContext.string", id, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", id, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", id, value } : { _key, _type: "effectsContext.ref", id, value };
@@ -708,7 +969,7 @@ function buildInstanceBase(args) {
708
969
  const { id, now, actor, perspective } = args;
709
970
  return {
710
971
  _id: id,
711
- _type: "workflow.instance",
972
+ _type: WORKFLOW_INSTANCE_TYPE,
712
973
  _rev: "",
713
974
  _createdAt: now,
714
975
  _updatedAt: now,
@@ -738,175 +999,337 @@ function buildInstanceBase(args) {
738
999
  lastChangedAt: now
739
1000
  };
740
1001
  }
741
- function lakeGuardId(args) {
742
- return `temp.system.guard.${args.instanceDocId}.${args.stageId}.${args.index}`;
743
- }
744
- function compileGuard(args) {
1002
+ function startMutation(instance) {
745
1003
  return {
746
- _id: args.id,
747
- _type: "temp.system.guard",
748
- resourceType: args.resourceType,
749
- resourceId: args.resourceId,
750
- owner: args.owner,
751
- ...args.name !== void 0 ? { name: args.name } : {},
752
- ...args.description !== void 0 ? { description: args.description } : {},
753
- match: args.match,
754
- predicate: args.predicate,
755
- metadata: args.metadata
1004
+ currentStageId: instance.currentStageId,
1005
+ // Deep-ish copy. Per-scope state slot arrays need their own copies
1006
+ // so ops can mutate without leaking into the source.
1007
+ state: (instance.state ?? []).map((s) => ({ ...s })),
1008
+ stages: instance.stages.map((s) => ({
1009
+ ...s,
1010
+ state: (s.state ?? []).map((slot) => ({ ...slot })),
1011
+ tasks: s.tasks.map((t) => ({
1012
+ ...t,
1013
+ ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1014
+ }))
1015
+ })),
1016
+ pendingEffects: [...instance.pendingEffects],
1017
+ effectHistory: [...instance.effectHistory],
1018
+ effectsContext: [...instance.effectsContext],
1019
+ history: [...instance.history],
1020
+ lastChangedAt: instance.lastChangedAt,
1021
+ ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1022
+ pendingCreates: []
756
1023
  };
757
1024
  }
758
- function toGroqJsPredicate(predicate) {
759
- return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
760
- }
761
- function globMatch(pattern, value) {
762
- if (!pattern.includes("*")) return pattern === value;
763
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
764
- return new RegExp(`^${escaped}$`).test(value);
1025
+ function taskStatusChangedEntry(args) {
1026
+ return {
1027
+ _key: randomKey(),
1028
+ _type: "workflow.history.taskStatusChanged",
1029
+ at: args.at,
1030
+ stageId: args.stageId,
1031
+ taskId: args.taskId,
1032
+ from: args.from,
1033
+ to: args.to,
1034
+ ...args.actor !== void 0 ? { actor: args.actor } : {}
1035
+ };
765
1036
  }
766
- function guardMatches(guard, doc, action) {
767
- const m = guard.match;
768
- if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
769
- return !1;
770
- if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
771
- const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
772
- if (!byRef && !byPattern) return !1;
773
- }
774
- return !0;
1037
+ function stageTransitionHistory(args) {
1038
+ const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1039
+ at,
1040
+ ...transitionId !== void 0 ? { transitionId } : {},
1041
+ ...actor !== void 0 ? { actor } : {},
1042
+ ...via !== void 0 ? { via } : {},
1043
+ ...reason !== void 0 ? { reason } : {}
1044
+ };
1045
+ return [
1046
+ {
1047
+ _key: randomKey(),
1048
+ _type: "workflow.history.stageExited",
1049
+ stageId: fromStageId,
1050
+ toStageId,
1051
+ ...shared
1052
+ },
1053
+ {
1054
+ _key: randomKey(),
1055
+ _type: "workflow.history.transitionFired",
1056
+ fromStageId,
1057
+ toStageId,
1058
+ ...shared
1059
+ },
1060
+ {
1061
+ _key: randomKey(),
1062
+ _type: "workflow.history.stageEntered",
1063
+ stageId: toStageId,
1064
+ fromStageId,
1065
+ ...shared
1066
+ }
1067
+ ];
775
1068
  }
776
- async function evaluateMutationGuard(args) {
777
- const { guard, context } = args;
778
- if (guard.predicate === "") return !1;
779
- const params = { guard, mutation: { action: context.action } };
780
- try {
781
- const tree = groqJs.parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
782
- return await (await groqJs.evaluate(tree, {
783
- before: context.before,
784
- after: context.after,
785
- params,
786
- ...context.identity !== void 0 ? { identity: context.identity } : {}
787
- })).get() === !0;
788
- } catch {
789
- return !1;
790
- }
1069
+ function instanceStateFields(src) {
1070
+ const fields = {
1071
+ currentStageId: src.currentStageId,
1072
+ state: src.state,
1073
+ stages: src.stages,
1074
+ pendingEffects: src.pendingEffects,
1075
+ effectHistory: src.effectHistory,
1076
+ effectsContext: src.effectsContext,
1077
+ history: src.history
1078
+ };
1079
+ return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
791
1080
  }
792
- async function denyingGuards(args) {
793
- const { guards, doc, context } = args, denied = [];
794
- for (const guard of guards)
795
- guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
796
- return denied;
1081
+ function materializeInstance(base, mutation) {
1082
+ return {
1083
+ ...base,
1084
+ currentStageId: mutation.currentStageId,
1085
+ state: mutation.state,
1086
+ stages: mutation.stages
1087
+ };
797
1088
  }
798
- function resourceOf(p) {
799
- return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
1089
+ async function persist(ctx, mutation) {
1090
+ const set = {
1091
+ ...instanceStateFields(mutation),
1092
+ lastChangedAt: ctx.now
1093
+ }, pendingCreates = mutation.pendingCreates;
1094
+ if (pendingCreates.length === 0)
1095
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1096
+ const tx = ctx.client.transaction();
1097
+ for (const body of pendingCreates) tx.create(body);
1098
+ tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1099
+ const actorForPriming = void 0;
1100
+ for (const body of pendingCreates)
1101
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1102
+ const reloaded = await ctx.client.getDocument(ctx.instance._id);
1103
+ if (!reloaded)
1104
+ throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1105
+ return reloaded;
800
1106
  }
801
- function resolveIdRefTarget(src, ctx) {
802
- const value = resolveSource(src, ctx);
803
- if (typeof value == "string" && isGdrUri(value))
804
- return { parsed: parseGdr(value) };
805
- if (value && typeof value == "object") {
806
- const v = value;
807
- if (typeof v.id == "string" && isGdrUri(v.id))
808
- return typeof v.type == "string" ? { parsed: parseGdr(v.id), type: v.type } : { parsed: parseGdr(v.id) };
809
- }
810
- return null;
1107
+ function currentStageEntry(mutation) {
1108
+ const entry = findOpenStageEntry(mutation);
1109
+ if (entry === void 0)
1110
+ throw new Error(
1111
+ `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
1112
+ );
1113
+ return entry;
811
1114
  }
812
- function resolveIdRefTargets(idRefs, ctx) {
813
- const targets = [];
814
- for (const src of idRefs ?? []) {
815
- const target = resolveIdRefTarget(src, ctx);
816
- if (target === null) return null;
817
- targets.push(target);
818
- }
819
- return targets.length === 0 ? null : targets;
1115
+ function currentTasks(mutation) {
1116
+ return currentStageEntry(mutation).tasks;
820
1117
  }
821
- function assertSingleResource(targets) {
822
- const resource = resourceOf(targets[0].parsed);
823
- for (const g of targets) {
824
- const r = resourceOf(g.parsed);
825
- if (r.type !== resource.type || r.id !== resource.id)
826
- throw new Error(
827
- `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
828
- );
829
- }
830
- return resource;
1118
+ function findTaskInCurrentStage(ctx, taskId) {
1119
+ const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1120
+ if (task === void 0)
1121
+ throw new Error(
1122
+ `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1123
+ );
1124
+ return { stage, task };
831
1125
  }
832
- function bareIdRefs(targets) {
833
- return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
1126
+ function requireMutationTaskEntry(mutation, taskId) {
1127
+ const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1128
+ if (mutEntry === void 0)
1129
+ throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1130
+ return mutEntry;
834
1131
  }
835
- function resolveMatchTypes(targets, authorTypes) {
836
- if (authorTypes !== void 0) return authorTypes;
837
- const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
838
- return inferred.length > 0 ? inferred : void 0;
1132
+ function findCurrentStageEntry(instance) {
1133
+ return findOpenStageEntry(instance);
839
1134
  }
840
- function resolveMetadata(metadata, ctx) {
841
- const out = {};
842
- for (const [k, src] of Object.entries(metadata ?? {}))
843
- out[k] = resolveSource(src, ctx);
844
- return out;
1135
+ function findCurrentTasks(instance) {
1136
+ return findCurrentStageEntry(instance)?.tasks ?? [];
845
1137
  }
846
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
847
- function resolveGuard(guard, index, instance, stageId) {
848
- const ctx = { instance, stageId }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
849
- if (targets === null) return null;
850
- const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
851
- return { doc: compileGuard({
852
- id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
853
- resourceType: resource.type,
854
- resourceId: resource.id,
855
- owner: GUARD_OWNER,
856
- ...guard.name !== void 0 ? { name: guard.name } : {},
857
- ...guard.description !== void 0 ? { description: guard.description } : {},
858
- match: {
859
- ...types !== void 0 ? { types } : {},
860
- idRefs: bareIdRefs(targets),
861
- ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
862
- actions: guard.match.actions
863
- },
864
- predicate: guard.predicate ?? "",
865
- metadata: resolveMetadata(guard.metadata, ctx)
866
- }), routeGdr: targets[0].parsed };
1138
+ async function completeEffect(args) {
1139
+ const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1140
+ ...options?.clock ? { clock: options.clock } : {}
1141
+ });
1142
+ return commitCompleteEffect(
1143
+ ctx,
1144
+ effectKey,
1145
+ status,
1146
+ outputs,
1147
+ detail,
1148
+ error,
1149
+ durationMs,
1150
+ options?.actor
1151
+ );
867
1152
  }
868
- async function upsertGuard(client, doc) {
869
- const existing = await client.getDocument(doc._id), set = {
870
- resourceType: doc.resourceType,
871
- resourceId: doc.resourceId,
872
- owner: doc.owner,
873
- match: doc.match,
874
- predicate: doc.predicate,
875
- metadata: doc.metadata
1153
+ function buildEffectHistoryEntry(pending, outcome) {
1154
+ const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
1155
+ return {
1156
+ _key: pending._key,
1157
+ effectId: pending.effectId,
1158
+ ...pending.title !== void 0 ? { title: pending.title } : {},
1159
+ ...pending.description !== void 0 ? { description: pending.description } : {},
1160
+ params: pending.params,
1161
+ origin: pending.origin,
1162
+ ...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
1163
+ ranAt,
1164
+ ...durationMs !== void 0 ? { durationMs } : {},
1165
+ status,
1166
+ ...detail !== void 0 ? { detail } : {},
1167
+ ...error !== void 0 ? { error } : {},
1168
+ ...outputs !== void 0 ? { outputs } : {}
876
1169
  };
877
- 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);
878
1170
  }
879
- function resolvedStageGuards(args) {
880
- const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
881
- for (const [index, guard] of (stage?.guards ?? []).entries()) {
882
- const resolved = resolveGuard(guard, index, args.instance, args.stageId);
883
- resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
1171
+ function upsertEffectsContext(mutation, outputs) {
1172
+ for (const [id, value] of Object.entries(outputs)) {
1173
+ const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1174
+ existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
884
1175
  }
885
- return out;
886
1176
  }
887
- async function deployStageGuards(args) {
888
- for (const { client, doc } of resolvedStageGuards(args))
889
- await upsertGuard(client, doc);
1177
+ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1178
+ const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1179
+ if (pending === void 0)
1180
+ throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1181
+ const mutation = startMutation(ctx.instance);
1182
+ mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1183
+ const ranAt = ctx.now;
1184
+ return mutation.effectHistory.push(
1185
+ buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1186
+ ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1187
+ _key: randomKey(),
1188
+ _type: "workflow.history.effectCompleted",
1189
+ at: ranAt,
1190
+ effectKey,
1191
+ effectId: pending.effectId,
1192
+ status,
1193
+ ...outputs !== void 0 ? { outputs } : {},
1194
+ ...detail !== void 0 ? { detail } : {},
1195
+ ...actor !== void 0 ? { actor } : {}
1196
+ }), await persist(ctx, mutation), { effectKey, status };
890
1197
  }
891
- async function retractStageGuards(args) {
892
- for (const { client, doc } of resolvedStageGuards(args))
893
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
1198
+ function buildQueuedEffect(effect, origin, params, actor, now) {
1199
+ const key = randomKey(), pending = {
1200
+ _key: key,
1201
+ _type: "workflow.pendingEffect",
1202
+ effectId: effect.id,
1203
+ ...effect.title !== void 0 ? { title: effect.title } : {},
1204
+ ...effect.description !== void 0 ? { description: effect.description } : {},
1205
+ ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1206
+ params,
1207
+ origin,
1208
+ ...actor !== void 0 ? { actor } : {},
1209
+ queuedAt: now
1210
+ }, history = {
1211
+ _key: randomKey(),
1212
+ _type: "workflow.history.effectQueued",
1213
+ at: now,
1214
+ effectKey: key,
1215
+ effectId: effect.id,
1216
+ origin
1217
+ };
1218
+ return { pending, history };
894
1219
  }
895
- async function reconcileStageGuards(args) {
896
- const { client, clientForGdr, instance, definition, exitedStageId, enteredStageId } = args;
897
- exitedStageId !== void 0 && await retractStageGuards({
898
- client,
899
- clientForGdr,
900
- instance,
901
- definition,
902
- stageId: exitedStageId
903
- }), enteredStageId !== void 0 && await deployStageGuards({
904
- client,
905
- clientForGdr,
906
- instance,
907
- definition,
908
- stageId: enteredStageId
909
- });
1220
+ async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1221
+ if (!effects) return;
1222
+ const now = ctx.now, liveInstance = {
1223
+ ...ctx.instance,
1224
+ state: mutation.state,
1225
+ stages: mutation.stages,
1226
+ effectsContext: mutation.effectsContext
1227
+ };
1228
+ for (const effect of effects) {
1229
+ const params = await resolveBindings({
1230
+ bindings: effect.bindings,
1231
+ staticInput: effect.input,
1232
+ instance: liveInstance,
1233
+ now,
1234
+ ...callerParams !== void 0 ? { params: callerParams } : {},
1235
+ ...actor !== void 0 ? { actor } : {}
1236
+ }), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
1237
+ mutation.pendingEffects.push(pending), mutation.history.push(history);
1238
+ }
1239
+ }
1240
+ class ActionParamsInvalidError extends Error {
1241
+ action;
1242
+ taskId;
1243
+ issues;
1244
+ constructor(args) {
1245
+ const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1246
+ `);
1247
+ super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
1248
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
1249
+ }
1250
+ }
1251
+ class WorkflowStateDivergedError extends Error {
1252
+ instanceId;
1253
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
1254
+ guardError;
1255
+ /** The rollback failure, when rollback was attempted and itself failed. */
1256
+ rollbackError;
1257
+ constructor(args) {
1258
+ super(
1259
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1260
+ { cause: args.guardError }
1261
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1262
+ }
1263
+ }
1264
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1265
+ class ConcurrentFireActionError extends Error {
1266
+ instanceId;
1267
+ taskId;
1268
+ action;
1269
+ attempts;
1270
+ constructor(args) {
1271
+ super(
1272
+ `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.`
1273
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.taskId = args.taskId, this.action = args.action, this.attempts = args.attempts;
1274
+ }
1275
+ }
1276
+ function isRevisionConflict(error) {
1277
+ if (typeof error != "object" || error === null) return !1;
1278
+ const { statusCode, message } = error;
1279
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1280
+ }
1281
+ class CascadeLimitError extends Error {
1282
+ instanceId;
1283
+ limit;
1284
+ constructor(args) {
1285
+ super(
1286
+ `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.`
1287
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1288
+ }
1289
+ }
1290
+ async function deployOrRollback(args) {
1291
+ try {
1292
+ await args.deploy();
1293
+ } catch (guardError) {
1294
+ if (!args.reversible)
1295
+ throw new WorkflowStateDivergedError({
1296
+ instanceId: args.instanceId,
1297
+ guardError,
1298
+ reason: "the move created child instances a parent-only rollback cannot undo"
1299
+ });
1300
+ try {
1301
+ let patch = args.client.patch(args.instanceId).set(args.restore);
1302
+ args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1303
+ } catch (rollbackError) {
1304
+ throw new WorkflowStateDivergedError({
1305
+ instanceId: args.instanceId,
1306
+ guardError,
1307
+ rollbackError,
1308
+ reason: "rollback of the committed move failed"
1309
+ });
1310
+ }
1311
+ throw guardError;
1312
+ }
1313
+ }
1314
+ const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
1315
+ function validateTags(tags) {
1316
+ if (tags.length === 0)
1317
+ throw new Error("tags: must be a non-empty array");
1318
+ for (const tag of tags)
1319
+ if (!TAG_RE.test(tag))
1320
+ throw new Error(
1321
+ `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
1322
+ );
1323
+ }
1324
+ function canonicalTag(tags) {
1325
+ return tags[0];
1326
+ }
1327
+ function tagScopeFilter(param = "engineTags") {
1328
+ return `count(tags[@ in $${param}]) > 0`;
1329
+ }
1330
+ function definitionLookupGroq(explicit) {
1331
+ const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && workflowId == $workflowId && ${tagScopeFilter()}`;
1332
+ return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
910
1333
  }
911
1334
  async function discoverItems(args) {
912
1335
  const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
@@ -923,31 +1346,29 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
923
1346
  const MAX_SPAWN_DEPTH = 6;
924
1347
  async function spawnChildren(ctx, mutation, task, spawn, actor) {
925
1348
  if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
926
- const chain = [
927
- ...ctx.instance.ancestors.map((a) => a.id),
928
- gdrFromResource(ctx.instance.workflowResource, ctx.instance._id)
929
- ].join(" \u2192 ");
1349
+ const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
930
1350
  throw new Error(
931
1351
  `Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
932
1352
  );
933
1353
  }
934
- const rows = await resolveSpawnRows(ctx, spawn), refs = [];
1354
+ const now = ctx.now, rows = await resolveSpawnRows(ctx, spawn), refs = [];
935
1355
  for (const row of rows) {
936
- const projected = projectSpawnRow(ctx.instance, spawn, row), { ref, body } = await prepareChildInstance({
1356
+ const projected = projectSpawnRow(ctx.instance, spawn, row, now), { ref, body } = await prepareChildInstance({
937
1357
  client: ctx.client,
938
1358
  parent: ctx.instance,
939
1359
  task,
940
1360
  spawn,
941
1361
  initialState: projected.initialState,
942
1362
  effectsContext: projected.effectsContext,
943
- actor
1363
+ actor,
1364
+ now
944
1365
  });
945
1366
  refs.push(ref), mutation.pendingCreates.push(body);
946
1367
  }
947
1368
  return refs;
948
1369
  }
949
1370
  async function resolveSpawnRows(ctx, spawn) {
950
- const params = buildParams(ctx.instance), groq = spawn.forEach.groq;
1371
+ const params = buildParams(ctx.instance, ctx.now), groq = spawn.forEach.groq;
951
1372
  let value;
952
1373
  if (groq.includes("*")) {
953
1374
  const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
@@ -969,7 +1390,7 @@ function firstDocRefGdrUri(slots) {
969
1390
  if (s._type === "workflow.state.doc.ref" && s.value !== null) return s.value.id;
970
1391
  }
971
1392
  }
972
- function resolveSpawnSource(source, parent, row) {
1393
+ function resolveSpawnSource(source, parent, row, now) {
973
1394
  switch (source.source) {
974
1395
  case "literal":
975
1396
  return source.value;
@@ -980,15 +1401,15 @@ function resolveSpawnSource(source, parent, row) {
980
1401
  return slot === void 0 ? null : source.path !== void 0 ? getPath(slot, source.path) : slot;
981
1402
  }
982
1403
  case "self":
983
- return gdrFromResource(parent.workflowResource, parent._id);
1404
+ return selfGdr(parent);
984
1405
  case "stageId":
985
1406
  return parent.currentStageId;
986
1407
  case "now":
987
- return (/* @__PURE__ */ new Date()).toISOString();
1408
+ return now;
988
1409
  case "object": {
989
1410
  const out = {};
990
- for (const [k, v] of Object.entries(source.fields))
991
- out[k] = resolveSpawnSource(v, parent, row);
1411
+ for (const [k, v2] of Object.entries(source.fields))
1412
+ out[k] = resolveSpawnSource(v2, parent, row, now);
992
1413
  return out;
993
1414
  }
994
1415
  case "actor":
@@ -998,10 +1419,10 @@ function resolveSpawnSource(source, parent, row) {
998
1419
  return null;
999
1420
  }
1000
1421
  }
1001
- function projectSpawnRow(parent, spawn, row) {
1422
+ function projectSpawnRow(parent, spawn, row, now) {
1002
1423
  const initialState = [];
1003
1424
  for (const entry of spawn.forEach.as.initialState ?? []) {
1004
- const raw = resolveSpawnSource(entry.value, parent, row), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1425
+ const raw = resolveSpawnSource(entry.value, parent, row, now), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1005
1426
  initialState.push({
1006
1427
  type: entry.type,
1007
1428
  id: entry.id,
@@ -1010,13 +1431,13 @@ function projectSpawnRow(parent, spawn, row) {
1010
1431
  }
1011
1432
  const effectsContext = {};
1012
1433
  for (const [k, src] of Object.entries(spawn.forEach.as.effectsContext ?? {})) {
1013
- const v = resolveSpawnSource(src, parent, row);
1014
- v != null && (typeof v == "string" || typeof v == "number" || typeof v == "boolean" || typeof v == "object" && "id" in v && "type" in v) && (effectsContext[k] = v);
1434
+ const v2 = resolveSpawnSource(src, parent, row, now);
1435
+ v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (effectsContext[k] = v2);
1015
1436
  }
1016
1437
  return { initialState, effectsContext };
1017
1438
  }
1018
1439
  function canonicaliseSpawnValue(slotType, value, workflowResource) {
1019
- 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;
1440
+ 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;
1020
1441
  }
1021
1442
  function coerceSpawnGdr(raw, workflowResource) {
1022
1443
  const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
@@ -1033,30 +1454,32 @@ function coerceSpawnGdr(raw, workflowResource) {
1033
1454
  return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1034
1455
  }
1035
1456
  async function resolveLogicalDefinition(client, ref, tags) {
1036
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
1037
- wantsExplicit && (params.version = ref.version);
1038
- const groq = wantsExplicit ? '*[_type == "workflow.definition" && workflowId == $workflowId && version == $version && count(tags[@ in $tags]) > 0][0]' : '*[_type == "workflow.definition" && workflowId == $workflowId && count(tags[@ in $tags]) > 0] | order(version desc)[0]';
1039
- return await client.fetch(groq, params) ?? null;
1457
+ const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
1458
+ return wantsExplicit && (params.version = ref.version), await client.fetch(
1459
+ definitionLookupGroq(wantsExplicit),
1460
+ params
1461
+ ) ?? null;
1040
1462
  }
1041
1463
  async function prepareChildInstance(args) {
1042
- const { client, parent, task, spawn, initialState, effectsContext, actor } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1464
+ const { client, parent, task, spawn, initialState, effectsContext, actor, now } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1043
1465
  if (definition === null) {
1044
1466
  const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
1045
1467
  throw new Error(
1046
1468
  `Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
1047
1469
  );
1048
1470
  }
1049
- const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: "workflow.instance" }, ancestors = [
1471
+ const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
1050
1472
  ...parent.ancestors,
1051
1473
  {
1052
- id: gdrFromResource(parent.workflowResource, parent._id),
1053
- type: "workflow.instance"
1474
+ id: selfGdr(parent),
1475
+ type: WORKFLOW_INSTANCE_TYPE
1054
1476
  }
1055
1477
  ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1056
1478
  slotDefs: definition.state,
1057
1479
  initialState,
1058
1480
  ctx: {
1059
1481
  client,
1482
+ now,
1060
1483
  selfId: childDocId,
1061
1484
  tags: childTags,
1062
1485
  workflowResource,
@@ -1076,7 +1499,7 @@ async function prepareChildInstance(args) {
1076
1499
  }
1077
1500
  ), base = buildInstanceBase({
1078
1501
  id: childDocId,
1079
- now: (/* @__PURE__ */ new Date()).toISOString(),
1502
+ now,
1080
1503
  tags: childTags,
1081
1504
  workflowResource,
1082
1505
  workflowId: definition.workflowId,
@@ -1112,7 +1535,9 @@ async function checkSpawnCompletion(client, entry, spawn) {
1112
1535
  }
1113
1536
  }
1114
1537
  async function invokeTask(args) {
1115
- const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId);
1538
+ const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId, {
1539
+ ...options?.clock ? { clock: options.clock } : {}
1540
+ });
1116
1541
  return commitInvoke(ctx, taskId, options?.actor);
1117
1542
  }
1118
1543
  async function commitInvoke(ctx, taskId, actor) {
@@ -1124,41 +1549,41 @@ async function commitInvoke(ctx, taskId, actor) {
1124
1549
  const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1125
1550
  return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, taskId };
1126
1551
  }
1127
- async function autoResolveOnActivate(ctx, mutation, task, entry) {
1552
+ async function autoResolveOnActivate(ctx, mutation, task, entry, now) {
1128
1553
  const checks = [
1129
1554
  { filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
1130
1555
  { filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
1131
1556
  ];
1132
- for (const { filter, outcome, systemId } of checks) {
1133
- if (filter === void 0 || !await ctxEvaluateFilter(ctx, filter)) continue;
1134
- const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1135
- return entry.status = outcome, entry.completedAt = completedAt, entry.completedBy = systemId, mutation.history.push(
1136
- taskStatusChangedEntry({
1137
- stageId: mutation.currentStageId,
1138
- taskId: task.id,
1139
- from: "active",
1140
- to: outcome,
1141
- actor: { kind: "system", id: systemId },
1142
- at: completedAt
1143
- })
1144
- ), !0;
1145
- }
1557
+ for (const { filter, outcome, systemId } of checks)
1558
+ if (filter !== void 0 && await ctxEvaluateFilter(ctx, filter))
1559
+ return entry.status = outcome, entry.completedAt = now, entry.completedBy = systemId, mutation.history.push(
1560
+ taskStatusChangedEntry({
1561
+ stageId: mutation.currentStageId,
1562
+ taskId: task.id,
1563
+ from: "active",
1564
+ to: outcome,
1565
+ at: now,
1566
+ actor: { kind: "system", id: systemId }
1567
+ })
1568
+ ), !0;
1146
1569
  return !1;
1147
1570
  }
1148
1571
  async function activateTask(ctx, mutation, task, entry, actor) {
1149
- if (entry.status = "active", entry.startedAt = (/* @__PURE__ */ new Date()).toISOString(), task.state !== void 0 && task.state.length > 0) {
1572
+ const now = ctx.now;
1573
+ if (entry.status = "active", entry.startedAt = now, task.state !== void 0 && task.state.length > 0) {
1150
1574
  const stage = findStage(ctx.definition, mutation.currentStageId);
1151
1575
  entry.state = await resolveTaskStateSlots({
1152
1576
  client: ctx.client,
1153
1577
  instance: ctx.instance,
1154
1578
  stage,
1155
- task
1579
+ task,
1580
+ now
1156
1581
  });
1157
1582
  }
1158
- if (!await autoResolveOnActivate(ctx, mutation, task, entry) && (mutation.history.push({
1583
+ if (!await autoResolveOnActivate(ctx, mutation, task, entry, now) && (mutation.history.push({
1159
1584
  _key: randomKey(),
1160
1585
  _type: "workflow.history.taskActivated",
1161
- at: (/* @__PURE__ */ new Date()).toISOString(),
1586
+ at: now,
1162
1587
  stageId: mutation.currentStageId,
1163
1588
  taskId: task.id,
1164
1589
  ...actor !== void 0 ? { actor } : {}
@@ -1178,18 +1603,19 @@ async function activateTask(ctx, mutation, task, entry, actor) {
1178
1603
  mutation.history.push({
1179
1604
  _key: randomKey(),
1180
1605
  _type: "workflow.history.spawned",
1181
- at: (/* @__PURE__ */ new Date()).toISOString(),
1606
+ at: now,
1182
1607
  taskId: task.id,
1183
1608
  instanceRef: ref
1184
1609
  });
1185
1610
  if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
1186
1611
  const previousStatus = entry.status;
1187
- entry.status = "done", entry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1612
+ entry.status = "done", entry.completedAt = now, mutation.history.push(
1188
1613
  taskStatusChangedEntry({
1189
1614
  stageId: mutation.currentStageId,
1190
1615
  taskId: task.id,
1191
1616
  from: previousStatus,
1192
1617
  to: "done",
1618
+ at: now,
1193
1619
  ...actor !== void 0 ? { actor } : {}
1194
1620
  })
1195
1621
  );
@@ -1206,7 +1632,7 @@ async function buildStageTasks(ctx, stage) {
1206
1632
  status: inScope ? "pending" : "skipped",
1207
1633
  ...task.filter !== void 0 ? {
1208
1634
  filterEvaluation: {
1209
- at: (/* @__PURE__ */ new Date()).toISOString(),
1635
+ at: ctx.now,
1210
1636
  truthy: inScope
1211
1637
  }
1212
1638
  } : {}
@@ -1218,7 +1644,9 @@ function activatesOnStageEnter(task) {
1218
1644
  return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
1219
1645
  }
1220
1646
  async function setStage(args) {
1221
- const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId);
1647
+ const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId, {
1648
+ ...options?.clock ? { clock: options.clock } : {}
1649
+ });
1222
1650
  return commitSetStage(ctx, targetStageId, reason, options?.actor);
1223
1651
  }
1224
1652
  async function enterStage(ctx, mutation, nextStage, actor, at) {
@@ -1230,7 +1658,8 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1230
1658
  state: await resolveStageStateSlots({
1231
1659
  client: ctx.client,
1232
1660
  instance: ctx.instance,
1233
- stage: nextStage
1661
+ stage: nextStage,
1662
+ now: at
1234
1663
  }),
1235
1664
  tasks: await buildStageTasks(ctx, nextStage)
1236
1665
  };
@@ -1249,7 +1678,7 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1249
1678
  nextStage.kind === "terminal" && (mutation.completedAt = at);
1250
1679
  }
1251
1680
  async function beginStageMove(ctx, currentStage, actor) {
1252
- const mutation = startMutation(ctx.instance), at = (/* @__PURE__ */ new Date()).toISOString();
1681
+ const mutation = startMutation(ctx.instance), at = ctx.now;
1253
1682
  return await queueEffects(
1254
1683
  ctx,
1255
1684
  mutation,
@@ -1283,13 +1712,46 @@ async function commitSetStage(ctx, targetStageId, reason, actor) {
1283
1712
  };
1284
1713
  }
1285
1714
  async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1286
- await persist(ctx, mutation), await reconcileStageGuards({
1715
+ await persistThenDeploy(
1716
+ ctx,
1717
+ mutation,
1718
+ () => deployStageGuards({
1719
+ client: ctx.client,
1720
+ clientForGdr: ctx.clientForGdr,
1721
+ instance: materializeInstance(ctx.instance, mutation),
1722
+ definition: ctx.definition,
1723
+ stageId: enteredStageId,
1724
+ now: ctx.now
1725
+ })
1726
+ ), await retractStageGuards({
1287
1727
  client: ctx.client,
1288
1728
  clientForGdr: ctx.clientForGdr,
1289
1729
  instance: ctx.instance,
1290
1730
  definition: ctx.definition,
1291
- exitedStageId,
1292
- enteredStageId
1731
+ stageId: exitedStageId,
1732
+ now: ctx.now
1733
+ });
1734
+ }
1735
+ async function persistThenDeploy(ctx, mutation, deploy) {
1736
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1737
+ await deployOrRollback({
1738
+ client: ctx.client,
1739
+ instanceId: ctx.instance._id,
1740
+ committedRev: committed._rev,
1741
+ restore: instanceStateFields(ctx.instance),
1742
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1743
+ reversible: !spawned,
1744
+ deploy
1745
+ });
1746
+ }
1747
+ async function refreshStageGuards(ctx, mutation, stageId) {
1748
+ await deployStageGuards({
1749
+ client: ctx.client,
1750
+ clientForGdr: ctx.clientForGdr,
1751
+ instance: materializeInstance(ctx.instance, mutation),
1752
+ definition: ctx.definition,
1753
+ stageId,
1754
+ now: ctx.now
1293
1755
  });
1294
1756
  }
1295
1757
  async function commitTransition(ctx, action, actor) {
@@ -1331,26 +1793,29 @@ async function pickTransition(ctx, stage, action) {
1331
1793
  if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
1332
1794
  }
1333
1795
  async function evaluateAutoTransitions(args) {
1334
- const { client, instanceId, options, clientForGdr } = args, ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0);
1796
+ const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
1797
+ ...clientForGdr ? { clientForGdr } : {},
1798
+ ...clock ? { clock } : {},
1799
+ ...overlay ? { overlay } : {}
1800
+ });
1335
1801
  return commitTransition(ctx, void 0, options?.actor);
1336
1802
  }
1337
- async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1803
+ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
1338
1804
  const instance = await client.getDocument(instanceId);
1339
1805
  if (!instance || instance.stages.length > 0 || instance.pendingEffects.length > 0) return;
1340
1806
  const definition = JSON.parse(instance.definitionSnapshot), stage = definition.stages.find((s) => s.id === instance.currentStageId);
1341
1807
  if (stage === void 0) return;
1342
- const snapshot = await hydrateSnapshot({
1808
+ const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
1343
1809
  client,
1344
1810
  clientForGdr: clientForGdr ?? (() => client),
1345
- instance,
1346
- definition
1811
+ instance
1347
1812
  }), tasks = [];
1348
1813
  for (const task of stage.tasks ?? []) {
1349
1814
  const inScope = await evaluateFilter({
1350
1815
  filter: task.filter,
1351
1816
  definition,
1352
1817
  snapshot,
1353
- params: buildParams(instance)
1818
+ params: buildParams(instance, now)
1354
1819
  });
1355
1820
  tasks.push({
1356
1821
  _key: randomKey(),
@@ -1361,8 +1826,8 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1361
1826
  const initialStageEntry = {
1362
1827
  _key: randomKey(),
1363
1828
  id: stage.id,
1364
- enteredAt: (/* @__PURE__ */ new Date()).toISOString(),
1365
- state: await resolveStageStateSlots({ client, instance, stage }),
1829
+ enteredAt: now,
1830
+ state: await resolveStageStateSlots({ client, instance, stage, now }),
1366
1831
  tasks
1367
1832
  }, pendingEffects = [], newHistory = [...instance.history];
1368
1833
  for (const effect of stage.onEnter ?? []) {
@@ -1370,29 +1835,44 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1370
1835
  bindings: effect.bindings,
1371
1836
  staticInput: effect.input,
1372
1837
  instance,
1838
+ now,
1373
1839
  ...actor !== void 0 ? { actor } : {}
1374
1840
  }), { pending, history } = buildQueuedEffect(
1375
1841
  effect,
1376
1842
  { kind: "stageEnter", id: stage.id },
1377
1843
  params,
1378
- actor
1844
+ actor,
1845
+ now
1379
1846
  );
1380
1847
  pendingEffects.push(pending), newHistory.push(history);
1381
1848
  }
1382
- await client.patch(instance._id).set({
1849
+ const committed = await client.patch(instance._id).set({
1383
1850
  stages: [initialStageEntry],
1384
1851
  pendingEffects,
1385
1852
  history: newHistory,
1386
- lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1387
- }).ifRevisionId(instance._rev).commit(), await deployStageGuards({
1853
+ lastChangedAt: now
1854
+ }).ifRevisionId(instance._rev).commit();
1855
+ await deployOrRollback({
1388
1856
  client,
1389
- clientForGdr: clientForGdr ?? (() => client),
1390
- instance,
1391
- definition,
1392
- stageId: stage.id
1393
- }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr);
1857
+ instanceId: instance._id,
1858
+ committedRev: committed._rev,
1859
+ restore: {
1860
+ stages: instance.stages,
1861
+ pendingEffects: instance.pendingEffects,
1862
+ history: instance.history
1863
+ },
1864
+ reversible: !0,
1865
+ deploy: () => deployStageGuards({
1866
+ client,
1867
+ clientForGdr: clientForGdr ?? (() => client),
1868
+ instance,
1869
+ definition,
1870
+ stageId: stage.id,
1871
+ now
1872
+ })
1873
+ }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
1394
1874
  }
1395
- async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr) {
1875
+ async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
1396
1876
  const resolvedClientForGdr = clientForGdr ?? (() => client);
1397
1877
  for (const task of stage.tasks ?? []) {
1398
1878
  if (!activatesOnStageEnter(task)) continue;
@@ -1402,7 +1882,8 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
1402
1882
  client,
1403
1883
  clientForGdr: resolvedClientForGdr,
1404
1884
  instance: updated,
1405
- definition
1885
+ definition,
1886
+ ...clock ? { clock } : {}
1406
1887
  }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1407
1888
  mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
1408
1889
  }
@@ -1420,8 +1901,12 @@ async function gatherAutoResolveCandidates(ctx, tasks) {
1420
1901
  }
1421
1902
  return candidates;
1422
1903
  }
1423
- async function resolveCompleteWhen(client, instanceId, clientForGdr) {
1424
- const ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1904
+ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
1905
+ const ctx = await loadContext(client, instanceId, {
1906
+ ...clientForGdr ? { clientForGdr } : {},
1907
+ ...clock ? { clock } : {},
1908
+ ...overlay ? { overlay } : {}
1909
+ }), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1425
1910
  (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
1426
1911
  );
1427
1912
  if (tasksWithAutoPredicate.length === 0) return 0;
@@ -1435,339 +1920,517 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr) {
1435
1920
  const previousStatus = mutEntry.status, actor = {
1436
1921
  kind: "system",
1437
1922
  id: outcome === "failed" ? "engine.failWhen" : "engine.completeWhen"
1438
- };
1439
- mutEntry.status = outcome, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutEntry.completedBy = actor.id, mutation.history.push(
1923
+ }, now = ctx.now;
1924
+ mutEntry.status = outcome, mutEntry.completedAt = now, mutEntry.completedBy = actor.id, mutation.history.push(
1440
1925
  taskStatusChangedEntry({
1441
1926
  stageId: stage.id,
1442
1927
  taskId: task.id,
1443
1928
  from: previousStatus,
1444
1929
  to: outcome,
1930
+ at: now,
1445
1931
  actor
1446
1932
  })
1447
1933
  ), count++;
1448
1934
  }
1449
1935
  return count > 0 && await persist(ctx, mutation), count;
1450
1936
  }
1451
- async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr) {
1937
+ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, clock, overlay) {
1452
1938
  let count = 0;
1453
1939
  for (; ; ) {
1454
- if (await resolveCompleteWhen(client, instanceId, clientForGdr), !(await evaluateAutoTransitions({
1940
+ if (await resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay), !(await evaluateAutoTransitions({
1455
1941
  client,
1456
1942
  instanceId,
1457
1943
  ...actor !== void 0 ? { options: { actor } } : {},
1458
- ...clientForGdr ? { clientForGdr } : {}
1944
+ ...clientForGdr ? { clientForGdr } : {},
1945
+ ...clock ? { clock } : {},
1946
+ ...overlay ? { overlay } : {}
1459
1947
  })).fired) return count;
1460
- if (count++, count > CASCADE_LIMIT)
1461
- throw new Error(
1462
- `Cascade did not stabilise after ${CASCADE_LIMIT} auto-transitions on ${instanceId} \u2014 possible infinite loop`
1463
- );
1948
+ if (count++, count >= CASCADE_LIMIT)
1949
+ throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
1464
1950
  }
1465
1951
  }
1466
- async function propagateToAncestors(client, instanceId, actor, clientForGdr) {
1467
- const instance = await client.getDocument(instanceId);
1468
- if (!instance || instance.ancestors.length === 0) return;
1469
- const parentRef = instance.ancestors[instance.ancestors.length - 1];
1952
+ async function findSatisfiedParentSpawn(client, child) {
1953
+ const parentRef = child.ancestors.at(-1);
1470
1954
  if (parentRef === void 0) return;
1471
- const parentId = gdrToBareId(parentRef.id), parent = await client.getDocument(parentId);
1472
- if (!parent || parent._type !== "workflow.instance" || typeof parent.definitionSnapshot != "string")
1473
- return;
1474
- const parentDefinition = JSON.parse(parent.definitionSnapshot), parentStage = parentDefinition.stages.find((s) => s.id === parent.currentStageId);
1475
- if (parentStage === void 0) return;
1476
- 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);
1477
- if (parentTask === void 0) return;
1478
- const parentEntry = parentCurrentTasks.find((e) => e.id === parentTask.id);
1479
- if (parentEntry === void 0 || parentEntry.status === "done" || !await checkSpawnCompletion(client, parentEntry, parentTask.spawns)) return;
1480
- const ctx = await buildEngineContext({
1955
+ const parent = await client.getDocument(gdrToBareId(parentRef.id));
1956
+ if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
1957
+ const definition = JSON.parse(parent.definitionSnapshot), stage = definition.stages.find((s) => s.id === parent.currentStageId);
1958
+ if (stage === void 0) return;
1959
+ const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.spawns === void 0 ? !1 : parentCurrentTasks.find((e) => e.id === t.id)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
1960
+ if (task?.spawns === void 0) return;
1961
+ const entry = parentCurrentTasks.find((e) => e.id === task.id);
1962
+ if (!(entry === void 0 || entry.status === "done") && await checkSpawnCompletion(client, entry, task.spawns))
1963
+ return { parent, definition, stage, task };
1964
+ }
1965
+ async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
1966
+ const instance = await client.getDocument(instanceId);
1967
+ if (!instance) return;
1968
+ const found = await findSatisfiedParentSpawn(client, instance);
1969
+ if (found === void 0) return;
1970
+ const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
1481
1971
  client,
1482
1972
  clientForGdr: clientForGdr ?? (() => client),
1483
1973
  instance: parent,
1484
- definition: parentDefinition
1485
- }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === parentTask.id);
1974
+ definition,
1975
+ ...clock ? { clock } : {}
1976
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === task.id);
1486
1977
  if (mutEntry === void 0) return;
1487
- const previousStatus = mutEntry.status;
1488
- mutEntry.status = "done", mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1978
+ const previousStatus = mutEntry.status, now = ctx.now;
1979
+ mutEntry.status = "done", mutEntry.completedAt = now, mutation.history.push(
1489
1980
  taskStatusChangedEntry({
1490
- stageId: parentStage.id,
1491
- taskId: parentTask.id,
1981
+ stageId: stage.id,
1982
+ taskId: task.id,
1492
1983
  from: previousStatus,
1493
1984
  to: "done",
1985
+ at: now,
1494
1986
  ...actor !== void 0 ? { actor } : {}
1495
1987
  })
1496
- ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parentId, actor, clientForGdr), await propagateToAncestors(client, parentId, actor, clientForGdr);
1497
- }
1498
- function startMutation(instance) {
1499
- return {
1500
- currentStageId: instance.currentStageId,
1501
- // Deep-ish copy. Per-scope state slot arrays need their own copies
1502
- // so ops can mutate without leaking into the source.
1503
- state: (instance.state ?? []).map((s) => ({ ...s })),
1504
- stages: instance.stages.map((s) => ({
1505
- ...s,
1506
- state: (s.state ?? []).map((slot) => ({ ...slot })),
1507
- tasks: s.tasks.map((t) => ({
1508
- ...t,
1509
- ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1510
- }))
1511
- })),
1512
- pendingEffects: [...instance.pendingEffects],
1513
- effectHistory: [...instance.effectHistory],
1514
- effectsContext: [...instance.effectsContext],
1515
- history: [...instance.history],
1516
- lastChangedAt: instance.lastChangedAt,
1517
- ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1518
- pendingCreates: []
1519
- };
1520
- }
1521
- function taskStatusChangedEntry(args) {
1522
- return {
1523
- _key: randomKey(),
1524
- _type: "workflow.history.taskStatusChanged",
1525
- at: args.at ?? (/* @__PURE__ */ new Date()).toISOString(),
1526
- stageId: args.stageId,
1527
- taskId: args.taskId,
1528
- from: args.from,
1529
- to: args.to,
1530
- ...args.actor !== void 0 ? { actor: args.actor } : {}
1531
- };
1988
+ ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock);
1532
1989
  }
1533
- function stageTransitionHistory(args) {
1534
- const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1535
- at,
1536
- ...transitionId !== void 0 ? { transitionId } : {},
1537
- ...actor !== void 0 ? { actor } : {},
1538
- ...via !== void 0 ? { via } : {},
1539
- ...reason !== void 0 ? { reason } : {}
1540
- };
1541
- return [
1542
- {
1543
- _key: randomKey(),
1544
- _type: "workflow.history.stageExited",
1545
- stageId: fromStageId,
1546
- toStageId,
1547
- ...shared
1548
- },
1549
- {
1550
- _key: randomKey(),
1551
- _type: "workflow.history.transitionFired",
1552
- fromStageId,
1553
- toStageId,
1554
- ...shared
1555
- },
1556
- {
1557
- _key: randomKey(),
1558
- _type: "workflow.history.stageEntered",
1559
- stageId: toStageId,
1560
- fromStageId,
1561
- ...shared
1562
- }
1563
- ];
1990
+ const parsedFilters = /* @__PURE__ */ new Map();
1991
+ async function matchesFilter(args) {
1992
+ const { document, filter, userId } = args;
1993
+ parsedFilters.has(filter) || parsedFilters.set(filter, groqJs.parse(`*[${filter}]`));
1994
+ const ast = parsedFilters.get(filter), data = await (await groqJs.evaluate(ast, {
1995
+ dataset: [document],
1996
+ ...userId !== void 0 ? { identity: userId } : {}
1997
+ })).get();
1998
+ return Array.isArray(data) && data.length === 1;
1564
1999
  }
1565
- async function persist(ctx, mutation) {
1566
- const set = {
1567
- currentStageId: mutation.currentStageId,
1568
- state: mutation.state,
1569
- stages: mutation.stages,
1570
- pendingEffects: mutation.pendingEffects,
1571
- effectHistory: mutation.effectHistory,
1572
- effectsContext: mutation.effectsContext,
1573
- history: mutation.history,
1574
- lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1575
- };
1576
- mutation.completedAt !== void 0 && (set.completedAt = mutation.completedAt);
1577
- const pendingCreates = mutation.pendingCreates;
1578
- if (pendingCreates.length === 0)
1579
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1580
- const tx = ctx.client.transaction();
1581
- for (const body of pendingCreates) tx.create(body);
1582
- tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1583
- const actorForPriming = void 0;
1584
- for (const body of pendingCreates)
1585
- 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);
1586
- const reloaded = await ctx.client.getDocument(ctx.instance._id);
1587
- if (!reloaded)
1588
- throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1589
- return reloaded;
2000
+ async function grantsPermissionOn(args) {
2001
+ const { document, grants, permission, userId } = args;
2002
+ if (!document || grants.length === 0) return !1;
2003
+ for (const grant of grants)
2004
+ if (grant.permissions.includes(permission) && await matchesFilter({
2005
+ document,
2006
+ filter: grant.filter,
2007
+ ...userId !== void 0 ? { userId } : {}
2008
+ }))
2009
+ return !0;
2010
+ return !1;
1590
2011
  }
1591
- function findOpenStageEntry(host) {
1592
- return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
2012
+ async function fetchGrants(args) {
2013
+ const { client, resourcePath, signal } = args;
2014
+ return client.request({ url: resourcePath, ...signal ? { signal } : {} });
1593
2015
  }
1594
- function currentStageEntry(mutation) {
1595
- const entry = findOpenStageEntry(mutation);
1596
- if (entry === void 0)
2016
+ const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
2017
+ async function resolveAccess(client, args = {}) {
2018
+ const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2019
+ if (overrideActor !== void 0 && overrideGrants !== void 0)
2020
+ return { actor: overrideActor, grants: overrideGrants };
2021
+ const requestFn = client.request === void 0 ? void 0 : (opts) => client.request(opts);
2022
+ if (requestFn === void 0) {
2023
+ if (overrideActor === void 0)
2024
+ throw new Error(
2025
+ "workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
2026
+ );
2027
+ return { actor: overrideActor };
2028
+ }
2029
+ const actorPromise = overrideActor !== void 0 ? Promise.resolve(overrideActor) : cachedActor(client, requestFn), grantsPromise = resolveGrants(overrideGrants, args.grantsFromPath, client, requestFn), [actor, grants] = await Promise.all([actorPromise, grantsPromise]);
2030
+ if (actor === void 0)
1597
2031
  throw new Error(
1598
- `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
2032
+ "workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity \u2014 check the token, or pass `access.actor` explicitly."
1599
2033
  );
1600
- return entry;
2034
+ return { actor, ...grants !== void 0 ? { grants } : {} };
1601
2035
  }
1602
- function currentTasks(mutation) {
1603
- return currentStageEntry(mutation).tasks;
2036
+ function cachedActor(client, requestFn) {
2037
+ const cached = actorCache.get(client);
2038
+ if (cached !== void 0) return cached;
2039
+ const pending = fetchActor(requestFn).catch((err) => {
2040
+ throw actorCache.get(client) === pending && actorCache.delete(client), err;
2041
+ });
2042
+ return actorCache.set(client, pending), pending;
1604
2043
  }
1605
- function findTaskInCurrentStage(ctx, taskId) {
1606
- const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1607
- if (task === void 0)
1608
- throw new Error(
1609
- `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1610
- );
1611
- return { stage, task };
1612
- }
1613
- function requireMutationTaskEntry(mutation, taskId) {
1614
- const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1615
- if (mutEntry === void 0)
1616
- throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1617
- return mutEntry;
1618
- }
1619
- function findCurrentStageEntry(instance) {
1620
- return findOpenStageEntry(instance);
1621
- }
1622
- function findCurrentTasks(instance) {
1623
- return findCurrentStageEntry(instance)?.tasks ?? [];
2044
+ function resolveGrants(overrideGrants, grantsFromPath, client, requestFn) {
2045
+ return overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : grantsFromPath !== void 0 ? cachedGrants(client, requestFn, grantsFromPath) : Promise.resolve(void 0);
1624
2046
  }
1625
- async function completeEffect(args) {
1626
- const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId);
1627
- return commitCompleteEffect(
1628
- ctx,
1629
- effectKey,
1630
- status,
1631
- outputs,
1632
- detail,
1633
- error,
1634
- durationMs,
1635
- options?.actor
1636
- );
2047
+ function cachedGrants(client, requestFn, resourcePath) {
2048
+ let byPath = grantsCache.get(client);
2049
+ byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
2050
+ let cached = byPath.get(resourcePath);
2051
+ return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
1637
2052
  }
1638
- function buildEffectHistoryEntry(pending, outcome) {
1639
- const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome;
2053
+ async function fetchActor(requestFn) {
2054
+ let user;
2055
+ try {
2056
+ user = await requestFn({
2057
+ uri: "/users/me",
2058
+ tag: "workflow.access.resolve-actor"
2059
+ });
2060
+ } catch (err) {
2061
+ throw new Error(
2062
+ 'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
2063
+ { cause: err }
2064
+ );
2065
+ }
2066
+ if (!user || typeof user.id != "string" || user.id.length === 0) return;
2067
+ const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
1640
2068
  return {
1641
- _key: pending._key,
1642
- effectId: pending.effectId,
1643
- ...pending.title !== void 0 ? { title: pending.title } : {},
1644
- ...pending.description !== void 0 ? { description: pending.description } : {},
1645
- params: pending.params,
1646
- origin: pending.origin,
1647
- ...actor !== void 0 ? { actor } : pending.actor !== void 0 ? { actor: pending.actor } : {},
1648
- ranAt,
1649
- ...durationMs !== void 0 ? { durationMs } : {},
1650
- status,
1651
- ...detail !== void 0 ? { detail } : {},
1652
- ...error !== void 0 ? { error } : {},
1653
- ...outputs !== void 0 ? { outputs } : {}
2069
+ kind: "user",
2070
+ id: user.id,
2071
+ ...roleNames.length > 0 ? { roles: roleNames } : {}
1654
2072
  };
1655
2073
  }
1656
- function upsertEffectsContext(mutation, outputs) {
1657
- for (const [id, value] of Object.entries(outputs)) {
1658
- const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1659
- existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
2074
+ async function fetchGrantsCached(requestFn, resourcePath) {
2075
+ try {
2076
+ return await fetchGrants({ client: { request: requestFn }, resourcePath });
2077
+ } catch (err) {
2078
+ console.warn(
2079
+ `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2080
+ );
2081
+ return;
1660
2082
  }
1661
2083
  }
1662
- async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1663
- const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1664
- if (pending === void 0)
1665
- throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1666
- const mutation = startMutation(ctx.instance);
1667
- mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1668
- const ranAt = (/* @__PURE__ */ new Date()).toISOString();
1669
- return mutation.effectHistory.push(
1670
- buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1671
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1672
- _key: randomKey(),
1673
- _type: "workflow.history.effectCompleted",
1674
- at: ranAt,
1675
- effectKey,
1676
- effectId: pending.effectId,
1677
- status,
1678
- ...outputs !== void 0 ? { outputs } : {},
1679
- ...detail !== void 0 ? { detail } : {},
1680
- ...actor !== void 0 ? { actor } : {}
1681
- }), await persist(ctx, mutation), { effectKey, status };
2084
+ const WILDCARD_ROLE = "*";
2085
+ async function evaluateInstance(args) {
2086
+ const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2087
+ validateTags(tags);
2088
+ const { actor, grants } = await resolveAccess(client, {
2089
+ ...args.access !== void 0 ? { override: args.access } : {},
2090
+ ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2091
+ }), instance = await client.getDocument(instanceId);
2092
+ if (!instance)
2093
+ throw new Error(`Workflow instance "${instanceId}" not found`);
2094
+ if (!tags.some((t) => instance.tags?.includes(t)))
2095
+ throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2096
+ const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance });
2097
+ return evaluateFromSnapshot({
2098
+ instance,
2099
+ definition,
2100
+ actor,
2101
+ snapshot,
2102
+ now,
2103
+ ...grants !== void 0 ? { grants } : {}
2104
+ });
1682
2105
  }
1683
- function buildQueuedEffect(effect, origin, params, actor) {
1684
- const key = randomKey(), pending = {
1685
- _key: key,
1686
- _type: "workflow.pendingEffect",
1687
- effectId: effect.id,
1688
- ...effect.title !== void 0 ? { title: effect.title } : {},
1689
- ...effect.description !== void 0 ? { description: effect.description } : {},
1690
- ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1691
- params,
1692
- origin,
1693
- ...actor !== void 0 ? { actor } : {},
1694
- queuedAt: (/* @__PURE__ */ new Date()).toISOString()
1695
- }, history = {
1696
- _key: randomKey(),
1697
- _type: "workflow.history.effectQueued",
1698
- at: (/* @__PURE__ */ new Date()).toISOString(),
1699
- effectKey: key,
1700
- effectId: effect.id,
1701
- origin
2106
+ async function evaluateFromSnapshot(args) {
2107
+ const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2108
+ if (stage === void 0)
2109
+ throw new Error(
2110
+ `Instance "${instance._id}" currentStageId "${instance.currentStageId}" not in definition`
2111
+ );
2112
+ const currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], taskEvaluations = [];
2113
+ for (const task of stage.tasks ?? [])
2114
+ taskEvaluations.push(
2115
+ await evaluateTask({
2116
+ task,
2117
+ statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2118
+ instance,
2119
+ definition,
2120
+ actor,
2121
+ grants,
2122
+ snapshot,
2123
+ now
2124
+ })
2125
+ );
2126
+ const transitionEvaluations = [];
2127
+ for (const transition of stage.transitions ?? [])
2128
+ transitionEvaluations.push({
2129
+ transition,
2130
+ filterSatisfied: await evaluateFilter({
2131
+ filter: transition.filter,
2132
+ definition,
2133
+ snapshot,
2134
+ params: buildParams(instance, now)
2135
+ })
2136
+ });
2137
+ const currentStage = {
2138
+ stage,
2139
+ tasks: taskEvaluations,
2140
+ transitions: transitionEvaluations
2141
+ }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2142
+ return {
2143
+ instance,
2144
+ definition,
2145
+ actor,
2146
+ currentStage,
2147
+ pendingOnYou,
2148
+ canInteract
1702
2149
  };
1703
- return { pending, history };
1704
2150
  }
1705
- async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1706
- if (!effects) return;
1707
- const liveInstance = {
1708
- ...ctx.instance,
1709
- state: mutation.state,
1710
- stages: mutation.stages,
1711
- effectsContext: mutation.effectsContext
2151
+ async function evaluateTask(args) {
2152
+ const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
2153
+ for (const action of task.actions ?? [])
2154
+ actions.push(
2155
+ await evaluateAction({
2156
+ action,
2157
+ task,
2158
+ ...statusEntry !== void 0 ? { statusEntry } : {},
2159
+ status,
2160
+ instance,
2161
+ definition,
2162
+ actor,
2163
+ ...grants !== void 0 ? { grants } : {},
2164
+ snapshot,
2165
+ now
2166
+ })
2167
+ );
2168
+ return {
2169
+ task,
2170
+ status,
2171
+ // "pending on actor" treats both `pending` and `active` as waiting on
2172
+ // the assignee — pending tasks just haven't been auto-invoked yet, but
2173
+ // they're still inbox items.
2174
+ pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2175
+ actions
1712
2176
  };
1713
- for (const effect of effects) {
1714
- const params = await resolveBindings({
1715
- bindings: effect.bindings,
1716
- staticInput: effect.input,
1717
- instance: liveInstance,
1718
- ...callerParams !== void 0 ? { params: callerParams } : {},
1719
- ...actor !== void 0 ? { actor } : {}
1720
- }), { pending, history } = buildQueuedEffect(effect, origin, params, actor);
1721
- mutation.pendingEffects.push(pending), mutation.history.push(history);
1722
- }
1723
2177
  }
1724
- class ActionParamsInvalidError extends Error {
1725
- action;
1726
- taskId;
1727
- issues;
1728
- constructor(args) {
1729
- const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1730
- `);
1731
- super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
1732
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
1733
- }
2178
+ async function evaluateAction(args) {
2179
+ const { action, task, status, instance, definition, actor, grants, snapshot, now } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2180
+ if (syncReason !== void 0) return disabled(action, syncReason);
2181
+ const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot, now);
2182
+ return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
1734
2183
  }
1735
- function validateActionParams(action, taskId, callerParams) {
1736
- const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
1737
- for (const decl of declared) {
1738
- const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
1739
- if (decl.required === !0 && !present) {
1740
- issues.push({ paramId: decl.id, reason: "required but missing" });
1741
- continue;
1742
- }
1743
- present && (checkParamType(value, decl) || issues.push({
1744
- paramId: decl.id,
1745
- reason: `expected paramType=${decl.paramType}, got ${typeof value}`
1746
- }));
1747
- }
1748
- if (issues.length > 0)
1749
- throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
1750
- return params;
2184
+ function lifecycleReason(instance, definition, status) {
2185
+ if (instance.completedAt !== void 0)
2186
+ return { kind: "instance-completed", completedAt: instance.completedAt };
2187
+ if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2188
+ return { kind: "stage-terminal", stageId: instance.currentStageId };
2189
+ if (status === "done" || status === "skipped" || status === "failed")
2190
+ return { kind: "task-not-active", status };
1751
2191
  }
1752
- function hasStringField(value, field) {
1753
- return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
2192
+ function actorReason(action, task, actor) {
2193
+ const actorRoles = actor.roles ?? [];
2194
+ if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2195
+ return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2196
+ if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2197
+ return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
1754
2198
  }
1755
- function checkParamType(value, decl) {
1756
- switch (decl.paramType) {
1757
- case "string":
1758
- case "url":
1759
- case "dateTime":
1760
- return typeof value == "string";
1761
- case "number":
1762
- return typeof value == "number" && Number.isFinite(value);
1763
- case "boolean":
2199
+ async function permissionReason(action, instance, actor, grants) {
2200
+ if (grants === void 0) return;
2201
+ const permission = action.requiredPermission ?? "update";
2202
+ if (!await grantsPermissionOn({
2203
+ document: instance,
2204
+ grants,
2205
+ permission,
2206
+ userId: actor.id
2207
+ }))
2208
+ return { kind: "permission-denied", permission, matchingGrants: grants.length };
2209
+ }
2210
+ async function filterReason(action, instance, definition, snapshot, now) {
2211
+ if (!(action.filter === void 0 || await evaluateFilter({
2212
+ filter: action.filter,
2213
+ definition,
2214
+ snapshot,
2215
+ params: buildParams(instance, now)
2216
+ })))
2217
+ return { kind: "filter-failed", filter: action.filter };
2218
+ }
2219
+ function disabled(action, reason) {
2220
+ return { action, allowed: !1, disabledReason: reason };
2221
+ }
2222
+ function actorIsAssignee(actor, assignees) {
2223
+ if (assignees.length === 0) return !1;
2224
+ const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2225
+ for (const assignee of assignees)
2226
+ if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
2227
+ return !0;
2228
+ return !1;
2229
+ }
2230
+ function parseDefinitionSnapshot(instance) {
2231
+ try {
2232
+ return JSON.parse(instance.definitionSnapshot);
2233
+ } catch (err) {
2234
+ throw new Error(
2235
+ `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2236
+ { cause: err }
2237
+ );
2238
+ }
2239
+ }
2240
+ const resourceKey = (r) => `${r.type}.${r.id}`;
2241
+ function guardsForResource(client) {
2242
+ return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2243
+ }
2244
+ async function guardsForInstance(args) {
2245
+ const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
2246
+ [resourceKey(instance.workflowResource), client]
2247
+ ]), stateUris = [
2248
+ ...collectSlotDocUris(instance.state),
2249
+ ...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
2250
+ ];
2251
+ for (const uri of stateUris) {
2252
+ const parsed = tryParseGdr(uri);
2253
+ if (parsed === void 0) continue;
2254
+ const key = resourceKey(resourceFromParsed(parsed));
2255
+ clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
2256
+ }
2257
+ const perResource = await Promise.all(
2258
+ [...clientsByResource.values()].map(
2259
+ (c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
2260
+ t: GUARD_DOC_TYPE,
2261
+ id: instance._id
2262
+ })
2263
+ )
2264
+ );
2265
+ return dedupById(perResource.flat());
2266
+ }
2267
+ function tryParseGdr(uri) {
2268
+ try {
2269
+ return parseGdr(uri);
2270
+ } catch {
2271
+ return;
2272
+ }
2273
+ }
2274
+ function dedupById(guards) {
2275
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2276
+ }
2277
+ function definitionDocId(_workflowResource, prefix, workflowId, version) {
2278
+ return `${prefix}.${workflowId}.v${version}`;
2279
+ }
2280
+ async function sortByDependencies(client, definitions, tags, workflowResource) {
2281
+ const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2282
+ for (const def of definitions) {
2283
+ const list = byWorkflowId.get(def.workflowId) ?? [];
2284
+ list.push(def), byWorkflowId.set(def.workflowId, list);
2285
+ }
2286
+ const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2287
+ return await assertCrossBatchRefsDeployed(client, definitions, {
2288
+ tags,
2289
+ workflowResource,
2290
+ prefix,
2291
+ findInBatch
2292
+ }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2293
+ }
2294
+ function findRefInBatch(byWorkflowId, ref) {
2295
+ const candidates = byWorkflowId.get(ref.workflowId);
2296
+ if (!(candidates === void 0 || candidates.length === 0))
2297
+ return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2298
+ (best, c) => best === void 0 || c.version > best.version ? c : best,
2299
+ void 0
2300
+ );
2301
+ }
2302
+ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2303
+ const missing = [];
2304
+ for (const def of definitions) {
2305
+ const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2306
+ for (const ref of refsOf(def)) {
2307
+ if (ctx.findInBatch(ref) !== void 0) continue;
2308
+ const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2309
+ label !== void 0 && missing.push({ from: fromId, ref: label });
2310
+ }
2311
+ }
2312
+ if (missing.length === 0) return;
2313
+ const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
2314
+ throw new Error(
2315
+ `workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
2316
+ ` + lines.join(`
2317
+ `)
2318
+ );
2319
+ }
2320
+ async function resolveDeployedRefLabel(client, ref, tags) {
2321
+ const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
2322
+ if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2323
+ definitionLookupGroq(wantsExplicit),
2324
+ params
2325
+ ))
2326
+ return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2327
+ }
2328
+ function topoSortDefinitions(definitions, ctx) {
2329
+ const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2330
+ const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2331
+ if (!visited.has(id)) {
2332
+ if (visiting.has(id))
2333
+ throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
2334
+ visiting.add(id);
2335
+ for (const ref of refsOf(def)) {
2336
+ const child = ctx.findInBatch(ref);
2337
+ child !== void 0 && visit(child);
2338
+ }
2339
+ visiting.delete(id), visited.add(id), ordered.push(def);
2340
+ }
2341
+ };
2342
+ for (const def of definitions) visit(def);
2343
+ return ordered;
2344
+ }
2345
+ function refsOf(def) {
2346
+ const out = [];
2347
+ for (const stage of def.stages)
2348
+ for (const task of stage.tasks ?? []) {
2349
+ const ref = task.spawns?.definitionRef;
2350
+ ref !== void 0 && out.push({
2351
+ workflowId: ref.workflowId,
2352
+ ...ref.version !== void 0 ? { version: ref.version } : {}
2353
+ });
2354
+ }
2355
+ return out;
2356
+ }
2357
+ function isDefinitionUnchanged(existing, expected) {
2358
+ return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
2359
+ }
2360
+ function stripSystemFields(doc) {
2361
+ const out = {};
2362
+ for (const [k, v2] of Object.entries(doc))
2363
+ k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v2);
2364
+ return out;
2365
+ }
2366
+ function stableStringify(value) {
2367
+ return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2368
+ ([a], [b]) => a.localeCompare(b)
2369
+ ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2370
+ }
2371
+ async function loadDefinition(client, workflowId, version, tags) {
2372
+ if (version !== void 0) {
2373
+ const doc = await client.fetch(
2374
+ definitionLookupGroq(!0),
2375
+ { workflowId, version, engineTags: tags }
2376
+ );
2377
+ if (!doc)
2378
+ throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2379
+ return doc;
2380
+ }
2381
+ const latest = await client.fetch(definitionLookupGroq(!1), {
2382
+ workflowId,
2383
+ engineTags: tags
2384
+ });
2385
+ if (!latest)
2386
+ throw new Error(`No deployed definition for workflow ${workflowId}`);
2387
+ return latest;
2388
+ }
2389
+ const STATE_OP_TYPES = [
2390
+ "workflow.op.state.set",
2391
+ "workflow.op.state.append",
2392
+ "workflow.op.state.updateWhere",
2393
+ "workflow.op.state.removeWhere"
2394
+ ];
2395
+ function isStateOp(summary) {
2396
+ return STATE_OP_TYPES.includes(summary.opType);
2397
+ }
2398
+ function validateActionParams(action, taskId, callerParams) {
2399
+ const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
2400
+ for (const decl of declared) {
2401
+ const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
2402
+ if (decl.required === !0 && !present) {
2403
+ issues.push({ paramId: decl.id, reason: "required but missing" });
2404
+ continue;
2405
+ }
2406
+ present && (checkParamType(value, decl) || issues.push({
2407
+ paramId: decl.id,
2408
+ reason: `expected paramType=${decl.paramType}, got ${typeof value}`
2409
+ }));
2410
+ }
2411
+ if (issues.length > 0)
2412
+ throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
2413
+ return params;
2414
+ }
2415
+ function hasStringField(value, field) {
2416
+ return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
2417
+ }
2418
+ function checkParamType(value, decl) {
2419
+ switch (decl.paramType) {
2420
+ case "string":
2421
+ case "url":
2422
+ case "dateTime":
2423
+ return typeof value == "string";
2424
+ case "number":
2425
+ return typeof value == "number" && Number.isFinite(value);
2426
+ case "boolean":
1764
2427
  return typeof value == "boolean";
1765
2428
  case "actor":
1766
2429
  return hasStringField(value, "kind");
1767
2430
  case "doc.ref":
1768
2431
  return hasStringField(value, "id");
1769
2432
  case "doc.refs":
1770
- return Array.isArray(value) && value.every((v) => checkParamType(v, { paramType: "doc.ref" }));
2433
+ return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
1771
2434
  case "json":
1772
2435
  return !0;
1773
2436
  default:
@@ -1775,15 +2438,15 @@ function checkParamType(value, decl) {
1775
2438
  }
1776
2439
  }
1777
2440
  async function runActionOps(args) {
1778
- const { ops, mutation, stageId, taskId, actionName, params, actor } = args;
2441
+ const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
1779
2442
  if (ops === void 0 || ops.length === 0) return [];
1780
2443
  const summaries = [];
1781
2444
  for (const op of ops) {
1782
- const summary = applyOp(op, { mutation, stageId, taskId, params, actor });
2445
+ const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
1783
2446
  summaries.push(summary), mutation.history.push({
1784
2447
  _key: randomKey(),
1785
2448
  _type: "workflow.history.opApplied",
1786
- at: (/* @__PURE__ */ new Date()).toISOString(),
2449
+ at: now,
1787
2450
  stageId,
1788
2451
  taskId,
1789
2452
  actionId: actionName,
@@ -1842,8 +2505,8 @@ function applyStateUpdateWhere(op, ctx) {
1842
2505
  /*createIfMissing*/
1843
2506
  !1
1844
2507
  ), resolvedMerge = {};
1845
- for (const [k, v] of Object.entries(op.merge))
1846
- resolvedMerge[k] = resolveOpSource(v, ctx);
2508
+ for (const [k, v2] of Object.entries(op.merge))
2509
+ resolvedMerge[k] = resolveOpSource(v2, ctx);
1847
2510
  return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
1848
2511
  (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
1849
2512
  )), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
@@ -1886,18 +2549,11 @@ function locateSlot(ctx, target, createIfMissing) {
1886
2549
  }, host.push(slot)), slot;
1887
2550
  }
1888
2551
  function resolveOpSource(src, ctx) {
2552
+ const staticValue = resolveStaticSource(src, ctx);
2553
+ if (staticValue.handled) return staticValue.value;
1889
2554
  switch (src.source) {
1890
- case "literal":
1891
- return src.value;
1892
- case "param":
1893
- return ctx.params[src.paramId];
1894
- case "actor":
1895
- return ctx.actor;
1896
- case "now":
1897
- return (/* @__PURE__ */ new Date()).toISOString();
1898
2555
  case "self":
1899
- return;
1900
- // Not applicable inside ops; reserved for higher-level binding.
2556
+ return ctx.self;
1901
2557
  case "stageId":
1902
2558
  return ctx.stageId;
1903
2559
  case "stateRead": {
@@ -1907,8 +2563,8 @@ function resolveOpSource(src, ctx) {
1907
2563
  case "effectOutput": {
1908
2564
  const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
1909
2565
  if (entry === void 0) return null;
1910
- const v = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
1911
- return v === void 0 ? null : v;
2566
+ const v2 = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
2567
+ return v2 === void 0 ? null : v2;
1912
2568
  }
1913
2569
  case "object": {
1914
2570
  const out = {};
@@ -1916,6 +2572,11 @@ function resolveOpSource(src, ctx) {
1916
2572
  out[field] = resolveOpSource(fieldSrc, ctx);
1917
2573
  return out;
1918
2574
  }
2575
+ case "row":
2576
+ case "parentState":
2577
+ return;
2578
+ default:
2579
+ return;
1919
2580
  }
1920
2581
  }
1921
2582
  function slotValue(slots, slotId) {
@@ -1934,333 +2595,101 @@ function evalOpPredicate(p, row, ctx) {
1934
2595
  const target = resolveOpSource(p.equals, ctx);
1935
2596
  return row[p.field] === target;
1936
2597
  }
1937
- async function fireAction(args) {
1938
- const { client, instanceId, taskId, action, params, options } = args, ctx = await loadContext(client, instanceId);
1939
- return commitAction(ctx, taskId, action, params, options?.actor);
1940
- }
1941
- async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
1942
- const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
1943
- if (action === void 0)
1944
- throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
1945
- if (!await ctxEvaluateFilter(ctx, action.filter))
1946
- throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
1947
- const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
1948
- if (entry === void 0 || entry.status !== "active")
1949
- throw new Error(
1950
- `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
1951
- );
1952
- const params = validateActionParams(action, taskId, callerParams);
1953
- return { stage, task, action, params };
1954
- }
1955
- function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor) {
1956
- if (action.setStatus === void 0) return;
1957
- const initialStatus = mutEntry.status, newStatus = action.setStatus;
1958
- return mutEntry.status = newStatus, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), actor && (mutEntry.completedBy = actor.id), mutation.history.push(
1959
- taskStatusChangedEntry({
1960
- stageId,
1961
- taskId,
1962
- from: initialStatus,
1963
- to: newStatus,
1964
- ...actor !== void 0 ? { actor } : {}
1965
- })
1966
- ), newStatus;
1967
- }
1968
- async function commitAction(ctx, taskId, actionName, callerParams, actor) {
1969
- const { stage, task, action, params } = await resolveActionCommit(
1970
- ctx,
1971
- taskId,
1972
- actionName,
1973
- callerParams
1974
- ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1975
- mutation.history.push({
1976
- _key: randomKey(),
1977
- _type: "workflow.history.actionFired",
1978
- at: (/* @__PURE__ */ new Date()).toISOString(),
1979
- stageId: stage.id,
1980
- taskId,
1981
- actionId: actionName,
1982
- ...actor !== void 0 ? { actor } : {}
1983
- });
1984
- const ranOps = await runActionOps({
1985
- ops: action.ops,
1986
- mutation,
1987
- stageId: stage.id,
1988
- taskId,
1989
- actionName,
1990
- params,
1991
- actor
1992
- }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor);
1993
- return await queueEffects(
1994
- ctx,
1995
- mutation,
1996
- action.effects,
1997
- {
1998
- kind: "action",
1999
- id: `${task.id}:${actionName}`
2000
- },
2001
- actor,
2002
- params
2003
- ), await persist(ctx, mutation), {
2004
- fired: !0,
2005
- taskId,
2006
- action: actionName,
2007
- ...newStatus !== void 0 ? { newStatus } : {},
2008
- ...ranOps.length > 0 ? { ranOps } : {}
2009
- };
2010
- }
2011
- const parsedFilters = /* @__PURE__ */ new Map();
2012
- async function matchesFilter(args) {
2013
- const { document, filter, userId } = args;
2014
- parsedFilters.has(filter) || parsedFilters.set(filter, groqJs.parse(`*[${filter}]`));
2015
- const ast = parsedFilters.get(filter), data = await (await groqJs.evaluate(ast, {
2016
- dataset: [document],
2017
- ...userId !== void 0 ? { identity: userId } : {}
2018
- })).get();
2019
- return Array.isArray(data) && data.length === 1;
2020
- }
2021
- async function grantsPermissionOn(args) {
2022
- const { document, grants, permission, userId } = args;
2023
- if (!document || grants.length === 0) return !1;
2024
- for (const grant of grants)
2025
- if (grant.permissions.includes(permission) && await matchesFilter({
2026
- document,
2027
- filter: grant.filter,
2028
- ...userId !== void 0 ? { userId } : {}
2029
- }))
2030
- return !0;
2031
- return !1;
2032
- }
2033
- async function fetchGrants(args) {
2034
- const { client, resourcePath, signal } = args;
2035
- return client.request({ url: resourcePath, ...signal ? { signal } : {} });
2036
- }
2037
- const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
2038
- async function resolveAccess(client, args = {}) {
2039
- const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2040
- if (overrideActor !== void 0 && overrideGrants !== void 0)
2041
- return { actor: overrideActor, grants: overrideGrants };
2042
- const requestFn = client.request;
2043
- if (requestFn === void 0) {
2044
- if (overrideActor === void 0)
2045
- throw new Error(
2046
- "workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
2047
- );
2048
- return { actor: overrideActor };
2049
- }
2050
- const actorPromise = overrideActor !== void 0 ? Promise.resolve(overrideActor) : cachedActor(client, requestFn), grantsPromise = overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : args.grantsFromPath !== void 0 ? cachedGrants(client, requestFn, args.grantsFromPath) : Promise.resolve(void 0), [actor, grants] = await Promise.all([actorPromise, grantsPromise]);
2051
- if (actor === void 0)
2052
- throw new Error(
2053
- "workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity \u2014 check the token, or pass `access.actor` explicitly."
2054
- );
2055
- return { actor, ...grants !== void 0 ? { grants } : {} };
2056
- }
2057
- function cachedActor(client, requestFn) {
2058
- const cached = actorCache.get(client);
2059
- if (cached !== void 0) return cached;
2060
- const pending = fetchActor(requestFn).catch((err) => {
2061
- throw actorCache.get(client) === pending && actorCache.delete(client), err;
2062
- });
2063
- return actorCache.set(client, pending), pending;
2064
- }
2065
- function cachedGrants(client, requestFn, resourcePath) {
2066
- let byPath = grantsCache.get(client);
2067
- byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
2068
- let cached = byPath.get(resourcePath);
2069
- return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
2070
- }
2071
- async function fetchActor(requestFn) {
2072
- let user;
2073
- try {
2074
- user = await requestFn({
2075
- uri: "/users/me",
2076
- tag: "workflow.access.resolve-actor"
2077
- });
2078
- } catch (err) {
2079
- throw new Error(
2080
- 'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
2081
- { cause: err }
2082
- );
2083
- }
2084
- if (!user || typeof user.id != "string" || user.id.length === 0) return;
2085
- const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
2086
- return {
2087
- kind: "user",
2088
- id: user.id,
2089
- ...roleNames.length > 0 ? { roles: roleNames } : {}
2090
- };
2091
- }
2092
- async function fetchGrantsCached(requestFn, resourcePath) {
2093
- try {
2094
- return await fetchGrants({ client: { request: requestFn }, resourcePath });
2095
- } catch (err) {
2096
- console.warn(
2097
- `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2098
- );
2099
- return;
2100
- }
2101
- }
2102
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
2103
- function validateTags(tags) {
2104
- if (tags.length === 0)
2105
- throw new Error("tags: must be a non-empty array");
2106
- for (const tag of tags)
2107
- if (!TAG_RE.test(tag))
2108
- throw new Error(
2109
- `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
2110
- );
2111
- }
2112
- function canonicalTag(tags) {
2113
- return tags[0];
2114
- }
2115
- const WILDCARD_ROLE = "*";
2116
- async function evaluateInstance(args) {
2117
- const { client, tags, instanceId, resourceClients } = args;
2118
- validateTags(tags);
2119
- const { actor, grants } = await resolveAccess(client, {
2120
- ...args.access !== void 0 ? { override: args.access } : {},
2121
- ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2122
- }), instance = await client.getDocument(instanceId);
2123
- if (!instance)
2124
- throw new Error(`Workflow instance "${instanceId}" not found`);
2125
- if (!tags.some((t) => instance.tags?.includes(t)))
2126
- throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2127
- const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2128
- if (stage === void 0)
2129
- throw new Error(
2130
- `Instance "${instanceId}" currentStageId "${instance.currentStageId}" not in definition`
2131
- );
2132
- const snapshot = await hydrateSnapshot({
2133
- client,
2134
- clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client,
2135
- instance,
2136
- definition
2137
- }), currentTaskEntries = instance.stages.find(
2138
- (s) => s.id === instance.currentStageId && s.exitedAt === void 0
2139
- )?.tasks ?? [], taskEvaluations = [];
2140
- for (const task of stage.tasks ?? [])
2141
- taskEvaluations.push(
2142
- await evaluateTask({
2143
- task,
2144
- statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2145
- instance,
2146
- definition,
2147
- actor,
2148
- grants,
2149
- snapshot
2150
- })
2151
- );
2152
- const transitionEvaluations = [];
2153
- for (const transition of stage.transitions ?? [])
2154
- transitionEvaluations.push({
2155
- transition,
2156
- filterSatisfied: await evaluateFilter({
2157
- filter: transition.filter,
2158
- definition,
2159
- snapshot,
2160
- params: buildParams(instance)
2161
- })
2162
- });
2163
- const currentStage = {
2164
- stage,
2165
- tasks: taskEvaluations,
2166
- transitions: transitionEvaluations
2167
- }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2168
- return {
2169
- instance,
2170
- definition,
2171
- actor,
2172
- currentStage,
2173
- pendingOnYou,
2174
- canInteract
2175
- };
2176
- }
2177
- async function evaluateTask(args) {
2178
- const { task, statusEntry, instance, definition, actor, grants, snapshot } = args, status = statusEntry?.status ?? "pending", actions = [];
2179
- for (const action of task.actions ?? [])
2180
- actions.push(
2181
- await evaluateAction({
2182
- action,
2183
- task,
2184
- ...statusEntry !== void 0 ? { statusEntry } : {},
2185
- status,
2186
- instance,
2187
- definition,
2188
- actor,
2189
- ...grants !== void 0 ? { grants } : {},
2190
- snapshot
2191
- })
2192
- );
2193
- return {
2194
- task,
2195
- status,
2196
- // "pending on actor" treats both `pending` and `active` as waiting on
2197
- // the assignee — pending tasks just haven't been auto-invoked yet, but
2198
- // they're still inbox items.
2199
- pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2200
- actions
2201
- };
2202
- }
2203
- async function evaluateAction(args) {
2204
- const { action, task, status, instance, definition, actor, grants, snapshot } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2205
- if (syncReason !== void 0) return disabled(action, syncReason);
2206
- const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot);
2207
- return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
2208
- }
2209
- function lifecycleReason(instance, definition, status) {
2210
- if (instance.completedAt !== void 0)
2211
- return { kind: "instance-completed", completedAt: instance.completedAt };
2212
- if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2213
- return { kind: "stage-terminal", stageId: instance.currentStageId };
2214
- if (status === "done" || status === "skipped" || status === "failed")
2215
- return { kind: "task-not-active", status };
2216
- }
2217
- function actorReason(action, task, actor) {
2218
- const actorRoles = actor.roles ?? [];
2219
- if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2220
- return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2221
- if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2222
- return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
2223
- }
2224
- async function permissionReason(action, instance, actor, grants) {
2225
- if (grants === void 0) return;
2226
- const permission = action.requiredPermission ?? "update";
2227
- if (!await grantsPermissionOn({
2228
- document: instance,
2229
- grants,
2230
- permission,
2231
- userId: actor.id
2232
- }))
2233
- return { kind: "permission-denied", permission, matchingGrants: grants.length };
2234
- }
2235
- async function filterReason(action, instance, definition, snapshot) {
2236
- if (!(action.filter === void 0 || await evaluateFilter({
2237
- filter: action.filter,
2238
- definition,
2239
- snapshot,
2240
- params: buildParams(instance)
2241
- })))
2242
- return { kind: "filter-failed", filter: action.filter };
2243
- }
2244
- function disabled(action, reason) {
2245
- return { action, allowed: !1, disabledReason: reason };
2246
- }
2247
- function actorIsAssignee(actor, assignees) {
2248
- if (assignees.length === 0) return !1;
2249
- const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2250
- for (const assignee of assignees)
2251
- if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
2252
- return !0;
2253
- return !1;
2598
+ async function fireAction(args) {
2599
+ const { client, instanceId, taskId, action, params, options } = args;
2600
+ for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2601
+ const ctx = await loadContext(client, instanceId, {
2602
+ ...options?.clock ? { clock: options.clock } : {}
2603
+ });
2604
+ try {
2605
+ return await commitAction(ctx, taskId, action, params, options?.actor);
2606
+ } catch (error) {
2607
+ if (!isRevisionConflict(error)) throw error;
2608
+ }
2609
+ }
2610
+ throw new ConcurrentFireActionError({
2611
+ instanceId,
2612
+ taskId,
2613
+ action,
2614
+ attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2615
+ });
2254
2616
  }
2255
- function parseDefinitionSnapshot(instance) {
2256
- try {
2257
- return JSON.parse(instance.definitionSnapshot);
2258
- } catch (err) {
2617
+ async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
2618
+ const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
2619
+ if (action === void 0)
2620
+ throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
2621
+ if (!await ctxEvaluateFilter(ctx, action.filter))
2622
+ throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
2623
+ const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
2624
+ if (entry === void 0 || entry.status !== "active")
2259
2625
  throw new Error(
2260
- `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2261
- { cause: err }
2626
+ `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2262
2627
  );
2263
- }
2628
+ const params = validateActionParams(action, taskId, callerParams);
2629
+ return { stage, task, action, params };
2630
+ }
2631
+ function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor, now) {
2632
+ if (action.setStatus === void 0) return;
2633
+ const initialStatus = mutEntry.status, newStatus = action.setStatus;
2634
+ return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
2635
+ taskStatusChangedEntry({
2636
+ stageId,
2637
+ taskId,
2638
+ from: initialStatus,
2639
+ to: newStatus,
2640
+ at: now,
2641
+ ...actor !== void 0 ? { actor } : {}
2642
+ })
2643
+ ), newStatus;
2644
+ }
2645
+ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
2646
+ const { stage, task, action, params } = await resolveActionCommit(
2647
+ ctx,
2648
+ taskId,
2649
+ actionName,
2650
+ callerParams
2651
+ ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
2652
+ mutation.history.push({
2653
+ _key: randomKey(),
2654
+ _type: "workflow.history.actionFired",
2655
+ at: now,
2656
+ stageId: stage.id,
2657
+ taskId,
2658
+ actionId: actionName,
2659
+ ...actor !== void 0 ? { actor } : {}
2660
+ });
2661
+ const ranOps = await runActionOps({
2662
+ ops: action.ops,
2663
+ mutation,
2664
+ stageId: stage.id,
2665
+ taskId,
2666
+ actionName,
2667
+ params,
2668
+ actor,
2669
+ self: selfGdr(ctx.instance),
2670
+ now
2671
+ }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor, now);
2672
+ return await queueEffects(
2673
+ ctx,
2674
+ mutation,
2675
+ action.effects,
2676
+ {
2677
+ kind: "action",
2678
+ id: `${task.id}:${actionName}`
2679
+ },
2680
+ actor,
2681
+ params
2682
+ ), await persistThenDeploy(
2683
+ ctx,
2684
+ mutation,
2685
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
2686
+ ), {
2687
+ fired: !0,
2688
+ taskId,
2689
+ action: actionName,
2690
+ ...newStatus !== void 0 ? { newStatus } : {},
2691
+ ...ranOps.length > 0 ? { ranOps } : {}
2692
+ };
2264
2693
  }
2265
2694
  class ActionDisabledError extends Error {
2266
2695
  reason;
@@ -2284,121 +2713,6 @@ function formatDisabledReason(taskId, action, reason) {
2284
2713
  const detail = disabledReasonDetail[reason.kind](reason);
2285
2714
  return `Action "${taskId}:${action}" is not allowed: ${detail}`;
2286
2715
  }
2287
- function definitionDocId(_workflowResource, prefix, workflowId, version) {
2288
- return `${prefix}.${workflowId}.v${version}`;
2289
- }
2290
- async function sortByDependencies(client, definitions, tags, workflowResource) {
2291
- const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2292
- for (const def of definitions) {
2293
- const list = byWorkflowId.get(def.workflowId) ?? [];
2294
- list.push(def), byWorkflowId.set(def.workflowId, list);
2295
- }
2296
- const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2297
- return await assertCrossBatchRefsDeployed(client, definitions, {
2298
- tags,
2299
- workflowResource,
2300
- prefix,
2301
- findInBatch
2302
- }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2303
- }
2304
- function findRefInBatch(byWorkflowId, ref) {
2305
- const candidates = byWorkflowId.get(ref.workflowId);
2306
- if (!(candidates === void 0 || candidates.length === 0))
2307
- return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2308
- (best, c) => best === void 0 || c.version > best.version ? c : best,
2309
- void 0
2310
- );
2311
- }
2312
- async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2313
- const missing = [];
2314
- for (const def of definitions) {
2315
- const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2316
- for (const ref of refsOf(def)) {
2317
- if (ctx.findInBatch(ref) !== void 0) continue;
2318
- const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2319
- label !== void 0 && missing.push({ from: fromId, ref: label });
2320
- }
2321
- }
2322
- if (missing.length === 0) return;
2323
- const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
2324
- throw new Error(
2325
- `workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
2326
- ` + lines.join(`
2327
- `)
2328
- );
2329
- }
2330
- async function resolveDeployedRefLabel(client, ref, tags) {
2331
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
2332
- if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2333
- definitionLookupGroq(wantsExplicit),
2334
- params
2335
- ))
2336
- return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2337
- }
2338
- function topoSortDefinitions(definitions, ctx) {
2339
- const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2340
- const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2341
- if (!visited.has(id)) {
2342
- if (visiting.has(id))
2343
- throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
2344
- visiting.add(id);
2345
- for (const ref of refsOf(def)) {
2346
- const child = ctx.findInBatch(ref);
2347
- child !== void 0 && visit(child);
2348
- }
2349
- visiting.delete(id), visited.add(id), ordered.push(def);
2350
- }
2351
- };
2352
- for (const def of definitions) visit(def);
2353
- return ordered;
2354
- }
2355
- function definitionLookupGroq(wantsExplicit) {
2356
- return wantsExplicit ? '*[_type == "workflow.definition" && workflowId == $workflowId && version == $version && count(tags[@ in $tags]) > 0][0]' : '*[_type == "workflow.definition" && workflowId == $workflowId && count(tags[@ in $tags]) > 0] | order(version desc)[0]';
2357
- }
2358
- function refsOf(def) {
2359
- const out = [];
2360
- for (const stage of def.stages)
2361
- for (const task of stage.tasks ?? []) {
2362
- const ref = task.spawns?.definitionRef;
2363
- ref !== void 0 && out.push({
2364
- workflowId: ref.workflowId,
2365
- ...ref.version !== void 0 ? { version: ref.version } : {}
2366
- });
2367
- }
2368
- return out;
2369
- }
2370
- function isDefinitionUnchanged(existing, expected) {
2371
- return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
2372
- }
2373
- function stripSystemFields(doc) {
2374
- const out = {};
2375
- for (const [k, v] of Object.entries(doc))
2376
- k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v);
2377
- return out;
2378
- }
2379
- function stableStringify(value) {
2380
- return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2381
- ([a], [b]) => a.localeCompare(b)
2382
- ).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
2383
- }
2384
- async function loadDefinition(client, workflowId, version, tags) {
2385
- if (version !== void 0) {
2386
- const doc = await client.fetch(
2387
- "*[_type == 'workflow.definition' && workflowId == $id && version == $v && count(tags[@ in $engineTags]) > 0][0]",
2388
- { id: workflowId, v: version, engineTags: tags }
2389
- );
2390
- if (!doc)
2391
- throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2392
- return doc;
2393
- }
2394
- const latest = await client.fetch(
2395
- "*[_type == 'workflow.definition' && workflowId == $id && count(tags[@ in $engineTags]) > 0] | order(version desc)[0]",
2396
- { id: workflowId, engineTags: tags }
2397
- );
2398
- if (!latest)
2399
- throw new Error(`No deployed definition for workflow ${workflowId}`);
2400
- return latest;
2401
- }
2402
2716
  function bareIdFromSpawnRef(uri) {
2403
2717
  if (!uri.includes(":")) return [uri];
2404
2718
  try {
@@ -2419,9 +2733,16 @@ async function resolveOperationContext(args) {
2419
2733
  clientForGdr: buildClientForGdr(args.client, args.resourceClients)
2420
2734
  };
2421
2735
  }
2422
- async function cascade(client, instanceId, actor, clientForGdr) {
2423
- const count = await cascadeAutoTransitions(client, instanceId, actor, clientForGdr);
2424
- return await propagateToAncestors(client, instanceId, actor, clientForGdr), count;
2736
+ async function cascade(client, instanceId, actor, clientForGdr, clock, overlay) {
2737
+ const count = await cascadeAutoTransitions(
2738
+ client,
2739
+ instanceId,
2740
+ actor,
2741
+ clientForGdr,
2742
+ clock,
2743
+ overlay
2744
+ );
2745
+ return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
2425
2746
  }
2426
2747
  function buildClientForGdr(defaultClient, resolver) {
2427
2748
  return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
@@ -2439,6 +2760,22 @@ function intersectsTags(docTags, engineTags) {
2439
2760
  function toEffectsContextEntries(ctx) {
2440
2761
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2441
2762
  }
2763
+ function assertActionAllowed(evaluation, taskId, action) {
2764
+ const actionEval = evaluation.currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2765
+ if (actionEval?.allowed === !1 && actionEval.disabledReason)
2766
+ throw new ActionDisabledError({ taskId, action, reason: actionEval.disabledReason });
2767
+ }
2768
+ async function applyAction(args) {
2769
+ const { client, instance, taskId, action, params, actor, clock } = args, options = { actor, ...clock !== void 0 ? { clock } : {} };
2770
+ return findOpenStageEntry(instance)?.tasks.find((t) => t.id === taskId)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, taskId, options }), (await fireAction({
2771
+ client,
2772
+ instanceId: instance._id,
2773
+ taskId,
2774
+ action,
2775
+ ...params !== void 0 ? { params } : {},
2776
+ options
2777
+ })).ranOps;
2778
+ }
2442
2779
  const workflow = {
2443
2780
  /**
2444
2781
  * Persist a workflow definition to the lake. Stored as a
@@ -2471,7 +2808,7 @@ const workflow = {
2471
2808
  const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
2472
2809
  ...def,
2473
2810
  _id: id,
2474
- _type: "workflow.definition",
2811
+ _type: schema.WORKFLOW_DEFINITION_TYPE,
2475
2812
  tags
2476
2813
  }, existing = await client.getDocument(id);
2477
2814
  if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
@@ -2513,16 +2850,17 @@ const workflow = {
2513
2850
  effectsContext,
2514
2851
  instanceId,
2515
2852
  perspective
2516
- } = args, { actor, clientForGdr } = await resolveOperationContext(args), definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2853
+ } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2517
2854
  if (definition.stages.find((s) => s.id === definition.initialStageId) === void 0)
2518
2855
  throw new Error(
2519
2856
  `Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
2520
2857
  );
2521
- const now = (/* @__PURE__ */ new Date()).toISOString(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2858
+ const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2522
2859
  slotDefs: definition.state ?? [],
2523
2860
  initialState: initialState ?? [],
2524
2861
  ctx: {
2525
2862
  client,
2863
+ now,
2526
2864
  selfId: id,
2527
2865
  tags,
2528
2866
  workflowResource,
@@ -2544,7 +2882,7 @@ const workflow = {
2544
2882
  initialStageId: definition.initialStageId,
2545
2883
  actor
2546
2884
  });
2547
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr), await cascade(client, id, actor, clientForGdr), reload(client, id, tags);
2885
+ return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
2548
2886
  },
2549
2887
  /**
2550
2888
  * Fire an action against a task. If the task is pending, it is
@@ -2566,43 +2904,32 @@ const workflow = {
2566
2904
  params,
2567
2905
  idempotent,
2568
2906
  resourceClients
2569
- } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), before = await reload(client, instanceId, tags), taskEntry = before.stages.find(
2570
- (s) => s.id === before.currentStageId && s.exitedAt === void 0
2571
- )?.tasks.find((t) => t.id === taskId);
2572
- if (taskEntry === void 0 && idempotent === !0)
2907
+ } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags);
2908
+ if (findOpenStageEntry(before)?.tasks.find((t) => t.id === taskId) === void 0 && idempotent === !0)
2573
2909
  return { instance: before, cascaded: 0, fired: !1 };
2574
- const actionEval = (await evaluateInstance({
2910
+ const evaluation = await evaluateInstance({
2575
2911
  client,
2576
2912
  tags,
2577
2913
  instanceId,
2578
2914
  access,
2915
+ clock,
2579
2916
  ...resourceClients !== void 0 ? { resourceClients } : {}
2580
- })).currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2581
- if (actionEval !== void 0 && !actionEval.allowed && actionEval.disabledReason)
2582
- throw new ActionDisabledError({
2583
- taskId,
2584
- action,
2585
- reason: actionEval.disabledReason
2586
- });
2587
- taskEntry?.status === "pending" && await invokeTask({
2588
- client,
2589
- instanceId,
2590
- taskId,
2591
- options: { actor }
2592
2917
  });
2593
- const actionResult = await fireAction({
2918
+ assertActionAllowed(evaluation, taskId, action);
2919
+ const ranOps = await applyAction({
2594
2920
  client,
2595
- instanceId,
2921
+ instance: before,
2596
2922
  taskId,
2597
2923
  action,
2598
- ...params !== void 0 ? { params } : {},
2599
- options: { actor }
2600
- }), cascaded = await cascade(client, instanceId, actor, clientForGdr);
2924
+ actor,
2925
+ clock,
2926
+ ...params !== void 0 ? { params } : {}
2927
+ }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2601
2928
  return {
2602
2929
  instance: await reload(client, instanceId, tags),
2603
2930
  cascaded,
2604
2931
  fired: !0,
2605
- ...actionResult.ranOps !== void 0 ? { ranOps: actionResult.ranOps } : {}
2932
+ ...ranOps !== void 0 ? { ranOps } : {}
2606
2933
  };
2607
2934
  },
2608
2935
  /**
@@ -2613,7 +2940,7 @@ const workflow = {
2613
2940
  * reference them. Cascades after.
2614
2941
  */
2615
2942
  completeEffect: async (args) => {
2616
- const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2943
+ const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2617
2944
  await completeEffect({
2618
2945
  client,
2619
2946
  instanceId,
@@ -2623,9 +2950,9 @@ const workflow = {
2623
2950
  ...detail !== void 0 ? { detail } : {},
2624
2951
  ...error !== void 0 ? { error } : {},
2625
2952
  ...durationMs !== void 0 ? { durationMs } : {},
2626
- ...actor !== void 0 ? { options: { actor } } : {}
2953
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
2627
2954
  });
2628
- const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2955
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2629
2956
  return {
2630
2957
  instance: await reload(client, instanceId, tags),
2631
2958
  cascaded,
@@ -2642,9 +2969,9 @@ const workflow = {
2642
2969
  * affected instances and the engine re-evaluates.
2643
2970
  */
2644
2971
  tick: async (args) => {
2645
- const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2972
+ const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2646
2973
  await reload(client, instanceId, tags);
2647
- const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2974
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2648
2975
  return {
2649
2976
  instance: await reload(client, instanceId, tags),
2650
2977
  cascaded,
@@ -2657,15 +2984,15 @@ const workflow = {
2657
2984
  * upstream; this verb performs the mechanical move.
2658
2985
  */
2659
2986
  setStage: async (args) => {
2660
- const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2987
+ const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2661
2988
  await reload(client, instanceId, tags);
2662
2989
  const result = await setStage({
2663
2990
  client,
2664
2991
  instanceId,
2665
2992
  targetStageId,
2666
2993
  ...reason !== void 0 ? { reason } : {},
2667
- ...actor !== void 0 ? { options: { actor } } : {}
2668
- }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr) : 0;
2994
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
2995
+ }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
2669
2996
  return {
2670
2997
  instance: await reload(client, instanceId, tags),
2671
2998
  cascaded,
@@ -2681,6 +3008,16 @@ const workflow = {
2681
3008
  const { client, tags, instanceId } = args;
2682
3009
  return validateTags(tags), reload(client, instanceId, tags);
2683
3010
  },
3011
+ guardsForInstance: async (args) => {
3012
+ const { client, tags, instanceId, resourceClients } = args;
3013
+ validateTags(tags);
3014
+ const instance = await reload(client, instanceId, tags);
3015
+ return guardsForInstance({
3016
+ client,
3017
+ clientForGdr: buildClientForGdr(client, resourceClients),
3018
+ instance
3019
+ });
3020
+ },
2684
3021
  /**
2685
3022
  * Run a caller-supplied GROQ query with the engine's tags bound as
2686
3023
  * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
@@ -2715,9 +3052,9 @@ const workflow = {
2715
3052
  * without re-implementing hydration. Pure read — never writes.
2716
3053
  */
2717
3054
  queryInScope: async (args) => {
2718
- const { client, tags, instanceId, groq, params, resourceClients } = args;
3055
+ const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
2719
3056
  validateTags(tags);
2720
- 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 = groqJs.parse(groq, { params: { ...reserved, ...params } });
3057
+ const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams(instance, clock()), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
2721
3058
  return await (await groqJs.evaluate(tree, {
2722
3059
  dataset: snapshot.docs,
2723
3060
  params: { ...reserved, ...params }
@@ -2760,7 +3097,7 @@ const workflow = {
2760
3097
  validateTags(tags);
2761
3098
  const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "workflow.history.spawned", ids = parent.history.filter(isSpawned).filter((h) => taskId === void 0 || h.taskId === taskId).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
2762
3099
  return ids.length === 0 ? [] : client.fetch(
2763
- "*[_id in $ids && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)",
3100
+ `*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
2764
3101
  { ids, engineTags: tags }
2765
3102
  );
2766
3103
  },
@@ -2779,7 +3116,7 @@ async function verifyDeployedDefinitionsInternal(args) {
2779
3116
  const { client, tags, effectHandlers, missingHandler, logger } = args;
2780
3117
  validateTags(tags);
2781
3118
  const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
2782
- '*[_type == "workflow.definition" && count(tags[@ in $engineTags]) > 0] | order(workflowId asc, version asc)',
3119
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(workflowId asc, version asc)`,
2783
3120
  { engineTags: tags }
2784
3121
  ), seen = [], missingByName = /* @__PURE__ */ new Map();
2785
3122
  for (const def of definitions)
@@ -2901,7 +3238,8 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
2901
3238
  };
2902
3239
  try {
2903
3240
  return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
2904
- } catch {
3241
+ } catch (error) {
3242
+ if (!isRevisionConflict(error)) throw error;
2905
3243
  return !1;
2906
3244
  }
2907
3245
  }
@@ -2929,6 +3267,89 @@ async function applyMissingHandler(policy, info, log) {
2929
3267
  return "fail";
2930
3268
  }
2931
3269
  }
3270
+ function createInstanceSession(args) {
3271
+ const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
3272
+ let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), committing = !1, buffered;
3273
+ const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
3274
+ const next = /* @__PURE__ */ new Map();
3275
+ for (const ld of docs) {
3276
+ const owned = { doc: structuredClone(ld.doc), resource: ld.resource }, uri = gdrFromResource(owned.resource, owned.doc._id);
3277
+ if (uri === selfUri()) {
3278
+ instance = asHeldInstance(owned.doc);
3279
+ continue;
3280
+ }
3281
+ next.set(uri, owned);
3282
+ }
3283
+ overlay = next;
3284
+ }, access = () => resolveAccess(client, {
3285
+ ...args.access !== void 0 ? { override: args.access } : {},
3286
+ ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
3287
+ }), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held) => {
3288
+ const { actor, grants } = await access();
3289
+ return evaluateFromSnapshot({
3290
+ instance,
3291
+ definition: parseDefinitionSnapshot(instance),
3292
+ actor,
3293
+ snapshot: snapshotFrom(held),
3294
+ ...clock !== void 0 ? { now: clock() } : {},
3295
+ ...grants !== void 0 ? { grants } : {}
3296
+ });
3297
+ }, commit = async (run) => {
3298
+ committing = !0;
3299
+ const held = new Map(overlay);
3300
+ try {
3301
+ const { actor } = await access();
3302
+ return await run(actor, held);
3303
+ } finally {
3304
+ if (committing = !1, buffered !== void 0) {
3305
+ const next = buffered;
3306
+ buffered = void 0, applyUpdate(next);
3307
+ }
3308
+ }
3309
+ };
3310
+ return {
3311
+ get subscriptionDocuments() {
3312
+ return subscriptionDocumentsForInstance(instance);
3313
+ },
3314
+ update(docs) {
3315
+ if (committing) {
3316
+ buffered = docs;
3317
+ return;
3318
+ }
3319
+ applyUpdate(docs);
3320
+ },
3321
+ evaluate() {
3322
+ return evaluateWith(overlay);
3323
+ },
3324
+ tick() {
3325
+ return commit(async (actor, held) => {
3326
+ const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3327
+ return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
3328
+ });
3329
+ },
3330
+ fireAction({ taskId, action, params }) {
3331
+ return commit(async (actor, held) => {
3332
+ assertActionAllowed(await evaluateWith(held), taskId, action);
3333
+ const ranOps = await applyAction({
3334
+ client,
3335
+ instance,
3336
+ taskId,
3337
+ action,
3338
+ actor,
3339
+ ...clock !== void 0 ? { clock } : {},
3340
+ ...params !== void 0 ? { params } : {}
3341
+ }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3342
+ return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
3343
+ });
3344
+ }
3345
+ };
3346
+ }
3347
+ function asHeldInstance(doc) {
3348
+ const candidate = doc;
3349
+ if (candidate._type !== "workflow.instance" || typeof candidate.currentStageId != "string" || !Array.isArray(candidate.stages))
3350
+ throw new Error(`update: self-doc ${doc._id} is not a valid workflow.instance`);
3351
+ return doc;
3352
+ }
2932
3353
  const defaultLoggerFactory = (name) => ({
2933
3354
  // info/warn/error all go to stderr — these are diagnostic, not
2934
3355
  // program output. Keeps stdout reserved for the caller's own data
@@ -2946,13 +3367,14 @@ const defaultLoggerFactory = (name) => ({
2946
3367
  }
2947
3368
  };
2948
3369
  function createEngine(args) {
2949
- const { client, workflowResource, tags, resourceClients } = args;
3370
+ const { client, workflowResource, tags, resourceClients, clock } = args;
2950
3371
  validateTags(tags);
2951
3372
  const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
2952
3373
  client,
2953
3374
  tags,
2954
3375
  workflowResource,
2955
3376
  ...resourceClients !== void 0 ? { resourceClients } : {},
3377
+ ...clock !== void 0 ? { clock } : {},
2956
3378
  ...rest
2957
3379
  });
2958
3380
  return {
@@ -2970,6 +3392,23 @@ function createEngine(args) {
2970
3392
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
2971
3393
  setStage: (rest) => workflow.setStage(bind(rest)),
2972
3394
  getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
3395
+ subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
3396
+ instance: (instanceDoc, opts) => createInstanceSession({
3397
+ client,
3398
+ tags,
3399
+ instance: instanceDoc,
3400
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3401
+ ...clock !== void 0 ? { clock } : {},
3402
+ ...opts?.access !== void 0 ? { access: opts.access } : {},
3403
+ ...opts?.grantsFromPath !== void 0 ? { grantsFromPath: opts.grantsFromPath } : {}
3404
+ }),
3405
+ guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
3406
+ client,
3407
+ tags,
3408
+ workflowResource,
3409
+ instanceId,
3410
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3411
+ }),
2973
3412
  children: ({ instanceId, taskId }) => workflow.children({
2974
3413
  client,
2975
3414
  tags,
@@ -2990,7 +3429,8 @@ function createEngine(args) {
2990
3429
  workflowResource,
2991
3430
  instanceId,
2992
3431
  groq,
2993
- ...params !== void 0 ? { params } : {}
3432
+ ...params !== void 0 ? { params } : {},
3433
+ ...clock !== void 0 ? { clock } : {}
2994
3434
  }),
2995
3435
  listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tags, workflowResource, instanceId }),
2996
3436
  findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
@@ -3021,15 +3461,6 @@ function createEngine(args) {
3021
3461
  })
3022
3462
  };
3023
3463
  }
3024
- class MutationGuardDeniedError extends Error {
3025
- denied;
3026
- documentId;
3027
- action;
3028
- constructor(args) {
3029
- const ids = args.denied.map((d) => d.guardId).join(", ");
3030
- 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;
3031
- }
3032
- }
3033
3464
  const HISTORY_DISPLAY = {
3034
3465
  "workflow.history.stageEntered": {
3035
3466
  title: "Stage entered",
@@ -3221,43 +3652,60 @@ function displayDescription(typeKey) {
3221
3652
  if (typeKey)
3222
3653
  return DISPLAY[typeKey]?.description;
3223
3654
  }
3655
+ exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
3224
3656
  exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
3225
3657
  exports.ActionDisabledError = ActionDisabledError;
3226
3658
  exports.ActionParamsInvalidError = ActionParamsInvalidError;
3659
+ exports.CascadeLimitError = CascadeLimitError;
3660
+ exports.ConcurrentFireActionError = ConcurrentFireActionError;
3227
3661
  exports.DISPLAY = DISPLAY;
3228
3662
  exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
3663
+ exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
3229
3664
  exports.GUARD_LIFTED_PREDICATE = GUARD_LIFTED_PREDICATE;
3230
3665
  exports.GUARD_OWNER = GUARD_OWNER;
3231
3666
  exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
3232
3667
  exports.MutationGuardDeniedError = MutationGuardDeniedError;
3233
3668
  exports.OP_DISPLAY = OP_DISPLAY;
3234
3669
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
3670
+ exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
3671
+ exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
3672
+ exports.buildSnapshot = buildSnapshot;
3235
3673
  exports.canonicalTag = canonicalTag;
3236
3674
  exports.compileGuard = compileGuard;
3675
+ exports.contentReleaseName = contentReleaseName;
3237
3676
  exports.createEngine = createEngine;
3238
3677
  exports.defaultLoggerFactory = defaultLoggerFactory;
3239
3678
  exports.denyingGuards = denyingGuards;
3240
3679
  exports.deployStageGuards = deployStageGuards;
3241
3680
  exports.displayDescription = displayDescription;
3242
3681
  exports.displayTitle = displayTitle;
3682
+ exports.evaluateFromSnapshot = evaluateFromSnapshot;
3243
3683
  exports.evaluateMutationGuard = evaluateMutationGuard;
3244
3684
  exports.extractDocumentId = extractDocumentId;
3245
3685
  exports.gdrFromResource = gdrFromResource;
3246
3686
  exports.gdrRef = gdrRef;
3247
3687
  exports.gdrUri = gdrUri;
3248
3688
  exports.guardMatches = guardMatches;
3689
+ exports.guardsForInstance = guardsForInstance;
3690
+ exports.guardsForResource = guardsForResource;
3691
+ exports.isDefinitionUnchanged = isDefinitionUnchanged;
3249
3692
  exports.isGdr = isGdr;
3250
3693
  exports.lakeGuardId = lakeGuardId;
3251
3694
  exports.parseGdr = parseGdr;
3252
- exports.reconcileStageGuards = reconcileStageGuards;
3695
+ exports.readsRaw = readsRaw;
3253
3696
  exports.refCanvas = refCanvas;
3254
3697
  exports.refDashboard = refDashboard;
3255
3698
  exports.refDataset = refDataset;
3256
3699
  exports.refMediaLibrary = refMediaLibrary;
3257
3700
  exports.resolveAccess = resolveAccess;
3701
+ exports.resourceFromParsed = resourceFromParsed;
3258
3702
  exports.retractStageGuards = retractStageGuards;
3259
3703
  exports.silentLogger = silentLogger;
3704
+ exports.stripSystemFields = stripSystemFields;
3705
+ exports.subscriptionDocumentsForInstance = subscriptionDocumentsForInstance;
3706
+ exports.tagScopeFilter = tagScopeFilter;
3260
3707
  exports.validateDefinition = validateDefinition;
3261
3708
  exports.validateTags = validateTags;
3709
+ exports.wallClock = wallClock;
3262
3710
  exports.workflow = workflow;
3263
3711
  //# sourceMappingURL=index.cjs.map