@sanity/workflow-engine 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3264 @@
1
+ import { parse, evaluate } from "groq-js";
2
+ import { z } from "zod";
3
+ function validateDefinition(definition) {
4
+ const v = createDefinitionValidator();
5
+ for (const slot of definition.state ?? [])
6
+ v.checkSlot(slot, "workflow.state", `workflow.state "${slot.id}"`);
7
+ for (const p of definition.predicates ?? [])
8
+ p.groq && v.checkFilter(p.groq, `predicate "${p.id}"`);
9
+ for (const stage of definition.stages)
10
+ validateStage(v, stage);
11
+ if (v.errors.length > 0)
12
+ throw new Error(
13
+ `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v.errors.length} validation error${v.errors.length === 1 ? "" : "s"}:
14
+ ` + v.errors.join(`
15
+ `)
16
+ );
17
+ }
18
+ function createDefinitionValidator() {
19
+ const errors = [], tryParse = (groq, where) => {
20
+ try {
21
+ parse(groq);
22
+ } catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ errors.push(` \xB7 ${where}: ${message}`);
25
+ }
26
+ }, rejectTypeScan = (groq, where) => {
27
+ /\*\s*\[\s*_type\b/.test(groq) && errors.push(
28
+ ` \xB7 ${where}: filter scans by \`_type\` \u2014 that's a discovery query, not a predicate. Filters evaluate against the in-memory snapshot (instance + ancestors + state-declared docs). To bring extra docs in scope, declare a \`workflow.state.doc.ref\` (or \`doc.refs\`) slot on the workflow or this stage. For lake scans like "all articles in this release", use spawn \`forEach.groq\` instead.`
29
+ );
30
+ };
31
+ return { errors, tryParse, checkFilter: (groq, where) => {
32
+ tryParse(groq, where), rejectTypeScan(groq, where);
33
+ }, checkSlot: (slot, scopeLabel, slotLabel) => {
34
+ slot.source.kind === "computed" && errors.push(
35
+ ` \xB7 ${scopeLabel} slot "${slot.id}": source.kind="computed" is reserved (not implemented in v0.1)`
36
+ ), slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
37
+ } };
38
+ }
39
+ function validateStage(v, stage) {
40
+ for (const slot of stage.state ?? [])
41
+ v.checkSlot(slot, `stage "${stage.id}".state`, `stage "${stage.id}".state "${slot.id}"`);
42
+ typeof stage.completion == "string" && v.tryParse(stage.completion, `stage "${stage.id}".completion`);
43
+ for (const t of stage.transitions ?? [])
44
+ typeof t.filter == "string" && v.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
45
+ for (const task of stage.tasks ?? [])
46
+ validateTask(v, stage.id, task);
47
+ }
48
+ function validateTask(v, stageId, task) {
49
+ const where = `stage "${stageId}" task "${task.id}"`;
50
+ for (const slot of task.state ?? [])
51
+ v.checkSlot(slot, `${where}.state`, `${where}.state "${slot.id}"`);
52
+ typeof task.filter == "string" && v.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v.checkFilter(task.completeWhen, `${where}.completeWhen`);
53
+ for (const a of task.actions ?? [])
54
+ typeof a.filter == "string" && v.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
55
+ task.spawns?.forEach?.groq && v.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
56
+ }
57
+ const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
58
+ function parseGdr(uri) {
59
+ const colon = uri.indexOf(":");
60
+ if (colon < 0)
61
+ throw new Error(
62
+ `Invalid GDR "${uri}": must be a URI of form "<scheme>:<...id-parts>". Known schemes: dataset, canvas, media-library, dashboard.`
63
+ );
64
+ const scheme = uri.slice(0, colon), rest = uri.slice(colon + 1);
65
+ if (!KNOWN_SCHEMES.has(scheme))
66
+ throw new Error(
67
+ `Invalid GDR "${uri}": unknown scheme "${scheme}". Known: dataset, canvas, media-library, dashboard.`
68
+ );
69
+ const parts = rest.split(":");
70
+ if (parts.some((part) => part.length === 0))
71
+ throw new Error(
72
+ `Invalid GDR "${uri}": id parts must be non-empty (no leading, trailing, or doubled ":").`
73
+ );
74
+ if (scheme === "dataset") {
75
+ if (parts.length !== 3)
76
+ throw new Error(
77
+ `Invalid GDR "${uri}": dataset scheme requires <projectId>:<dataset>:<documentId> (3 parts after scheme); got ${parts.length}.`
78
+ );
79
+ return {
80
+ scheme: "dataset",
81
+ projectId: parts[0],
82
+ dataset: parts[1],
83
+ documentId: parts[2]
84
+ };
85
+ }
86
+ if (parts.length !== 2)
87
+ throw new Error(
88
+ `Invalid GDR "${uri}": ${scheme} scheme requires <resourceId>:<documentId> (2 parts after scheme); got ${parts.length}.`
89
+ );
90
+ return {
91
+ scheme,
92
+ resourceId: parts[0],
93
+ documentId: parts[1]
94
+ };
95
+ }
96
+ function gdrUri(parts) {
97
+ return parts.scheme === "dataset" ? `dataset:${parts.projectId}:${parts.dataset}:${parts.documentId}` : `${parts.scheme}:${parts.resourceId}:${parts.documentId}`;
98
+ }
99
+ function extractDocumentId(gdrUriString) {
100
+ return parseGdr(gdrUriString).documentId;
101
+ }
102
+ function refDataset(projectId, dataset, documentId, type) {
103
+ return { id: gdrUri({ scheme: "dataset", projectId, dataset, documentId }), type };
104
+ }
105
+ function refCanvas(resourceId, documentId, type) {
106
+ return { id: gdrUri({ scheme: "canvas", resourceId, documentId }), type };
107
+ }
108
+ function refMediaLibrary(resourceId, documentId, type) {
109
+ return { id: gdrUri({ scheme: "media-library", resourceId, documentId }), type };
110
+ }
111
+ function refDashboard(resourceId, documentId, type) {
112
+ return { id: gdrUri({ scheme: "dashboard", resourceId, documentId }), type };
113
+ }
114
+ function gdrFromResource(res, documentId) {
115
+ if (res.type === "dataset") {
116
+ const dot = res.id.indexOf(".");
117
+ if (dot < 0)
118
+ throw new Error(`Invalid dataset resource id "${res.id}": expected "<projectId>.<dataset>".`);
119
+ const projectId = res.id.slice(0, dot), dataset = res.id.slice(dot + 1);
120
+ return gdrUri({ scheme: "dataset", projectId, dataset, documentId });
121
+ }
122
+ return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
123
+ }
124
+ function isGdrUri(value) {
125
+ if (typeof value != "string") return !1;
126
+ try {
127
+ return parseGdr(value), !0;
128
+ } catch {
129
+ return !1;
130
+ }
131
+ }
132
+ function gdrRef(res, documentId, type) {
133
+ return { id: gdrFromResource(res, documentId), type };
134
+ }
135
+ function isGdr(value) {
136
+ return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
137
+ }
138
+ function buildParams(instance, extra) {
139
+ return {
140
+ self: gdrFromResource(instance.workflowResource, instance._id),
141
+ state: stateMap(instance.state ?? []),
142
+ parent: instance.ancestors.at(-1)?.id ?? null,
143
+ ancestors: instance.ancestors.map((a) => a.id),
144
+ /** Current stage id — bind for filters that scope to the current stage. */
145
+ stage: instance.currentStageId,
146
+ now: (/* @__PURE__ */ new Date()).toISOString(),
147
+ ...extra
148
+ };
149
+ }
150
+ function stateMap(slots) {
151
+ const out = {};
152
+ for (const s of slots) out[s.id] = s;
153
+ return out;
154
+ }
155
+ function paramsForLake(params) {
156
+ return {
157
+ ...params,
158
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
159
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
160
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
161
+ state: stripStateForLake(params.state)
162
+ };
163
+ }
164
+ function stripStateForLake(value) {
165
+ if (value == null) return value;
166
+ if (typeof value == "string") return bareId(value);
167
+ if (Array.isArray(value)) return value.map(stripStateForLake);
168
+ if (typeof value == "object") {
169
+ const out = {};
170
+ for (const [k, v] of Object.entries(value))
171
+ out[k] = stripStateForLake(v);
172
+ return out;
173
+ }
174
+ return value;
175
+ }
176
+ function bareId(id) {
177
+ return isGdrUri(id) ? extractDocumentId(id) : id;
178
+ }
179
+ function randomKey(length = 12) {
180
+ const bytes = new Uint8Array(length);
181
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
182
+ }
183
+ async function evaluateFilter(args) {
184
+ const { filter, definition, snapshot, params } = args;
185
+ if (filter === void 0) return !0;
186
+ if (typeof filter == "string")
187
+ return runGroqAgainstSnapshot(filter, params, snapshot);
188
+ const predicate = (definition.predicates ?? []).find((p) => p.id === filter.ref);
189
+ if (predicate === void 0)
190
+ throw new Error(`Unknown predicate ref: ${filter.ref}`);
191
+ validatePredicateArgs(predicate, filter.args ?? {});
192
+ const merged = { ...params, ...filter.args };
193
+ return runGroqAgainstSnapshot(predicate.groq, merged, snapshot);
194
+ }
195
+ async function runGroqAgainstSnapshot(groq, params, snapshot) {
196
+ if (!groq.includes("*")) {
197
+ const tree2 = parse(groq, { params });
198
+ return !!await (await evaluate(tree2, { params })).get();
199
+ }
200
+ const tree = parse(groq, { params });
201
+ return !!await (await evaluate(tree, { dataset: snapshot.docs, params })).get();
202
+ }
203
+ function validatePredicateArgs(predicate, args) {
204
+ for (const param of predicate.params ?? [])
205
+ validatePredicateParam(predicate.id, param, args);
206
+ }
207
+ function validatePredicateParam(predicateId, param, args) {
208
+ if (!(param.id in args))
209
+ throw new Error(`Predicate "${predicateId}" requires param "${param.id}"`);
210
+ const value = args[param.id];
211
+ if (!matchesParamType(param.type, value))
212
+ throw new Error(
213
+ `Predicate "${predicateId}" param "${param.id}" must be of type ${param.type}; got ${describeValue(value)}`
214
+ );
215
+ if (param.enum && !param.enum.includes(value))
216
+ throw new Error(
217
+ `Predicate "${predicateId}" param "${param.id}" must be one of ${param.enum.join(", ")}; got "${String(value)}"`
218
+ );
219
+ }
220
+ function matchesParamType(type, value) {
221
+ switch (type) {
222
+ case "string":
223
+ return typeof value == "string";
224
+ case "number":
225
+ return typeof value == "number";
226
+ case "boolean":
227
+ return typeof value == "boolean";
228
+ case "string[]":
229
+ return Array.isArray(value) && value.every((v) => typeof v == "string");
230
+ case "reference":
231
+ return isGdr(value);
232
+ }
233
+ }
234
+ function describeValue(value) {
235
+ return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
236
+ }
237
+ function buildSnapshot(args) {
238
+ const docs = [], knownIds = /* @__PURE__ */ new Set();
239
+ for (const { doc, resource } of args.docs) {
240
+ const uri = gdrFromResource(resource, doc._id), restamped = restampForSnapshot(doc, uri, resource);
241
+ docs.push(restamped), knownIds.add(uri);
242
+ }
243
+ return { docs, knownIds };
244
+ }
245
+ function restampForSnapshot(doc, gdrUri2, resource) {
246
+ return { ...rewriteRefsRecursive(doc, resource), _id: gdrUri2 };
247
+ }
248
+ function rewriteRefsRecursive(value, resource) {
249
+ if (Array.isArray(value))
250
+ return value.map((v) => rewriteRefsRecursive(v, resource));
251
+ if (value === null || typeof value != "object") return value;
252
+ const obj = value, out = {};
253
+ for (const [k, v] of Object.entries(obj))
254
+ k === "_ref" && typeof v == "string" ? out[k] = v.includes(":") ? v : gdrFromResource(resource, v) : out[k] = rewriteRefsRecursive(v, resource);
255
+ return out;
256
+ }
257
+ async function hydrateSnapshot(args) {
258
+ const { client, clientForGdr, instance, definition } = args, loaded = [], visited = /* @__PURE__ */ new Set(), loadInto = async (uri) => {
259
+ if (visited.has(uri)) return;
260
+ const fetched = await loadByGdr(client, clientForGdr, instance.workflowResource, uri);
261
+ fetched && (loaded.push(fetched), visited.add(uri));
262
+ };
263
+ loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(gdrFromResource(instance.workflowResource, instance._id));
264
+ for (const ancestor of instance.ancestors)
265
+ await loadInto(ancestor.id);
266
+ for (const uri of collectSlotDocUris(instance.state))
267
+ await loadInto(uri);
268
+ if (definition) {
269
+ const stageEntry = instance.stages.find(
270
+ (s) => s.id === instance.currentStageId && s.exitedAt === void 0
271
+ );
272
+ for (const uri of collectSlotDocUris(stageEntry?.state))
273
+ await loadInto(uri);
274
+ }
275
+ return buildSnapshot({ docs: loaded });
276
+ }
277
+ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri) {
278
+ let parsed;
279
+ try {
280
+ parsed = parseGdr(uri);
281
+ } catch {
282
+ const doc2 = await defaultClient.getDocument(uri);
283
+ return doc2 ? { doc: doc2, resource: defaultResource } : null;
284
+ }
285
+ const doc = await clientForGdr(parsed).getDocument(parsed.documentId);
286
+ return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
287
+ }
288
+ function resourceFromParsed(parsed) {
289
+ return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
290
+ }
291
+ function collectSlotDocUris(resolvedStateSlots) {
292
+ return Array.isArray(resolvedStateSlots) ? resolvedStateSlots.flatMap(slotDocUris) : [];
293
+ }
294
+ function slotDocUris(slot) {
295
+ if (!slot || typeof slot != "object") return [];
296
+ const s = slot;
297
+ if (s._type === "workflow.state.doc.ref") {
298
+ const v = s.value, id = refId(v);
299
+ return id !== void 0 ? [id] : [];
300
+ }
301
+ if (s._type === "workflow.state.doc.refs") {
302
+ const v = s.value;
303
+ return Array.isArray(v) ? v.map(refId).filter((id) => id !== void 0) : [];
304
+ }
305
+ return [];
306
+ }
307
+ function refId(ref) {
308
+ return ref && typeof ref.id == "string" ? ref.id : void 0;
309
+ }
310
+ function getPath(value, path) {
311
+ let current = value;
312
+ for (const part of path.split(".")) {
313
+ if (current == null || typeof current != "object") return;
314
+ current = current[part];
315
+ }
316
+ return current;
317
+ }
318
+ class SlotValueShapeError extends Error {
319
+ slotType;
320
+ slotId;
321
+ issues;
322
+ constructor(args) {
323
+ const issueText = args.issues.join("; ");
324
+ super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
325
+ }
326
+ }
327
+ const GdrShape = z.object({
328
+ id: z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
329
+ type: z.string().min(1)
330
+ }).passthrough(), ReleaseRefShape = z.object({
331
+ id: z.string().refine((s) => isGdrUri(s), { message: "must be a GDR URI" }),
332
+ type: z.literal("system.release"),
333
+ releaseName: z.string().min(1)
334
+ }).passthrough(), ActorShape = z.object({
335
+ kind: z.enum(["user", "ai", "system"]),
336
+ id: z.string().min(1),
337
+ roles: z.array(z.string()).optional(),
338
+ onBehalfOf: z.string().optional()
339
+ }).passthrough(), AssigneeShape = z.union([
340
+ z.object({ kind: z.literal("user"), id: z.string().min(1) }).passthrough(),
341
+ z.object({ kind: z.literal("role"), role: z.string().min(1) }).passthrough()
342
+ ]), ChecklistItemShape = z.object({
343
+ label: z.string().min(1),
344
+ done: z.boolean(),
345
+ doneBy: z.string().optional(),
346
+ doneAt: z.string().optional(),
347
+ _key: z.string().min(1).optional()
348
+ }).passthrough(), NoteItemShape = z.object({
349
+ _key: z.string().min(1).optional()
350
+ }).passthrough().refine((v) => typeof v == "object" && v !== null && !Array.isArray(v), {
351
+ message: "must be an object"
352
+ }), NullableString = z.union([z.null(), z.string()]), NullableNumber = z.union([z.null(), z.number()]), NullableBoolean = z.union([z.null(), z.boolean()]), NullableDateTime = z.union([
353
+ z.null(),
354
+ z.string().refine((s) => !Number.isNaN(Date.parse(s)), {
355
+ message: "must be an ISO-8601 datetime string"
356
+ })
357
+ ]), NullableUrl = NullableString, valueSchemas = {
358
+ "workflow.state.doc.ref": z.union([z.null(), GdrShape]),
359
+ "workflow.state.doc.refs": z.array(GdrShape),
360
+ "workflow.state.release.ref": z.union([z.null(), ReleaseRefShape]),
361
+ "workflow.state.query": z.any(),
362
+ "workflow.state.value.string": NullableString,
363
+ "workflow.state.value.url": NullableUrl,
364
+ "workflow.state.value.number": NullableNumber,
365
+ "workflow.state.value.boolean": NullableBoolean,
366
+ "workflow.state.value.dateTime": NullableDateTime,
367
+ "workflow.state.value.actor": z.union([z.null(), ActorShape]),
368
+ "workflow.state.checklist": z.array(ChecklistItemShape),
369
+ "workflow.state.notes": z.array(NoteItemShape),
370
+ "workflow.state.assignees": z.array(AssigneeShape)
371
+ }, itemSchemas = {
372
+ "workflow.state.doc.refs": GdrShape,
373
+ "workflow.state.checklist": ChecklistItemShape,
374
+ "workflow.state.notes": NoteItemShape,
375
+ "workflow.state.assignees": AssigneeShape
376
+ };
377
+ function isAppendable(slotType) {
378
+ return slotType in itemSchemas;
379
+ }
380
+ function validateSlotValue(args) {
381
+ const schema = valueSchemas[args.slotType];
382
+ if (schema === void 0)
383
+ throw new SlotValueShapeError({
384
+ slotType: args.slotType,
385
+ slotId: args.slotId,
386
+ issues: [`unknown slot type ${args.slotType}`],
387
+ mode: "value"
388
+ });
389
+ const result = schema.safeParse(args.value);
390
+ if (!result.success)
391
+ throw new SlotValueShapeError({
392
+ slotType: args.slotType,
393
+ slotId: args.slotId,
394
+ issues: formatIssues(result.error.issues),
395
+ mode: "value"
396
+ });
397
+ }
398
+ function validateSlotAppendItem(args) {
399
+ if (!isAppendable(args.slotType))
400
+ throw new SlotValueShapeError({
401
+ slotType: args.slotType,
402
+ slotId: args.slotId,
403
+ issues: [`slot type ${args.slotType} does not support append`],
404
+ mode: "item"
405
+ });
406
+ const result = itemSchemas[args.slotType].safeParse(args.item);
407
+ if (!result.success)
408
+ throw new SlotValueShapeError({
409
+ slotType: args.slotType,
410
+ slotId: args.slotId,
411
+ issues: formatIssues(result.error.issues),
412
+ mode: "item"
413
+ });
414
+ }
415
+ function formatIssues(issues) {
416
+ return issues.map((i) => `${i.path.length > 0 ? `at ${i.path.join(".")}: ` : ""}${i.message}`);
417
+ }
418
+ function derivePerspectiveFromState(slots) {
419
+ if (slots !== void 0) {
420
+ for (const slot of slots)
421
+ if (slot._type === "workflow.state.release.ref" && slot.value !== null) {
422
+ const releaseName = slot.value.releaseName;
423
+ if (typeof releaseName == "string" && releaseName.length > 0)
424
+ return [releaseName];
425
+ }
426
+ }
427
+ }
428
+ async function resolveDeclaredState(args) {
429
+ const { slotDefs, initialState, ctx, randomKey: randomKey2 } = args;
430
+ if (slotDefs === void 0 || slotDefs.length === 0) return [];
431
+ const out = [];
432
+ for (const slot of slotDefs) {
433
+ const scopedCtx = { ...ctx, resolvedState: out };
434
+ out.push(await resolveOneSlot(slot, initialState, scopedCtx, randomKey2));
435
+ }
436
+ return out;
437
+ }
438
+ const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
439
+ "workflow.state.doc.refs",
440
+ "workflow.state.checklist",
441
+ "workflow.state.notes",
442
+ "workflow.state.assignees"
443
+ ]);
444
+ function defaultSlotValue(slotType) {
445
+ return ARRAY_SLOT_TYPES.has(slotType) ? [] : null;
446
+ }
447
+ function resolveInitValue(slot, initialState, defaultValue) {
448
+ const initMatch = initialState.find((s) => s.id === slot.id && s.type === slot.type);
449
+ return initMatch === void 0 ? defaultValue : (assertInitValueShape(slot, initMatch.value), initMatch.value);
450
+ }
451
+ function resolveStateReadValue(source, ctx, defaultValue) {
452
+ const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.id === source.slotId), targetValue = target === void 0 ? void 0 : target.value;
453
+ return (source.path !== void 0 ? getPath(targetValue, source.path) : targetValue) ?? defaultValue;
454
+ }
455
+ async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
456
+ const params = paramsForLake({
457
+ self: ctx.selfId ?? null,
458
+ state: stateMapFromResolved(ctx.resolvedState ?? []),
459
+ stage: ctx.stageId ?? null,
460
+ task: ctx.taskId ?? null,
461
+ now: (/* @__PURE__ */ new Date()).toISOString(),
462
+ engineTags: ctx.tags ?? []
463
+ });
464
+ try {
465
+ const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
466
+ return normalizeQueryResult(slot.type, result, ctx.workflowResource) ?? defaultValue;
467
+ } catch (err) {
468
+ throw new Error(
469
+ `Failed to resolve query slot "${slot.id}" (${slot.type}): ${err instanceof Error ? err.message : String(err)}`,
470
+ { cause: err }
471
+ );
472
+ }
473
+ }
474
+ async function resolveSlotValue(slot, initialState, ctx, defaultValue) {
475
+ const source = slot.source;
476
+ return source.kind === "init" ? resolveInitValue(slot, initialState, defaultValue) : source.kind === "literal" ? source.value ?? defaultValue : source.kind === "stateRead" ? resolveStateReadValue(source, ctx, defaultValue) : source.kind === "query" && ctx.client !== void 0 ? resolveQueryValue(slot, source, ctx, ctx.client, defaultValue) : defaultValue;
477
+ }
478
+ function buildResolvedSlot(slot, value, _key) {
479
+ const titleProp = slot.title !== void 0 ? { title: slot.title } : {}, descriptionProp = slot.description !== void 0 ? { description: slot.description } : {};
480
+ return slot.type === "workflow.state.query" ? {
481
+ _key,
482
+ _type: "workflow.state.query",
483
+ id: slot.id,
484
+ ...titleProp,
485
+ ...descriptionProp,
486
+ value,
487
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
488
+ } : {
489
+ _key,
490
+ _type: slot.type,
491
+ id: slot.id,
492
+ ...titleProp,
493
+ ...descriptionProp,
494
+ value
495
+ };
496
+ }
497
+ async function resolveOneSlot(slot, initialState, ctx, randomKey2) {
498
+ const defaultValue = defaultSlotValue(slot.type), value = await resolveSlotValue(slot, initialState, ctx, defaultValue);
499
+ return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2());
500
+ }
501
+ function stateMapFromResolved(slots) {
502
+ const out = {};
503
+ for (const s of slots) out[s.id] = s;
504
+ return out;
505
+ }
506
+ function assertInitValueShape(slot, value) {
507
+ if (slot.type === "workflow.state.doc.ref") {
508
+ assertGdrShape(value, `state slot "${slot.id}" (workflow.state.doc.ref)`);
509
+ return;
510
+ }
511
+ if (slot.type === "workflow.state.release.ref") {
512
+ assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
513
+ const v = value;
514
+ if (typeof v.releaseName != "string" || v.releaseName.length === 0)
515
+ throw new Error(
516
+ `Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v.releaseName)}.`
517
+ );
518
+ return;
519
+ }
520
+ if (slot.type === "workflow.state.doc.refs") {
521
+ if (!Array.isArray(value))
522
+ throw new Error(
523
+ `Invalid init value for state slot "${slot.id}" (workflow.state.doc.refs): expected an array of GDRs, got ${typeof value}.`
524
+ );
525
+ for (const [i, item] of value.entries())
526
+ assertGdrShape(item, `state slot "${slot.id}" (workflow.state.doc.refs) item [${i}]`);
527
+ }
528
+ }
529
+ function assertGdrShape(value, context) {
530
+ if (typeof value != "object" || value === null)
531
+ throw new Error(
532
+ `Invalid GDR for ${context}: expected { id: "<scheme>:...", type: "<schema>" }, got ${typeof value}.`
533
+ );
534
+ const v = value;
535
+ if (typeof v.id != "string" || !isGdrUri(v.id))
536
+ throw new Error(
537
+ `Invalid GDR for ${context}: \`id\` must be a GDR URI ("<scheme>:<...id-parts>" with scheme dataset|canvas|media-library|dashboard). Got ${JSON.stringify(v.id)}. Construct via \`gdrFromResource\` / \`refDataset\` / \`refCanvas\` etc. \u2014 bare document ids are not accepted.`
538
+ );
539
+ if (typeof v.type != "string" || v.type.length === 0)
540
+ throw new Error(
541
+ `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v.type)}.`
542
+ );
543
+ }
544
+ function normalizeQueryResult(slotType, raw, workflowResource) {
545
+ return raw == null ? raw : slotType === "workflow.state.doc.ref" ? coerceToGdr(raw, workflowResource) : slotType === "workflow.state.doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v) => v !== null) : [] : raw;
546
+ }
547
+ function toGdrUri(docId, workflowResource) {
548
+ return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
549
+ }
550
+ function coerceGdrShape(raw, workflowResource) {
551
+ if (!("id" in raw) || !("type" in raw)) return null;
552
+ const r = raw;
553
+ return typeof r.id != "string" || typeof r.type != "string" ? null : isGdrUri(r.id) ? { id: r.id, type: r.type } : workflowResource ? { id: gdrFromResource(workflowResource, r.id), type: r.type } : null;
554
+ }
555
+ function coerceRefEnvelope(raw, workflowResource) {
556
+ if (!("_ref" in raw)) return null;
557
+ const r = raw;
558
+ return typeof r._ref != "string" || !workflowResource ? null : { id: toGdrUri(r._ref, workflowResource), type: "document" };
559
+ }
560
+ function coerceToGdr(raw, workflowResource) {
561
+ return raw == null ? null : typeof raw == "object" ? coerceGdrShape(raw, workflowResource) ?? coerceRefEnvelope(raw, workflowResource) : typeof raw == "string" && workflowResource ? { id: toGdrUri(raw, workflowResource), type: "document" } : null;
562
+ }
563
+ function gdrToBareId(uri) {
564
+ return uri.includes(":") ? extractDocumentId(uri) : uri;
565
+ }
566
+ async function loadContext(client, instanceId, options) {
567
+ const instance = await client.getDocument(instanceId);
568
+ if (!instance)
569
+ throw new Error(`Workflow instance ${instanceId} not found`);
570
+ const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition });
571
+ return { client, clientForGdr, instance, definition, snapshot };
572
+ }
573
+ function ctxEvaluateFilter(ctx, filter) {
574
+ return evaluateFilter({
575
+ filter,
576
+ definition: ctx.definition,
577
+ snapshot: ctx.snapshot,
578
+ params: buildParams(ctx.instance)
579
+ });
580
+ }
581
+ async function buildEngineContext(args) {
582
+ const { client, clientForGdr, instance, definition } = args;
583
+ return {
584
+ client,
585
+ clientForGdr,
586
+ instance,
587
+ definition,
588
+ snapshot: await hydrateSnapshot({ client, clientForGdr, instance, definition })
589
+ };
590
+ }
591
+ function findStage(definition, stageId) {
592
+ const stage = definition.stages.find((s) => s.id === stageId);
593
+ if (stage === void 0)
594
+ throw new Error(`Stage "${stageId}" not found in definition ${definition.workflowId}`);
595
+ return stage;
596
+ }
597
+ async function resolveStageStateSlots(args) {
598
+ const { client, instance, stage, initialState } = args;
599
+ return resolveDeclaredState({
600
+ slotDefs: stage.state,
601
+ initialState: initialState ?? [],
602
+ ctx: {
603
+ client,
604
+ selfId: instance._id,
605
+ tags: instance.tags ?? [],
606
+ stageId: stage.id,
607
+ workflowResource: instance.workflowResource,
608
+ // Allow stage-scope slots' `source: { kind: "stateRead", scope:
609
+ // "workflow", ... }` to see the already-resolved workflow-scope
610
+ // state.
611
+ workflowState: instance.state ?? [],
612
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
613
+ },
614
+ randomKey
615
+ });
616
+ }
617
+ async function resolveTaskStateSlots(args) {
618
+ const { client, instance, stage, task } = args;
619
+ return resolveDeclaredState({
620
+ slotDefs: task.state,
621
+ initialState: [],
622
+ ctx: {
623
+ client,
624
+ selfId: instance._id,
625
+ tags: instance.tags ?? [],
626
+ stageId: stage.id,
627
+ workflowResource: instance.workflowResource,
628
+ taskId: task.id,
629
+ workflowState: instance.state ?? [],
630
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
631
+ },
632
+ randomKey
633
+ });
634
+ }
635
+ function readStateSlot(instance, scope, slotId, stageId, taskId) {
636
+ if (scope === "workflow")
637
+ return slotValue$1(instance.state?.find((s) => s.id === slotId));
638
+ const stageEntry = instance.stages.find(
639
+ (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
640
+ );
641
+ if (stageEntry === void 0) return;
642
+ if (scope === "stage")
643
+ return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
644
+ const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
645
+ return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
646
+ }
647
+ function slotValue$1(slot) {
648
+ if (slot != null && typeof slot == "object")
649
+ return slot.value;
650
+ }
651
+ function resolveSource(src, ctx) {
652
+ switch (src.source) {
653
+ case "literal":
654
+ return src.value;
655
+ case "param":
656
+ return ctx.params?.[src.paramId];
657
+ case "actor":
658
+ return ctx.actor;
659
+ case "now":
660
+ return (/* @__PURE__ */ new Date()).toISOString();
661
+ case "self":
662
+ return ctx.instance._id;
663
+ case "stageId":
664
+ return ctx.stageId ?? ctx.instance.currentStageId;
665
+ case "stateRead":
666
+ return resolveStateRead(src, ctx);
667
+ case "effectOutput":
668
+ return resolveEffectOutput(src, ctx);
669
+ case "object":
670
+ return resolveObject(src, ctx);
671
+ }
672
+ }
673
+ function resolveStateRead(src, ctx) {
674
+ const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
675
+ return src.path !== void 0 ? getPath(value, src.path) : value;
676
+ }
677
+ function resolveEffectOutput(src, ctx) {
678
+ const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
679
+ if (entry === void 0) return null;
680
+ const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
681
+ return value === void 0 ? null : value;
682
+ }
683
+ function resolveObject(src, ctx) {
684
+ const out = {};
685
+ for (const [field, fieldSrc] of Object.entries(src.fields))
686
+ out[field] = resolveSource(fieldSrc, ctx);
687
+ return out;
688
+ }
689
+ async function resolveBindings(args) {
690
+ const { bindings, staticInput, instance, params, actor } = args, resolved = {};
691
+ if (bindings) {
692
+ const ctx = {
693
+ instance,
694
+ ...params !== void 0 ? { params } : {},
695
+ ...actor !== void 0 ? { actor } : {}
696
+ };
697
+ for (const [key, src] of Object.entries(bindings))
698
+ resolved[key] = resolveSource(src, ctx);
699
+ }
700
+ return { ...resolved, ...staticInput };
701
+ }
702
+ function effectsContextEntry(id, value) {
703
+ const _key = randomKey();
704
+ 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 };
705
+ }
706
+ function buildInstanceBase(args) {
707
+ const { id, now, actor, perspective } = args;
708
+ return {
709
+ _id: id,
710
+ _type: "workflow.instance",
711
+ _rev: "",
712
+ _createdAt: now,
713
+ _updatedAt: now,
714
+ tags: args.tags,
715
+ workflowResource: args.workflowResource,
716
+ workflowId: args.workflowId,
717
+ pinnedVersion: args.pinnedVersion,
718
+ definitionSnapshot: JSON.stringify(args.definition),
719
+ state: args.state,
720
+ effectsContext: args.effectsContext,
721
+ ancestors: args.ancestors,
722
+ ...perspective !== void 0 ? { perspective } : {},
723
+ currentStageId: args.initialStageId,
724
+ stages: [],
725
+ pendingEffects: [],
726
+ effectHistory: [],
727
+ history: [
728
+ {
729
+ _key: randomKey(),
730
+ _type: "workflow.history.stageEntered",
731
+ at: now,
732
+ stageId: args.initialStageId,
733
+ ...actor !== void 0 ? { actor } : {}
734
+ }
735
+ ],
736
+ startedAt: now,
737
+ lastChangedAt: now
738
+ };
739
+ }
740
+ function lakeGuardId(args) {
741
+ return `temp.system.guard.${args.instanceDocId}.${args.stageId}.${args.index}`;
742
+ }
743
+ function compileGuard(args) {
744
+ return {
745
+ _id: args.id,
746
+ _type: "temp.system.guard",
747
+ resourceType: args.resourceType,
748
+ resourceId: args.resourceId,
749
+ owner: args.owner,
750
+ ...args.name !== void 0 ? { name: args.name } : {},
751
+ ...args.description !== void 0 ? { description: args.description } : {},
752
+ match: args.match,
753
+ predicate: args.predicate,
754
+ metadata: args.metadata
755
+ };
756
+ }
757
+ function toGroqJsPredicate(predicate) {
758
+ return predicate.replace(new RegExp("(?<!\\$)\\bguard\\.", "g"), "$guard.").replace(new RegExp("(?<!\\$)\\bmutation\\.", "g"), "$mutation.");
759
+ }
760
+ function globMatch(pattern, value) {
761
+ if (!pattern.includes("*")) return pattern === value;
762
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
763
+ return new RegExp(`^${escaped}$`).test(value);
764
+ }
765
+ function guardMatches(guard, doc, action) {
766
+ const m = guard.match;
767
+ if (!m.actions.includes(action) || m.types && m.types.length > 0 && !(doc.type !== void 0 && m.types.includes(doc.type)))
768
+ return !1;
769
+ if (m.idRefs && m.idRefs.length > 0 || m.idPatterns && m.idPatterns.length > 0) {
770
+ const byRef = m.idRefs?.includes(doc.id) ?? !1, byPattern = m.idPatterns?.some((p) => globMatch(p, doc.id)) ?? !1;
771
+ if (!byRef && !byPattern) return !1;
772
+ }
773
+ return !0;
774
+ }
775
+ async function evaluateMutationGuard(args) {
776
+ const { guard, context } = args;
777
+ if (guard.predicate === "") return !1;
778
+ const params = { guard, mutation: { action: context.action } };
779
+ try {
780
+ const tree = parse(toGroqJsPredicate(guard.predicate), { mode: "delta", params });
781
+ return await (await evaluate(tree, {
782
+ before: context.before,
783
+ after: context.after,
784
+ params,
785
+ ...context.identity !== void 0 ? { identity: context.identity } : {}
786
+ })).get() === !0;
787
+ } catch {
788
+ return !1;
789
+ }
790
+ }
791
+ async function denyingGuards(args) {
792
+ const { guards, doc, context } = args, denied = [];
793
+ for (const guard of guards)
794
+ guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
795
+ return denied;
796
+ }
797
+ function resourceOf(p) {
798
+ return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
799
+ }
800
+ function resolveIdRefTarget(src, ctx) {
801
+ const value = resolveSource(src, ctx);
802
+ if (typeof value == "string" && isGdrUri(value))
803
+ return { parsed: parseGdr(value) };
804
+ if (value && typeof value == "object") {
805
+ const v = value;
806
+ if (typeof v.id == "string" && isGdrUri(v.id))
807
+ return typeof v.type == "string" ? { parsed: parseGdr(v.id), type: v.type } : { parsed: parseGdr(v.id) };
808
+ }
809
+ return null;
810
+ }
811
+ function resolveIdRefTargets(idRefs, ctx) {
812
+ const targets = [];
813
+ for (const src of idRefs ?? []) {
814
+ const target = resolveIdRefTarget(src, ctx);
815
+ if (target === null) return null;
816
+ targets.push(target);
817
+ }
818
+ return targets.length === 0 ? null : targets;
819
+ }
820
+ function assertSingleResource(targets) {
821
+ const resource = resourceOf(targets[0].parsed);
822
+ for (const g of targets) {
823
+ const r = resourceOf(g.parsed);
824
+ if (r.type !== resource.type || r.id !== resource.id)
825
+ throw new Error(
826
+ `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
827
+ );
828
+ }
829
+ return resource;
830
+ }
831
+ function bareIdRefs(targets) {
832
+ return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
833
+ }
834
+ function resolveMatchTypes(targets, authorTypes) {
835
+ if (authorTypes !== void 0) return authorTypes;
836
+ const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
837
+ return inferred.length > 0 ? inferred : void 0;
838
+ }
839
+ function resolveMetadata(metadata, ctx) {
840
+ const out = {};
841
+ for (const [k, src] of Object.entries(metadata ?? {}))
842
+ out[k] = resolveSource(src, ctx);
843
+ return out;
844
+ }
845
+ const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
846
+ function resolveGuard(guard, index, instance, stageId) {
847
+ const ctx = { instance, stageId }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
848
+ if (targets === null) return null;
849
+ const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
850
+ return { doc: compileGuard({
851
+ id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
852
+ resourceType: resource.type,
853
+ resourceId: resource.id,
854
+ owner: GUARD_OWNER,
855
+ ...guard.name !== void 0 ? { name: guard.name } : {},
856
+ ...guard.description !== void 0 ? { description: guard.description } : {},
857
+ match: {
858
+ ...types !== void 0 ? { types } : {},
859
+ idRefs: bareIdRefs(targets),
860
+ ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
861
+ actions: guard.match.actions
862
+ },
863
+ predicate: guard.predicate ?? "",
864
+ metadata: resolveMetadata(guard.metadata, ctx)
865
+ }), routeGdr: targets[0].parsed };
866
+ }
867
+ async function upsertGuard(client, doc) {
868
+ const existing = await client.getDocument(doc._id), set = {
869
+ resourceType: doc.resourceType,
870
+ resourceId: doc.resourceId,
871
+ owner: doc.owner,
872
+ match: doc.match,
873
+ predicate: doc.predicate,
874
+ metadata: doc.metadata
875
+ };
876
+ doc.name !== void 0 && (set.name = doc.name), doc.description !== void 0 && (set.description = doc.description), existing ? await client.patch(doc._id).set(set).commit() : await client.create(doc);
877
+ }
878
+ function resolvedStageGuards(args) {
879
+ const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
880
+ for (const [index, guard] of (stage?.guards ?? []).entries()) {
881
+ const resolved = resolveGuard(guard, index, args.instance, args.stageId);
882
+ resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
883
+ }
884
+ return out;
885
+ }
886
+ async function deployStageGuards(args) {
887
+ for (const { client, doc } of resolvedStageGuards(args))
888
+ await upsertGuard(client, doc);
889
+ }
890
+ async function retractStageGuards(args) {
891
+ for (const { client, doc } of resolvedStageGuards(args))
892
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
893
+ }
894
+ async function reconcileStageGuards(args) {
895
+ const { client, clientForGdr, instance, definition, exitedStageId, enteredStageId } = args;
896
+ exitedStageId !== void 0 && await retractStageGuards({
897
+ client,
898
+ clientForGdr,
899
+ instance,
900
+ definition,
901
+ stageId: exitedStageId
902
+ }), enteredStageId !== void 0 && await deployStageGuards({
903
+ client,
904
+ clientForGdr,
905
+ instance,
906
+ definition,
907
+ stageId: enteredStageId
908
+ });
909
+ }
910
+ async function discoverItems(args) {
911
+ const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
912
+ return client.fetch(groq, paramsForLake(params), fetchOptions);
913
+ }
914
+ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
915
+ if (!subjectGdrUri || !subjectGdrUri.includes(":")) return defaultClient;
916
+ try {
917
+ return clientForGdr(parseGdr(subjectGdrUri));
918
+ } catch {
919
+ return defaultClient;
920
+ }
921
+ }
922
+ const MAX_SPAWN_DEPTH = 6;
923
+ async function spawnChildren(ctx, mutation, task, spawn, actor) {
924
+ if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
925
+ const chain = [
926
+ ...ctx.instance.ancestors.map((a) => a.id),
927
+ gdrFromResource(ctx.instance.workflowResource, ctx.instance._id)
928
+ ].join(" \u2192 ");
929
+ throw new Error(
930
+ `Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
931
+ );
932
+ }
933
+ const rows = await resolveSpawnRows(ctx, spawn), refs = [];
934
+ for (const row of rows) {
935
+ const projected = projectSpawnRow(ctx.instance, spawn, row), { ref, body } = await prepareChildInstance({
936
+ client: ctx.client,
937
+ parent: ctx.instance,
938
+ task,
939
+ spawn,
940
+ initialState: projected.initialState,
941
+ effectsContext: projected.effectsContext,
942
+ actor
943
+ });
944
+ refs.push(ref), mutation.pendingCreates.push(body);
945
+ }
946
+ return refs;
947
+ }
948
+ async function resolveSpawnRows(ctx, spawn) {
949
+ const params = buildParams(ctx.instance), groq = spawn.forEach.groq;
950
+ let value;
951
+ if (groq.includes("*")) {
952
+ const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
953
+ value = await discoverItems({
954
+ client,
955
+ groq,
956
+ params,
957
+ ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
958
+ });
959
+ } else {
960
+ const tree = parse(groq, { params });
961
+ value = await (await evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
962
+ }
963
+ return Array.isArray(value) ? value : value == null ? [] : [value];
964
+ }
965
+ function firstDocRefGdrUri(slots) {
966
+ if (slots) {
967
+ for (const s of slots)
968
+ if (s._type === "workflow.state.doc.ref" && s.value !== null) return s.value.id;
969
+ }
970
+ }
971
+ function resolveSpawnSource(source, parent, row) {
972
+ switch (source.source) {
973
+ case "literal":
974
+ return source.value;
975
+ case "row":
976
+ return source.path !== void 0 ? getPath(row, source.path) : row;
977
+ case "parentState": {
978
+ const slot = parent.state?.find((s) => s.id === source.slotId);
979
+ return slot === void 0 ? null : source.path !== void 0 ? getPath(slot, source.path) : slot;
980
+ }
981
+ case "self":
982
+ return gdrFromResource(parent.workflowResource, parent._id);
983
+ case "stageId":
984
+ return parent.currentStageId;
985
+ case "now":
986
+ return (/* @__PURE__ */ new Date()).toISOString();
987
+ case "object": {
988
+ const out = {};
989
+ for (const [k, v] of Object.entries(source.fields))
990
+ out[k] = resolveSpawnSource(v, parent, row);
991
+ return out;
992
+ }
993
+ case "actor":
994
+ case "param":
995
+ case "stateRead":
996
+ case "effectOutput":
997
+ return null;
998
+ }
999
+ }
1000
+ function projectSpawnRow(parent, spawn, row) {
1001
+ const initialState = [];
1002
+ for (const entry of spawn.forEach.as.initialState ?? []) {
1003
+ const raw = resolveSpawnSource(entry.value, parent, row), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1004
+ initialState.push({
1005
+ type: entry.type,
1006
+ id: entry.id,
1007
+ value
1008
+ });
1009
+ }
1010
+ const effectsContext = {};
1011
+ for (const [k, src] of Object.entries(spawn.forEach.as.effectsContext ?? {})) {
1012
+ const v = resolveSpawnSource(src, parent, row);
1013
+ v != null && (typeof v == "string" || typeof v == "number" || typeof v == "boolean" || typeof v == "object" && "id" in v && "type" in v) && (effectsContext[k] = v);
1014
+ }
1015
+ return { initialState, effectsContext };
1016
+ }
1017
+ function canonicaliseSpawnValue(slotType, value, workflowResource) {
1018
+ return slotType === "workflow.state.doc.ref" ? coerceSpawnGdr(value, workflowResource) : slotType === "workflow.state.doc.refs" && Array.isArray(value) ? value.map((v) => coerceSpawnGdr(v, workflowResource)).filter((v) => v !== null) : value;
1019
+ }
1020
+ function coerceSpawnGdr(raw, workflowResource) {
1021
+ const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1022
+ if (raw == null) return null;
1023
+ if (typeof raw == "object" && "id" in raw && "type" in raw) {
1024
+ const r = raw;
1025
+ if (typeof r.id == "string" && typeof r.type == "string")
1026
+ return { id: toUri(r.id), type: r.type };
1027
+ }
1028
+ if (typeof raw == "object" && "_ref" in raw) {
1029
+ const r = raw;
1030
+ if (typeof r._ref == "string") return { id: toUri(r._ref), type: "document" };
1031
+ }
1032
+ return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1033
+ }
1034
+ async function resolveLogicalDefinition(client, ref, tags) {
1035
+ const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
1036
+ wantsExplicit && (params.version = ref.version);
1037
+ const groq = wantsExplicit ? '*[_type == "workflow.definition" && workflowId == $workflowId && version == $version && count(tags[@ in $tags]) > 0][0]' : '*[_type == "workflow.definition" && workflowId == $workflowId && count(tags[@ in $tags]) > 0] | order(version desc)[0]';
1038
+ return await client.fetch(groq, params) ?? null;
1039
+ }
1040
+ async function prepareChildInstance(args) {
1041
+ const { client, parent, task, spawn, initialState, effectsContext, actor } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1042
+ if (definition === null) {
1043
+ const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
1044
+ throw new Error(
1045
+ `Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
1046
+ );
1047
+ }
1048
+ const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: "workflow.instance" }, ancestors = [
1049
+ ...parent.ancestors,
1050
+ {
1051
+ id: gdrFromResource(parent.workflowResource, parent._id),
1052
+ type: "workflow.instance"
1053
+ }
1054
+ ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1055
+ slotDefs: definition.state,
1056
+ initialState,
1057
+ ctx: {
1058
+ client,
1059
+ selfId: childDocId,
1060
+ tags: childTags,
1061
+ workflowResource,
1062
+ ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1063
+ },
1064
+ randomKey
1065
+ }), childPerspective = derivePerspectiveFromState(childState) ?? inheritedPerspective, effectsContextEntries = Object.entries(effectsContext).map(
1066
+ ([key, value]) => {
1067
+ if (typeof value == "object" && value !== null && "id" in value && "type" in value) {
1068
+ if (!isGdrUri(value.id))
1069
+ throw new Error(
1070
+ `spawn.forEach.as.effectsContext["${key}"]: GDR id must be a "<scheme>:..." URI, got ${JSON.stringify(value.id)}.`
1071
+ );
1072
+ return effectsContextEntry(key, { id: value.id, type: value.type });
1073
+ }
1074
+ return effectsContextEntry(key, value);
1075
+ }
1076
+ ), base = buildInstanceBase({
1077
+ id: childDocId,
1078
+ now: (/* @__PURE__ */ new Date()).toISOString(),
1079
+ tags: childTags,
1080
+ workflowResource,
1081
+ workflowId: definition.workflowId,
1082
+ pinnedVersion: definition.version,
1083
+ definition,
1084
+ state: childState,
1085
+ effectsContext: effectsContextEntries,
1086
+ ancestors,
1087
+ perspective: childPerspective,
1088
+ initialStageId: definition.initialStageId,
1089
+ actor
1090
+ });
1091
+ return { ref: childRef, body: base };
1092
+ }
1093
+ async function checkSpawnCompletion(client, entry, spawn) {
1094
+ const refs = entry.spawnedInstances ?? [];
1095
+ if (refs.length === 0) {
1096
+ const policy2 = spawn.completionPolicy ?? { kind: "all" };
1097
+ return policy2.kind === "all" || policy2.kind === "count" && policy2.n === 0;
1098
+ }
1099
+ const ids = refs.map((r) => r.id.includes(":") ? gdrToBareId(r.id) : r.id), children = await client.fetch("*[_id in $ids]{_id, currentStageId, definitionSnapshot}", { ids });
1100
+ let terminalCount = 0;
1101
+ for (const child of children)
1102
+ JSON.parse(child.definitionSnapshot).stages.find((s) => s.id === child.currentStageId)?.kind === "terminal" && terminalCount++;
1103
+ const policy = spawn.completionPolicy ?? { kind: "all" };
1104
+ switch (policy.kind) {
1105
+ case "all":
1106
+ return terminalCount === refs.length;
1107
+ case "any":
1108
+ return terminalCount >= 1;
1109
+ case "count":
1110
+ return terminalCount >= policy.n;
1111
+ }
1112
+ }
1113
+ async function invokeTask(args) {
1114
+ const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId);
1115
+ return commitInvoke(ctx, taskId, options?.actor);
1116
+ }
1117
+ async function commitInvoke(ctx, taskId, actor) {
1118
+ const { task } = findTaskInCurrentStage(ctx, taskId), entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
1119
+ if (entry === void 0)
1120
+ throw new Error(`Task "${taskId}" has no status entry on instance ${ctx.instance._id}`);
1121
+ if (entry.status !== "pending")
1122
+ return { invoked: !1, taskId };
1123
+ const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1124
+ return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, taskId };
1125
+ }
1126
+ async function autoResolveOnActivate(ctx, mutation, task, entry) {
1127
+ const checks = [
1128
+ { filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
1129
+ { filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
1130
+ ];
1131
+ for (const { filter, outcome, systemId } of checks) {
1132
+ if (filter === void 0 || !await ctxEvaluateFilter(ctx, filter)) continue;
1133
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1134
+ return entry.status = outcome, entry.completedAt = completedAt, entry.completedBy = systemId, mutation.history.push(
1135
+ taskStatusChangedEntry({
1136
+ stageId: mutation.currentStageId,
1137
+ taskId: task.id,
1138
+ from: "active",
1139
+ to: outcome,
1140
+ actor: { kind: "system", id: systemId },
1141
+ at: completedAt
1142
+ })
1143
+ ), !0;
1144
+ }
1145
+ return !1;
1146
+ }
1147
+ async function activateTask(ctx, mutation, task, entry, actor) {
1148
+ if (entry.status = "active", entry.startedAt = (/* @__PURE__ */ new Date()).toISOString(), task.state !== void 0 && task.state.length > 0) {
1149
+ const stage = findStage(ctx.definition, mutation.currentStageId);
1150
+ entry.state = await resolveTaskStateSlots({
1151
+ client: ctx.client,
1152
+ instance: ctx.instance,
1153
+ stage,
1154
+ task
1155
+ });
1156
+ }
1157
+ if (!await autoResolveOnActivate(ctx, mutation, task, entry) && (mutation.history.push({
1158
+ _key: randomKey(),
1159
+ _type: "workflow.history.taskActivated",
1160
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1161
+ stageId: mutation.currentStageId,
1162
+ taskId: task.id,
1163
+ ...actor !== void 0 ? { actor } : {}
1164
+ }), await queueEffects(
1165
+ ctx,
1166
+ mutation,
1167
+ task.effects,
1168
+ {
1169
+ kind: "taskInvoke",
1170
+ id: task.id
1171
+ },
1172
+ actor
1173
+ ), task.spawns !== void 0)) {
1174
+ const refs = await spawnChildren(ctx, mutation, task, task.spawns, actor);
1175
+ entry.spawnedInstances = refs;
1176
+ for (const ref of refs)
1177
+ mutation.history.push({
1178
+ _key: randomKey(),
1179
+ _type: "workflow.history.spawned",
1180
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1181
+ taskId: task.id,
1182
+ instanceRef: ref
1183
+ });
1184
+ if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
1185
+ const previousStatus = entry.status;
1186
+ entry.status = "done", entry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1187
+ taskStatusChangedEntry({
1188
+ stageId: mutation.currentStageId,
1189
+ taskId: task.id,
1190
+ from: previousStatus,
1191
+ to: "done",
1192
+ ...actor !== void 0 ? { actor } : {}
1193
+ })
1194
+ );
1195
+ }
1196
+ }
1197
+ }
1198
+ async function buildStageTasks(ctx, stage) {
1199
+ const entries = [];
1200
+ for (const task of stage.tasks ?? []) {
1201
+ const inScope = await ctxEvaluateFilter(ctx, task.filter);
1202
+ entries.push({
1203
+ _key: randomKey(),
1204
+ id: task.id,
1205
+ status: inScope ? "pending" : "skipped",
1206
+ ...task.filter !== void 0 ? {
1207
+ filterEvaluation: {
1208
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1209
+ truthy: inScope
1210
+ }
1211
+ } : {}
1212
+ });
1213
+ }
1214
+ return entries;
1215
+ }
1216
+ function activatesOnStageEnter(task) {
1217
+ return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
1218
+ }
1219
+ async function setStage(args) {
1220
+ const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId);
1221
+ return commitSetStage(ctx, targetStageId, reason, options?.actor);
1222
+ }
1223
+ async function enterStage(ctx, mutation, nextStage, actor, at) {
1224
+ mutation.currentStageId = nextStage.id;
1225
+ const nextStageEntry = {
1226
+ _key: randomKey(),
1227
+ id: nextStage.id,
1228
+ enteredAt: at,
1229
+ state: await resolveStageStateSlots({
1230
+ client: ctx.client,
1231
+ instance: ctx.instance,
1232
+ stage: nextStage
1233
+ }),
1234
+ tasks: await buildStageTasks(ctx, nextStage)
1235
+ };
1236
+ mutation.stages.push(nextStageEntry), await queueEffects(
1237
+ ctx,
1238
+ mutation,
1239
+ nextStage.onEnter,
1240
+ { kind: "stageEnter", id: nextStage.id },
1241
+ actor
1242
+ );
1243
+ for (const task of nextStage.tasks ?? []) {
1244
+ if (!activatesOnStageEnter(task)) continue;
1245
+ const entry = nextStageEntry.tasks.find((t) => t.id === task.id);
1246
+ entry && entry.status === "pending" && await activateTask(ctx, mutation, task, entry, actor);
1247
+ }
1248
+ nextStage.kind === "terminal" && (mutation.completedAt = at);
1249
+ }
1250
+ async function beginStageMove(ctx, currentStage, actor) {
1251
+ const mutation = startMutation(ctx.instance), at = (/* @__PURE__ */ new Date()).toISOString();
1252
+ return await queueEffects(
1253
+ ctx,
1254
+ mutation,
1255
+ currentStage.onExit,
1256
+ { kind: "stageExit", id: currentStage.id },
1257
+ actor
1258
+ ), { mutation, at };
1259
+ }
1260
+ async function commitSetStage(ctx, targetStageId, reason, actor) {
1261
+ const currentStage = findStage(ctx.definition, ctx.instance.currentStageId), nextStage = findStage(ctx.definition, targetStageId);
1262
+ if (currentStage.id === nextStage.id)
1263
+ return { fired: !1 };
1264
+ const { mutation, at } = await beginStageMove(ctx, currentStage, actor), transitionId = `setStage:${currentStage.id}->${nextStage.id}`;
1265
+ mutation.history.push(
1266
+ ...stageTransitionHistory({
1267
+ fromStageId: currentStage.id,
1268
+ toStageId: nextStage.id,
1269
+ at,
1270
+ transitionId,
1271
+ via: "setStage",
1272
+ ...actor !== void 0 ? { actor } : {},
1273
+ ...reason !== void 0 ? { reason } : {}
1274
+ })
1275
+ );
1276
+ const priorEntry = findOpenStageEntry(mutation);
1277
+ return priorEntry !== void 0 && (priorEntry.exitedAt = at), await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.id, nextStage.id), {
1278
+ fired: !0,
1279
+ fromStageId: currentStage.id,
1280
+ toStageId: nextStage.id,
1281
+ transitionId
1282
+ };
1283
+ }
1284
+ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1285
+ await persist(ctx, mutation), await reconcileStageGuards({
1286
+ client: ctx.client,
1287
+ clientForGdr: ctx.clientForGdr,
1288
+ instance: ctx.instance,
1289
+ definition: ctx.definition,
1290
+ exitedStageId,
1291
+ enteredStageId
1292
+ });
1293
+ }
1294
+ async function commitTransition(ctx, action, actor) {
1295
+ const currentStage = findStage(ctx.definition, ctx.instance.currentStageId), transition = await pickTransition(ctx, currentStage);
1296
+ if (transition === void 0)
1297
+ return { fired: !1 };
1298
+ const { mutation, at } = await beginStageMove(ctx, currentStage, actor);
1299
+ await queueEffects(
1300
+ ctx,
1301
+ mutation,
1302
+ transition.effects,
1303
+ {
1304
+ kind: "transition",
1305
+ id: transition.id ?? `${currentStage.id}->${transition.to}`
1306
+ },
1307
+ actor
1308
+ ), mutation.history.push(
1309
+ ...stageTransitionHistory({
1310
+ fromStageId: currentStage.id,
1311
+ toStageId: transition.to,
1312
+ at,
1313
+ ...transition.id !== void 0 ? { transitionId: transition.id } : {},
1314
+ ...actor !== void 0 ? { actor } : {}
1315
+ })
1316
+ );
1317
+ const priorEntry = findOpenStageEntry(mutation);
1318
+ priorEntry !== void 0 && (priorEntry.exitedAt = at);
1319
+ const nextStage = findStage(ctx.definition, transition.to);
1320
+ return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.id, nextStage.id), {
1321
+ fired: !0,
1322
+ fromStageId: currentStage.id,
1323
+ toStageId: transition.to,
1324
+ ...transition.id !== void 0 ? { transitionId: transition.id } : {}
1325
+ };
1326
+ }
1327
+ async function pickTransition(ctx, stage, action) {
1328
+ const candidates = (stage.transitions ?? []).filter((t) => t.on === void 0);
1329
+ for (const candidate of candidates)
1330
+ if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
1331
+ }
1332
+ async function evaluateAutoTransitions(args) {
1333
+ const { client, instanceId, options, clientForGdr } = args, ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0);
1334
+ return commitTransition(ctx, void 0, options?.actor);
1335
+ }
1336
+ async function primeInitialStage(client, instanceId, actor, clientForGdr) {
1337
+ const instance = await client.getDocument(instanceId);
1338
+ if (!instance || instance.stages.length > 0 || instance.pendingEffects.length > 0) return;
1339
+ const definition = JSON.parse(instance.definitionSnapshot), stage = definition.stages.find((s) => s.id === instance.currentStageId);
1340
+ if (stage === void 0) return;
1341
+ const snapshot = await hydrateSnapshot({
1342
+ client,
1343
+ clientForGdr: clientForGdr ?? (() => client),
1344
+ instance,
1345
+ definition
1346
+ }), tasks = [];
1347
+ for (const task of stage.tasks ?? []) {
1348
+ const inScope = await evaluateFilter({
1349
+ filter: task.filter,
1350
+ definition,
1351
+ snapshot,
1352
+ params: buildParams(instance)
1353
+ });
1354
+ tasks.push({
1355
+ _key: randomKey(),
1356
+ id: task.id,
1357
+ status: inScope ? "pending" : "skipped"
1358
+ });
1359
+ }
1360
+ const initialStageEntry = {
1361
+ _key: randomKey(),
1362
+ id: stage.id,
1363
+ enteredAt: (/* @__PURE__ */ new Date()).toISOString(),
1364
+ state: await resolveStageStateSlots({ client, instance, stage }),
1365
+ tasks
1366
+ }, pendingEffects = [], newHistory = [...instance.history];
1367
+ for (const effect of stage.onEnter ?? []) {
1368
+ const params = await resolveBindings({
1369
+ bindings: effect.bindings,
1370
+ staticInput: effect.input,
1371
+ instance,
1372
+ ...actor !== void 0 ? { actor } : {}
1373
+ }), { pending, history } = buildQueuedEffect(
1374
+ effect,
1375
+ { kind: "stageEnter", id: stage.id },
1376
+ params,
1377
+ actor
1378
+ );
1379
+ pendingEffects.push(pending), newHistory.push(history);
1380
+ }
1381
+ await client.patch(instance._id).set({
1382
+ stages: [initialStageEntry],
1383
+ pendingEffects,
1384
+ history: newHistory,
1385
+ lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1386
+ }).ifRevisionId(instance._rev).commit(), await deployStageGuards({
1387
+ client,
1388
+ clientForGdr: clientForGdr ?? (() => client),
1389
+ instance,
1390
+ definition,
1391
+ stageId: stage.id
1392
+ }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr);
1393
+ }
1394
+ async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr) {
1395
+ const resolvedClientForGdr = clientForGdr ?? (() => client);
1396
+ for (const task of stage.tasks ?? []) {
1397
+ if (!activatesOnStageEnter(task)) continue;
1398
+ const updated = await client.getDocument(instanceId);
1399
+ if (!updated) continue;
1400
+ const updatedCtx = await buildEngineContext({
1401
+ client,
1402
+ clientForGdr: resolvedClientForGdr,
1403
+ instance: updated,
1404
+ definition
1405
+ }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1406
+ mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
1407
+ }
1408
+ }
1409
+ const CASCADE_LIMIT = 100;
1410
+ async function gatherAutoResolveCandidates(ctx, tasks) {
1411
+ const currentTasksList = findCurrentTasks(ctx.instance), candidates = [];
1412
+ for (const task of tasks)
1413
+ if (currentTasksList.find((e) => e.id === task.id)?.status === "active") {
1414
+ if (task.failWhen !== void 0 && await ctxEvaluateFilter(ctx, task.failWhen)) {
1415
+ candidates.push({ task, outcome: "failed" });
1416
+ continue;
1417
+ }
1418
+ task.completeWhen !== void 0 && await ctxEvaluateFilter(ctx, task.completeWhen) && candidates.push({ task, outcome: "done" });
1419
+ }
1420
+ return candidates;
1421
+ }
1422
+ async function resolveCompleteWhen(client, instanceId, clientForGdr) {
1423
+ const ctx = await loadContext(client, instanceId, clientForGdr ? { clientForGdr } : void 0), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1424
+ (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
1425
+ );
1426
+ if (tasksWithAutoPredicate.length === 0) return 0;
1427
+ const candidates = await gatherAutoResolveCandidates(ctx, tasksWithAutoPredicate);
1428
+ if (candidates.length === 0) return 0;
1429
+ const mutation = startMutation(ctx.instance);
1430
+ let count = 0;
1431
+ for (const { task, outcome } of candidates) {
1432
+ const mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1433
+ if (mutEntry === void 0 || mutEntry.status !== "active") continue;
1434
+ const previousStatus = mutEntry.status, actor = {
1435
+ kind: "system",
1436
+ id: outcome === "failed" ? "engine.failWhen" : "engine.completeWhen"
1437
+ };
1438
+ mutEntry.status = outcome, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutEntry.completedBy = actor.id, mutation.history.push(
1439
+ taskStatusChangedEntry({
1440
+ stageId: stage.id,
1441
+ taskId: task.id,
1442
+ from: previousStatus,
1443
+ to: outcome,
1444
+ actor
1445
+ })
1446
+ ), count++;
1447
+ }
1448
+ return count > 0 && await persist(ctx, mutation), count;
1449
+ }
1450
+ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr) {
1451
+ let count = 0;
1452
+ for (; ; ) {
1453
+ if (await resolveCompleteWhen(client, instanceId, clientForGdr), !(await evaluateAutoTransitions({
1454
+ client,
1455
+ instanceId,
1456
+ ...actor !== void 0 ? { options: { actor } } : {},
1457
+ ...clientForGdr ? { clientForGdr } : {}
1458
+ })).fired) return count;
1459
+ if (count++, count > CASCADE_LIMIT)
1460
+ throw new Error(
1461
+ `Cascade did not stabilise after ${CASCADE_LIMIT} auto-transitions on ${instanceId} \u2014 possible infinite loop`
1462
+ );
1463
+ }
1464
+ }
1465
+ async function propagateToAncestors(client, instanceId, actor, clientForGdr) {
1466
+ const instance = await client.getDocument(instanceId);
1467
+ if (!instance || instance.ancestors.length === 0) return;
1468
+ const parentRef = instance.ancestors[instance.ancestors.length - 1];
1469
+ if (parentRef === void 0) return;
1470
+ const parentId = gdrToBareId(parentRef.id), parent = await client.getDocument(parentId);
1471
+ if (!parent || parent._type !== "workflow.instance" || typeof parent.definitionSnapshot != "string")
1472
+ return;
1473
+ const parentDefinition = JSON.parse(parent.definitionSnapshot), parentStage = parentDefinition.stages.find((s) => s.id === parent.currentStageId);
1474
+ if (parentStage === void 0) return;
1475
+ const parentCurrentTasks = findCurrentTasks(parent), parentTask = (parentStage.tasks ?? []).find((t) => t.spawns === void 0 ? !1 : parentCurrentTasks.find((e) => e.id === t.id)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === instance._id) ?? !1);
1476
+ if (parentTask === void 0) return;
1477
+ const parentEntry = parentCurrentTasks.find((e) => e.id === parentTask.id);
1478
+ if (parentEntry === void 0 || parentEntry.status === "done" || !await checkSpawnCompletion(client, parentEntry, parentTask.spawns)) return;
1479
+ const ctx = await buildEngineContext({
1480
+ client,
1481
+ clientForGdr: clientForGdr ?? (() => client),
1482
+ instance: parent,
1483
+ definition: parentDefinition
1484
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === parentTask.id);
1485
+ if (mutEntry === void 0) return;
1486
+ const previousStatus = mutEntry.status;
1487
+ mutEntry.status = "done", mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), mutation.history.push(
1488
+ taskStatusChangedEntry({
1489
+ stageId: parentStage.id,
1490
+ taskId: parentTask.id,
1491
+ from: previousStatus,
1492
+ to: "done",
1493
+ ...actor !== void 0 ? { actor } : {}
1494
+ })
1495
+ ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parentId, actor, clientForGdr), await propagateToAncestors(client, parentId, actor, clientForGdr);
1496
+ }
1497
+ function startMutation(instance) {
1498
+ return {
1499
+ currentStageId: instance.currentStageId,
1500
+ // Deep-ish copy. Per-scope state slot arrays need their own copies
1501
+ // so ops can mutate without leaking into the source.
1502
+ state: (instance.state ?? []).map((s) => ({ ...s })),
1503
+ stages: instance.stages.map((s) => ({
1504
+ ...s,
1505
+ state: (s.state ?? []).map((slot) => ({ ...slot })),
1506
+ tasks: s.tasks.map((t) => ({
1507
+ ...t,
1508
+ ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1509
+ }))
1510
+ })),
1511
+ pendingEffects: [...instance.pendingEffects],
1512
+ effectHistory: [...instance.effectHistory],
1513
+ effectsContext: [...instance.effectsContext],
1514
+ history: [...instance.history],
1515
+ lastChangedAt: instance.lastChangedAt,
1516
+ ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1517
+ pendingCreates: []
1518
+ };
1519
+ }
1520
+ function taskStatusChangedEntry(args) {
1521
+ return {
1522
+ _key: randomKey(),
1523
+ _type: "workflow.history.taskStatusChanged",
1524
+ at: args.at ?? (/* @__PURE__ */ new Date()).toISOString(),
1525
+ stageId: args.stageId,
1526
+ taskId: args.taskId,
1527
+ from: args.from,
1528
+ to: args.to,
1529
+ ...args.actor !== void 0 ? { actor: args.actor } : {}
1530
+ };
1531
+ }
1532
+ function stageTransitionHistory(args) {
1533
+ const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1534
+ at,
1535
+ ...transitionId !== void 0 ? { transitionId } : {},
1536
+ ...actor !== void 0 ? { actor } : {},
1537
+ ...via !== void 0 ? { via } : {},
1538
+ ...reason !== void 0 ? { reason } : {}
1539
+ };
1540
+ return [
1541
+ {
1542
+ _key: randomKey(),
1543
+ _type: "workflow.history.stageExited",
1544
+ stageId: fromStageId,
1545
+ toStageId,
1546
+ ...shared
1547
+ },
1548
+ {
1549
+ _key: randomKey(),
1550
+ _type: "workflow.history.transitionFired",
1551
+ fromStageId,
1552
+ toStageId,
1553
+ ...shared
1554
+ },
1555
+ {
1556
+ _key: randomKey(),
1557
+ _type: "workflow.history.stageEntered",
1558
+ stageId: toStageId,
1559
+ fromStageId,
1560
+ ...shared
1561
+ }
1562
+ ];
1563
+ }
1564
+ async function persist(ctx, mutation) {
1565
+ const set = {
1566
+ currentStageId: mutation.currentStageId,
1567
+ state: mutation.state,
1568
+ stages: mutation.stages,
1569
+ pendingEffects: mutation.pendingEffects,
1570
+ effectHistory: mutation.effectHistory,
1571
+ effectsContext: mutation.effectsContext,
1572
+ history: mutation.history,
1573
+ lastChangedAt: (/* @__PURE__ */ new Date()).toISOString()
1574
+ };
1575
+ mutation.completedAt !== void 0 && (set.completedAt = mutation.completedAt);
1576
+ const pendingCreates = mutation.pendingCreates;
1577
+ if (pendingCreates.length === 0)
1578
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1579
+ const tx = ctx.client.transaction();
1580
+ for (const body of pendingCreates) tx.create(body);
1581
+ tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1582
+ const actorForPriming = void 0;
1583
+ for (const body of pendingCreates)
1584
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr);
1585
+ const reloaded = await ctx.client.getDocument(ctx.instance._id);
1586
+ if (!reloaded)
1587
+ throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1588
+ return reloaded;
1589
+ }
1590
+ function findOpenStageEntry(host) {
1591
+ return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
1592
+ }
1593
+ function currentStageEntry(mutation) {
1594
+ const entry = findOpenStageEntry(mutation);
1595
+ if (entry === void 0)
1596
+ throw new Error(
1597
+ `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
1598
+ );
1599
+ return entry;
1600
+ }
1601
+ function currentTasks(mutation) {
1602
+ return currentStageEntry(mutation).tasks;
1603
+ }
1604
+ function findTaskInCurrentStage(ctx, taskId) {
1605
+ const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1606
+ if (task === void 0)
1607
+ throw new Error(
1608
+ `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1609
+ );
1610
+ return { stage, task };
1611
+ }
1612
+ function requireMutationTaskEntry(mutation, taskId) {
1613
+ const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1614
+ if (mutEntry === void 0)
1615
+ throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1616
+ return mutEntry;
1617
+ }
1618
+ function findCurrentStageEntry(instance) {
1619
+ return findOpenStageEntry(instance);
1620
+ }
1621
+ function findCurrentTasks(instance) {
1622
+ return findCurrentStageEntry(instance)?.tasks ?? [];
1623
+ }
1624
+ async function completeEffect(args) {
1625
+ const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId);
1626
+ return commitCompleteEffect(
1627
+ ctx,
1628
+ effectKey,
1629
+ status,
1630
+ outputs,
1631
+ detail,
1632
+ error,
1633
+ durationMs,
1634
+ options?.actor
1635
+ );
1636
+ }
1637
+ function buildEffectHistoryEntry(pending, outcome) {
1638
+ const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome;
1639
+ return {
1640
+ _key: pending._key,
1641
+ effectId: pending.effectId,
1642
+ ...pending.title !== void 0 ? { title: pending.title } : {},
1643
+ ...pending.description !== void 0 ? { description: pending.description } : {},
1644
+ params: pending.params,
1645
+ origin: pending.origin,
1646
+ ...actor !== void 0 ? { actor } : pending.actor !== void 0 ? { actor: pending.actor } : {},
1647
+ ranAt,
1648
+ ...durationMs !== void 0 ? { durationMs } : {},
1649
+ status,
1650
+ ...detail !== void 0 ? { detail } : {},
1651
+ ...error !== void 0 ? { error } : {},
1652
+ ...outputs !== void 0 ? { outputs } : {}
1653
+ };
1654
+ }
1655
+ function upsertEffectsContext(mutation, outputs) {
1656
+ for (const [id, value] of Object.entries(outputs)) {
1657
+ const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1658
+ existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
1659
+ }
1660
+ }
1661
+ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1662
+ const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1663
+ if (pending === void 0)
1664
+ throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1665
+ const mutation = startMutation(ctx.instance);
1666
+ mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1667
+ const ranAt = (/* @__PURE__ */ new Date()).toISOString();
1668
+ return mutation.effectHistory.push(
1669
+ buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1670
+ ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1671
+ _key: randomKey(),
1672
+ _type: "workflow.history.effectCompleted",
1673
+ at: ranAt,
1674
+ effectKey,
1675
+ effectId: pending.effectId,
1676
+ status,
1677
+ ...outputs !== void 0 ? { outputs } : {},
1678
+ ...detail !== void 0 ? { detail } : {},
1679
+ ...actor !== void 0 ? { actor } : {}
1680
+ }), await persist(ctx, mutation), { effectKey, status };
1681
+ }
1682
+ function buildQueuedEffect(effect, origin, params, actor) {
1683
+ const key = randomKey(), pending = {
1684
+ _key: key,
1685
+ _type: "workflow.pendingEffect",
1686
+ effectId: effect.id,
1687
+ ...effect.title !== void 0 ? { title: effect.title } : {},
1688
+ ...effect.description !== void 0 ? { description: effect.description } : {},
1689
+ ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1690
+ params,
1691
+ origin,
1692
+ ...actor !== void 0 ? { actor } : {},
1693
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
1694
+ }, history = {
1695
+ _key: randomKey(),
1696
+ _type: "workflow.history.effectQueued",
1697
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1698
+ effectKey: key,
1699
+ effectId: effect.id,
1700
+ origin
1701
+ };
1702
+ return { pending, history };
1703
+ }
1704
+ async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1705
+ if (!effects) return;
1706
+ const liveInstance = {
1707
+ ...ctx.instance,
1708
+ state: mutation.state,
1709
+ stages: mutation.stages,
1710
+ effectsContext: mutation.effectsContext
1711
+ };
1712
+ for (const effect of effects) {
1713
+ const params = await resolveBindings({
1714
+ bindings: effect.bindings,
1715
+ staticInput: effect.input,
1716
+ instance: liveInstance,
1717
+ ...callerParams !== void 0 ? { params: callerParams } : {},
1718
+ ...actor !== void 0 ? { actor } : {}
1719
+ }), { pending, history } = buildQueuedEffect(effect, origin, params, actor);
1720
+ mutation.pendingEffects.push(pending), mutation.history.push(history);
1721
+ }
1722
+ }
1723
+ class ActionParamsInvalidError extends Error {
1724
+ action;
1725
+ taskId;
1726
+ issues;
1727
+ constructor(args) {
1728
+ const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1729
+ `);
1730
+ super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
1731
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
1732
+ }
1733
+ }
1734
+ function validateActionParams(action, taskId, callerParams) {
1735
+ const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
1736
+ for (const decl of declared) {
1737
+ const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
1738
+ if (decl.required === !0 && !present) {
1739
+ issues.push({ paramId: decl.id, reason: "required but missing" });
1740
+ continue;
1741
+ }
1742
+ present && (checkParamType(value, decl) || issues.push({
1743
+ paramId: decl.id,
1744
+ reason: `expected paramType=${decl.paramType}, got ${typeof value}`
1745
+ }));
1746
+ }
1747
+ if (issues.length > 0)
1748
+ throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
1749
+ return params;
1750
+ }
1751
+ function hasStringField(value, field) {
1752
+ return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
1753
+ }
1754
+ function checkParamType(value, decl) {
1755
+ switch (decl.paramType) {
1756
+ case "string":
1757
+ case "url":
1758
+ case "dateTime":
1759
+ return typeof value == "string";
1760
+ case "number":
1761
+ return typeof value == "number" && Number.isFinite(value);
1762
+ case "boolean":
1763
+ return typeof value == "boolean";
1764
+ case "actor":
1765
+ return hasStringField(value, "kind");
1766
+ case "doc.ref":
1767
+ return hasStringField(value, "id");
1768
+ case "doc.refs":
1769
+ return Array.isArray(value) && value.every((v) => checkParamType(v, { paramType: "doc.ref" }));
1770
+ case "json":
1771
+ return !0;
1772
+ default:
1773
+ return !1;
1774
+ }
1775
+ }
1776
+ async function runActionOps(args) {
1777
+ const { ops, mutation, stageId, taskId, actionName, params, actor } = args;
1778
+ if (ops === void 0 || ops.length === 0) return [];
1779
+ const summaries = [];
1780
+ for (const op of ops) {
1781
+ const summary = applyOp(op, { mutation, stageId, taskId, params, actor });
1782
+ summaries.push(summary), mutation.history.push({
1783
+ _key: randomKey(),
1784
+ _type: "workflow.history.opApplied",
1785
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1786
+ stageId,
1787
+ taskId,
1788
+ actionId: actionName,
1789
+ opType: summary.opType,
1790
+ ...summary.target !== void 0 ? { target: summary.target } : {},
1791
+ ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
1792
+ ...actor !== void 0 ? { actor } : {}
1793
+ });
1794
+ }
1795
+ return summaries;
1796
+ }
1797
+ function applyOp(op, ctx) {
1798
+ switch (op.type) {
1799
+ case "workflow.op.state.set":
1800
+ return applyStateSet(op, ctx);
1801
+ case "workflow.op.state.append":
1802
+ return applyStateAppend(op, ctx);
1803
+ case "workflow.op.state.updateWhere":
1804
+ return applyStateUpdateWhere(op, ctx);
1805
+ case "workflow.op.state.removeWhere":
1806
+ return applyStateRemoveWhere(op, ctx);
1807
+ case "workflow.op.status.set":
1808
+ return applyStatusSet(op, ctx);
1809
+ }
1810
+ }
1811
+ function applyStateSet(op, ctx) {
1812
+ const value = resolveOpSource(op.value, ctx), slot = locateSlot(
1813
+ ctx,
1814
+ op.target,
1815
+ /*createIfMissing*/
1816
+ !0
1817
+ );
1818
+ return slot !== void 0 && (validateSlotValue({ slotType: slot._type, slotId: slot.id, value }), slot.value = value), { opType: op.type, target: op.target, resolved: { value } };
1819
+ }
1820
+ function applyStateAppend(op, ctx) {
1821
+ const item = resolveOpSource(op.item, ctx), slot = locateSlot(
1822
+ ctx,
1823
+ op.target,
1824
+ /*createIfMissing*/
1825
+ !0
1826
+ );
1827
+ if (slot !== void 0) {
1828
+ validateSlotAppendItem({ slotType: slot._type, slotId: slot.id, item });
1829
+ const current = Array.isArray(slot.value) ? slot.value : [];
1830
+ slot.value = [...current, withRowKey(item)];
1831
+ }
1832
+ return { opType: op.type, target: op.target, resolved: { item } };
1833
+ }
1834
+ function withRowKey(item) {
1835
+ return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
1836
+ }
1837
+ function applyStateUpdateWhere(op, ctx) {
1838
+ const slot = locateSlot(
1839
+ ctx,
1840
+ op.target,
1841
+ /*createIfMissing*/
1842
+ !1
1843
+ ), resolvedMerge = {};
1844
+ for (const [k, v] of Object.entries(op.merge))
1845
+ resolvedMerge[k] = resolveOpSource(v, ctx);
1846
+ return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
1847
+ (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
1848
+ )), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
1849
+ }
1850
+ function applyStateRemoveWhere(op, ctx) {
1851
+ const slot = locateSlot(
1852
+ ctx,
1853
+ op.target,
1854
+ /*createIfMissing*/
1855
+ !1
1856
+ );
1857
+ return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.filter(
1858
+ (row) => !evalOpPredicate(op.where, row, ctx)
1859
+ )), { opType: op.type, target: op.target };
1860
+ }
1861
+ function applyStatusSet(op, ctx) {
1862
+ const entry = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.id === op.taskId);
1863
+ return entry !== void 0 && (entry.status = op.status), { opType: op.type, resolved: { taskId: op.taskId, status: op.status } };
1864
+ }
1865
+ function locateSlot(ctx, target, createIfMissing) {
1866
+ let host;
1867
+ if (target.scope === "workflow")
1868
+ host = ctx.mutation.state;
1869
+ else if (target.scope === "stage")
1870
+ host = findOpenStageEntry(ctx.mutation)?.state;
1871
+ else {
1872
+ const task = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.id === ctx.taskId);
1873
+ host = task?.state, host === void 0 && task !== void 0 && createIfMissing && (task.state = [], host = task.state);
1874
+ }
1875
+ if (host === void 0)
1876
+ throw new Error(
1877
+ `Op target scope "${target.scope}" has no state container on this instance \u2014 declare the slot or check stage/task identity`
1878
+ );
1879
+ let slot = host.find((s) => s.id === target.slotId);
1880
+ return slot === void 0 && createIfMissing && (slot = {
1881
+ _key: randomKey(),
1882
+ _type: "workflow.state.value.string",
1883
+ id: target.slotId,
1884
+ value: null
1885
+ }, host.push(slot)), slot;
1886
+ }
1887
+ function resolveOpSource(src, ctx) {
1888
+ switch (src.source) {
1889
+ case "literal":
1890
+ return src.value;
1891
+ case "param":
1892
+ return ctx.params[src.paramId];
1893
+ case "actor":
1894
+ return ctx.actor;
1895
+ case "now":
1896
+ return (/* @__PURE__ */ new Date()).toISOString();
1897
+ case "self":
1898
+ return;
1899
+ // Not applicable inside ops; reserved for higher-level binding.
1900
+ case "stageId":
1901
+ return ctx.stageId;
1902
+ case "stateRead": {
1903
+ const value = readSlotFromMutation(ctx, src.scope, src.slotId);
1904
+ return src.path !== void 0 ? getPath(value, src.path) : value;
1905
+ }
1906
+ case "effectOutput": {
1907
+ const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
1908
+ if (entry === void 0) return null;
1909
+ const v = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
1910
+ return v === void 0 ? null : v;
1911
+ }
1912
+ case "object": {
1913
+ const out = {};
1914
+ for (const [field, fieldSrc] of Object.entries(src.fields))
1915
+ out[field] = resolveOpSource(fieldSrc, ctx);
1916
+ return out;
1917
+ }
1918
+ }
1919
+ }
1920
+ function slotValue(slots, slotId) {
1921
+ return slots?.find((s) => s.id === slotId)?.value;
1922
+ }
1923
+ function readSlotFromMutation(ctx, scope, slotId) {
1924
+ if (scope === "workflow") return slotValue(ctx.mutation.state, slotId);
1925
+ const stageEntry = findOpenStageEntry(ctx.mutation);
1926
+ if (scope === "stage") return slotValue(stageEntry?.state, slotId);
1927
+ const task = stageEntry?.tasks.find((t) => t.id === ctx.taskId);
1928
+ return slotValue(task?.state, slotId);
1929
+ }
1930
+ function evalOpPredicate(p, row, ctx) {
1931
+ if ("all" in p) return p.all.every((sub) => evalOpPredicate(sub, row, ctx));
1932
+ if ("any" in p) return p.any.some((sub) => evalOpPredicate(sub, row, ctx));
1933
+ const target = resolveOpSource(p.equals, ctx);
1934
+ return row[p.field] === target;
1935
+ }
1936
+ async function fireAction(args) {
1937
+ const { client, instanceId, taskId, action, params, options } = args, ctx = await loadContext(client, instanceId);
1938
+ return commitAction(ctx, taskId, action, params, options?.actor);
1939
+ }
1940
+ async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
1941
+ const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
1942
+ if (action === void 0)
1943
+ throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
1944
+ if (!await ctxEvaluateFilter(ctx, action.filter))
1945
+ throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
1946
+ const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
1947
+ if (entry === void 0 || entry.status !== "active")
1948
+ throw new Error(
1949
+ `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
1950
+ );
1951
+ const params = validateActionParams(action, taskId, callerParams);
1952
+ return { stage, task, action, params };
1953
+ }
1954
+ function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor) {
1955
+ if (action.setStatus === void 0) return;
1956
+ const initialStatus = mutEntry.status, newStatus = action.setStatus;
1957
+ return mutEntry.status = newStatus, mutEntry.completedAt = (/* @__PURE__ */ new Date()).toISOString(), actor && (mutEntry.completedBy = actor.id), mutation.history.push(
1958
+ taskStatusChangedEntry({
1959
+ stageId,
1960
+ taskId,
1961
+ from: initialStatus,
1962
+ to: newStatus,
1963
+ ...actor !== void 0 ? { actor } : {}
1964
+ })
1965
+ ), newStatus;
1966
+ }
1967
+ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
1968
+ const { stage, task, action, params } = await resolveActionCommit(
1969
+ ctx,
1970
+ taskId,
1971
+ actionName,
1972
+ callerParams
1973
+ ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1974
+ mutation.history.push({
1975
+ _key: randomKey(),
1976
+ _type: "workflow.history.actionFired",
1977
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1978
+ stageId: stage.id,
1979
+ taskId,
1980
+ actionId: actionName,
1981
+ ...actor !== void 0 ? { actor } : {}
1982
+ });
1983
+ const ranOps = await runActionOps({
1984
+ ops: action.ops,
1985
+ mutation,
1986
+ stageId: stage.id,
1987
+ taskId,
1988
+ actionName,
1989
+ params,
1990
+ actor
1991
+ }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor);
1992
+ return await queueEffects(
1993
+ ctx,
1994
+ mutation,
1995
+ action.effects,
1996
+ {
1997
+ kind: "action",
1998
+ id: `${task.id}:${actionName}`
1999
+ },
2000
+ actor,
2001
+ params
2002
+ ), await persist(ctx, mutation), {
2003
+ fired: !0,
2004
+ taskId,
2005
+ action: actionName,
2006
+ ...newStatus !== void 0 ? { newStatus } : {},
2007
+ ...ranOps.length > 0 ? { ranOps } : {}
2008
+ };
2009
+ }
2010
+ const parsedFilters = /* @__PURE__ */ new Map();
2011
+ async function matchesFilter(args) {
2012
+ const { document, filter, userId } = args;
2013
+ parsedFilters.has(filter) || parsedFilters.set(filter, parse(`*[${filter}]`));
2014
+ const ast = parsedFilters.get(filter), data = await (await evaluate(ast, {
2015
+ dataset: [document],
2016
+ ...userId !== void 0 ? { identity: userId } : {}
2017
+ })).get();
2018
+ return Array.isArray(data) && data.length === 1;
2019
+ }
2020
+ async function grantsPermissionOn(args) {
2021
+ const { document, grants, permission, userId } = args;
2022
+ if (!document || grants.length === 0) return !1;
2023
+ for (const grant of grants)
2024
+ if (grant.permissions.includes(permission) && await matchesFilter({
2025
+ document,
2026
+ filter: grant.filter,
2027
+ ...userId !== void 0 ? { userId } : {}
2028
+ }))
2029
+ return !0;
2030
+ return !1;
2031
+ }
2032
+ async function fetchGrants(args) {
2033
+ const { client, resourcePath, signal } = args;
2034
+ return client.request({ url: resourcePath, ...signal ? { signal } : {} });
2035
+ }
2036
+ const actorCache = /* @__PURE__ */ new WeakMap(), grantsCache = /* @__PURE__ */ new WeakMap();
2037
+ async function resolveAccess(client, args = {}) {
2038
+ const overrideActor = args.override?.actor, overrideGrants = args.override?.grants;
2039
+ if (overrideActor !== void 0 && overrideGrants !== void 0)
2040
+ return { actor: overrideActor, grants: overrideGrants };
2041
+ const requestFn = client.request;
2042
+ if (requestFn === void 0) {
2043
+ if (overrideActor === void 0)
2044
+ throw new Error(
2045
+ "workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ url: '/users/me' })`. Supply a real `@sanity/client` configured with a token, or pass an explicit `access.actor` override (the test bench provides one by default)."
2046
+ );
2047
+ return { actor: overrideActor };
2048
+ }
2049
+ const actorPromise = overrideActor !== void 0 ? Promise.resolve(overrideActor) : cachedActor(client, requestFn), grantsPromise = overrideGrants !== void 0 ? Promise.resolve(overrideGrants) : args.grantsFromPath !== void 0 ? cachedGrants(client, requestFn, args.grantsFromPath) : Promise.resolve(void 0), [actor, grants] = await Promise.all([actorPromise, grantsPromise]);
2050
+ if (actor === void 0)
2051
+ throw new Error(
2052
+ "workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity \u2014 check the token, or pass `access.actor` explicitly."
2053
+ );
2054
+ return { actor, ...grants !== void 0 ? { grants } : {} };
2055
+ }
2056
+ function cachedActor(client, requestFn) {
2057
+ const cached = actorCache.get(client);
2058
+ if (cached !== void 0) return cached;
2059
+ const pending = fetchActor(requestFn).catch((err) => {
2060
+ throw actorCache.get(client) === pending && actorCache.delete(client), err;
2061
+ });
2062
+ return actorCache.set(client, pending), pending;
2063
+ }
2064
+ function cachedGrants(client, requestFn, resourcePath) {
2065
+ let byPath = grantsCache.get(client);
2066
+ byPath === void 0 && (byPath = /* @__PURE__ */ new Map(), grantsCache.set(client, byPath));
2067
+ let cached = byPath.get(resourcePath);
2068
+ return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath), byPath.set(resourcePath, cached)), cached;
2069
+ }
2070
+ async function fetchActor(requestFn) {
2071
+ let user;
2072
+ try {
2073
+ user = await requestFn({
2074
+ uri: "/users/me",
2075
+ tag: "workflow.access.resolve-actor"
2076
+ });
2077
+ } catch (err) {
2078
+ throw new Error(
2079
+ 'workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity, or pass an explicit `access.actor` override.',
2080
+ { cause: err }
2081
+ );
2082
+ }
2083
+ if (!user || typeof user.id != "string" || user.id.length === 0) return;
2084
+ const roleNames = user.roles?.map((r) => r.name).filter((n) => !!n) ?? [];
2085
+ return {
2086
+ kind: "user",
2087
+ id: user.id,
2088
+ ...roleNames.length > 0 ? { roles: roleNames } : {}
2089
+ };
2090
+ }
2091
+ async function fetchGrantsCached(requestFn, resourcePath) {
2092
+ try {
2093
+ return await fetchGrants({ client: { request: requestFn }, resourcePath });
2094
+ } catch (err) {
2095
+ console.warn(
2096
+ `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2097
+ );
2098
+ return;
2099
+ }
2100
+ }
2101
+ const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
2102
+ function validateTags(tags) {
2103
+ if (tags.length === 0)
2104
+ throw new Error("tags: must be a non-empty array");
2105
+ for (const tag of tags)
2106
+ if (!TAG_RE.test(tag))
2107
+ throw new Error(
2108
+ `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
2109
+ );
2110
+ }
2111
+ function canonicalTag(tags) {
2112
+ return tags[0];
2113
+ }
2114
+ const WILDCARD_ROLE = "*";
2115
+ async function evaluateInstance(args) {
2116
+ const { client, tags, instanceId, resourceClients } = args;
2117
+ validateTags(tags);
2118
+ const { actor, grants } = await resolveAccess(client, {
2119
+ ...args.access !== void 0 ? { override: args.access } : {},
2120
+ ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2121
+ }), instance = await client.getDocument(instanceId);
2122
+ if (!instance)
2123
+ throw new Error(`Workflow instance "${instanceId}" not found`);
2124
+ if (!tags.some((t) => instance.tags?.includes(t)))
2125
+ throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2126
+ const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2127
+ if (stage === void 0)
2128
+ throw new Error(
2129
+ `Instance "${instanceId}" currentStageId "${instance.currentStageId}" not in definition`
2130
+ );
2131
+ const snapshot = await hydrateSnapshot({
2132
+ client,
2133
+ clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client,
2134
+ instance,
2135
+ definition
2136
+ }), currentTaskEntries = instance.stages.find(
2137
+ (s) => s.id === instance.currentStageId && s.exitedAt === void 0
2138
+ )?.tasks ?? [], taskEvaluations = [];
2139
+ for (const task of stage.tasks ?? [])
2140
+ taskEvaluations.push(
2141
+ await evaluateTask({
2142
+ task,
2143
+ statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2144
+ instance,
2145
+ definition,
2146
+ actor,
2147
+ grants,
2148
+ snapshot
2149
+ })
2150
+ );
2151
+ const transitionEvaluations = [];
2152
+ for (const transition of stage.transitions ?? [])
2153
+ transitionEvaluations.push({
2154
+ transition,
2155
+ filterSatisfied: await evaluateFilter({
2156
+ filter: transition.filter,
2157
+ definition,
2158
+ snapshot,
2159
+ params: buildParams(instance)
2160
+ })
2161
+ });
2162
+ const currentStage = {
2163
+ stage,
2164
+ tasks: taskEvaluations,
2165
+ transitions: transitionEvaluations
2166
+ }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2167
+ return {
2168
+ instance,
2169
+ definition,
2170
+ actor,
2171
+ currentStage,
2172
+ pendingOnYou,
2173
+ canInteract
2174
+ };
2175
+ }
2176
+ async function evaluateTask(args) {
2177
+ const { task, statusEntry, instance, definition, actor, grants, snapshot } = args, status = statusEntry?.status ?? "pending", actions = [];
2178
+ for (const action of task.actions ?? [])
2179
+ actions.push(
2180
+ await evaluateAction({
2181
+ action,
2182
+ task,
2183
+ ...statusEntry !== void 0 ? { statusEntry } : {},
2184
+ status,
2185
+ instance,
2186
+ definition,
2187
+ actor,
2188
+ ...grants !== void 0 ? { grants } : {},
2189
+ snapshot
2190
+ })
2191
+ );
2192
+ return {
2193
+ task,
2194
+ status,
2195
+ // "pending on actor" treats both `pending` and `active` as waiting on
2196
+ // the assignee — pending tasks just haven't been auto-invoked yet, but
2197
+ // they're still inbox items.
2198
+ pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2199
+ actions
2200
+ };
2201
+ }
2202
+ async function evaluateAction(args) {
2203
+ const { action, task, status, instance, definition, actor, grants, snapshot } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2204
+ if (syncReason !== void 0) return disabled(action, syncReason);
2205
+ const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot);
2206
+ return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
2207
+ }
2208
+ function lifecycleReason(instance, definition, status) {
2209
+ if (instance.completedAt !== void 0)
2210
+ return { kind: "instance-completed", completedAt: instance.completedAt };
2211
+ if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2212
+ return { kind: "stage-terminal", stageId: instance.currentStageId };
2213
+ if (status === "done" || status === "skipped" || status === "failed")
2214
+ return { kind: "task-not-active", status };
2215
+ }
2216
+ function actorReason(action, task, actor) {
2217
+ const actorRoles = actor.roles ?? [];
2218
+ if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2219
+ return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2220
+ if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2221
+ return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
2222
+ }
2223
+ async function permissionReason(action, instance, actor, grants) {
2224
+ if (grants === void 0) return;
2225
+ const permission = action.requiredPermission ?? "update";
2226
+ if (!await grantsPermissionOn({
2227
+ document: instance,
2228
+ grants,
2229
+ permission,
2230
+ userId: actor.id
2231
+ }))
2232
+ return { kind: "permission-denied", permission, matchingGrants: grants.length };
2233
+ }
2234
+ async function filterReason(action, instance, definition, snapshot) {
2235
+ if (!(action.filter === void 0 || await evaluateFilter({
2236
+ filter: action.filter,
2237
+ definition,
2238
+ snapshot,
2239
+ params: buildParams(instance)
2240
+ })))
2241
+ return { kind: "filter-failed", filter: action.filter };
2242
+ }
2243
+ function disabled(action, reason) {
2244
+ return { action, allowed: !1, disabledReason: reason };
2245
+ }
2246
+ function actorIsAssignee(actor, assignees) {
2247
+ if (assignees.length === 0) return !1;
2248
+ const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2249
+ for (const assignee of assignees)
2250
+ if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
2251
+ return !0;
2252
+ return !1;
2253
+ }
2254
+ function parseDefinitionSnapshot(instance) {
2255
+ try {
2256
+ return JSON.parse(instance.definitionSnapshot);
2257
+ } catch (err) {
2258
+ throw new Error(
2259
+ `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2260
+ { cause: err }
2261
+ );
2262
+ }
2263
+ }
2264
+ class ActionDisabledError extends Error {
2265
+ reason;
2266
+ taskId;
2267
+ action;
2268
+ constructor(args) {
2269
+ super(formatDisabledReason(args.taskId, args.action, args.reason)), this.name = "ActionDisabledError", this.reason = args.reason, this.taskId = args.taskId, this.action = args.action;
2270
+ }
2271
+ }
2272
+ const disabledReasonDetail = {
2273
+ "role-required": (r) => `requires one of [${r.required.join(", ")}]; actor has [${r.actorRoles.join(", ") || "(none)"}]`,
2274
+ "not-assigned": (r) => `actor "${r.actorId}" is not in the task assignees`,
2275
+ "permission-denied": (r) => `actor lacks "${r.permission}" permission on the workflow instance`,
2276
+ "filter-failed": (r) => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
2277
+ "task-not-active": (r) => `task status is "${r.status}"`,
2278
+ "stage-terminal": (r) => `stage "${r.stageId}" is terminal`,
2279
+ "instance-completed": (r) => `instance completed at ${r.completedAt}`,
2280
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
2281
+ };
2282
+ function formatDisabledReason(taskId, action, reason) {
2283
+ const detail = disabledReasonDetail[reason.kind](reason);
2284
+ return `Action "${taskId}:${action}" is not allowed: ${detail}`;
2285
+ }
2286
+ function definitionDocId(_workflowResource, prefix, workflowId, version) {
2287
+ return `${prefix}.${workflowId}.v${version}`;
2288
+ }
2289
+ async function sortByDependencies(client, definitions, tags, workflowResource) {
2290
+ const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2291
+ for (const def of definitions) {
2292
+ const list = byWorkflowId.get(def.workflowId) ?? [];
2293
+ list.push(def), byWorkflowId.set(def.workflowId, list);
2294
+ }
2295
+ const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2296
+ return await assertCrossBatchRefsDeployed(client, definitions, {
2297
+ tags,
2298
+ workflowResource,
2299
+ prefix,
2300
+ findInBatch
2301
+ }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2302
+ }
2303
+ function findRefInBatch(byWorkflowId, ref) {
2304
+ const candidates = byWorkflowId.get(ref.workflowId);
2305
+ if (!(candidates === void 0 || candidates.length === 0))
2306
+ return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2307
+ (best, c) => best === void 0 || c.version > best.version ? c : best,
2308
+ void 0
2309
+ );
2310
+ }
2311
+ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2312
+ const missing = [];
2313
+ for (const def of definitions) {
2314
+ const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2315
+ for (const ref of refsOf(def)) {
2316
+ if (ctx.findInBatch(ref) !== void 0) continue;
2317
+ const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2318
+ label !== void 0 && missing.push({ from: fromId, ref: label });
2319
+ }
2320
+ }
2321
+ if (missing.length === 0) return;
2322
+ const lines = missing.map((m) => ` - ${m.from} \u2192 ${m.ref} (neither in batch nor deployed)`);
2323
+ throw new Error(
2324
+ `workflow.deployDefinitions: ${missing.length} unresolved reference${missing.length === 1 ? "" : "s"}:
2325
+ ` + lines.join(`
2326
+ `)
2327
+ );
2328
+ }
2329
+ async function resolveDeployedRefLabel(client, ref, tags) {
2330
+ const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, tags };
2331
+ if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2332
+ definitionLookupGroq(wantsExplicit),
2333
+ params
2334
+ ))
2335
+ return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2336
+ }
2337
+ function topoSortDefinitions(definitions, ctx) {
2338
+ const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2339
+ const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2340
+ if (!visited.has(id)) {
2341
+ if (visiting.has(id))
2342
+ throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
2343
+ visiting.add(id);
2344
+ for (const ref of refsOf(def)) {
2345
+ const child = ctx.findInBatch(ref);
2346
+ child !== void 0 && visit(child);
2347
+ }
2348
+ visiting.delete(id), visited.add(id), ordered.push(def);
2349
+ }
2350
+ };
2351
+ for (const def of definitions) visit(def);
2352
+ return ordered;
2353
+ }
2354
+ function definitionLookupGroq(wantsExplicit) {
2355
+ return wantsExplicit ? '*[_type == "workflow.definition" && workflowId == $workflowId && version == $version && count(tags[@ in $tags]) > 0][0]' : '*[_type == "workflow.definition" && workflowId == $workflowId && count(tags[@ in $tags]) > 0] | order(version desc)[0]';
2356
+ }
2357
+ function refsOf(def) {
2358
+ const out = [];
2359
+ for (const stage of def.stages)
2360
+ for (const task of stage.tasks ?? []) {
2361
+ const ref = task.spawns?.definitionRef;
2362
+ ref !== void 0 && out.push({
2363
+ workflowId: ref.workflowId,
2364
+ ...ref.version !== void 0 ? { version: ref.version } : {}
2365
+ });
2366
+ }
2367
+ return out;
2368
+ }
2369
+ function isDefinitionUnchanged(existing, expected) {
2370
+ return stableStringify(stripSystemFields(existing)) === stableStringify(stripSystemFields(expected));
2371
+ }
2372
+ function stripSystemFields(doc) {
2373
+ const out = {};
2374
+ for (const [k, v] of Object.entries(doc))
2375
+ k === "_rev" || k === "_createdAt" || k === "_updatedAt" || (out[k] = v);
2376
+ return out;
2377
+ }
2378
+ function stableStringify(value) {
2379
+ return value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.entries(value).toSorted(
2380
+ ([a], [b]) => a.localeCompare(b)
2381
+ ).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
2382
+ }
2383
+ async function loadDefinition(client, workflowId, version, tags) {
2384
+ if (version !== void 0) {
2385
+ const doc = await client.fetch(
2386
+ "*[_type == 'workflow.definition' && workflowId == $id && version == $v && count(tags[@ in $engineTags]) > 0][0]",
2387
+ { id: workflowId, v: version, engineTags: tags }
2388
+ );
2389
+ if (!doc)
2390
+ throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2391
+ return doc;
2392
+ }
2393
+ const latest = await client.fetch(
2394
+ "*[_type == 'workflow.definition' && workflowId == $id && count(tags[@ in $engineTags]) > 0] | order(version desc)[0]",
2395
+ { id: workflowId, engineTags: tags }
2396
+ );
2397
+ if (!latest)
2398
+ throw new Error(`No deployed definition for workflow ${workflowId}`);
2399
+ return latest;
2400
+ }
2401
+ function bareIdFromSpawnRef(uri) {
2402
+ if (!uri.includes(":")) return [uri];
2403
+ try {
2404
+ return [extractDocumentId(uri)];
2405
+ } catch {
2406
+ return [];
2407
+ }
2408
+ }
2409
+ async function resolveOperationContext(args) {
2410
+ validateTags(args.tags);
2411
+ const access = await resolveAccess(args.client, {
2412
+ ...args.access !== void 0 ? { override: args.access } : {},
2413
+ ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2414
+ });
2415
+ return {
2416
+ access,
2417
+ actor: access.actor,
2418
+ clientForGdr: buildClientForGdr(args.client, args.resourceClients)
2419
+ };
2420
+ }
2421
+ async function cascade(client, instanceId, actor, clientForGdr) {
2422
+ const count = await cascadeAutoTransitions(client, instanceId, actor, clientForGdr);
2423
+ return await propagateToAncestors(client, instanceId, actor, clientForGdr), count;
2424
+ }
2425
+ function buildClientForGdr(defaultClient, resolver) {
2426
+ return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
2427
+ }
2428
+ async function reload(client, instanceId, tags) {
2429
+ const doc = await client.getDocument(instanceId);
2430
+ if (!doc) throw new Error(`Workflow instance ${instanceId} not found`);
2431
+ if (!intersectsTags(doc.tags, tags))
2432
+ throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
2433
+ return doc;
2434
+ }
2435
+ function intersectsTags(docTags, engineTags) {
2436
+ return !docTags || docTags.length === 0 ? !1 : engineTags.some((t) => docTags.includes(t));
2437
+ }
2438
+ function toEffectsContextEntries(ctx) {
2439
+ return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2440
+ }
2441
+ const workflow = {
2442
+ /**
2443
+ * Persist a workflow definition to the lake. Stored as a
2444
+ * `workflow.definition` document with id
2445
+ * `workflow.definition.<workflowId>.v<version>`. Subsequent versions
2446
+ * are stored alongside earlier ones — `startInstance` picks the
2447
+ * highest version by default.
2448
+ */
2449
+ /**
2450
+ * Deploy a set of workflow definitions as one call. The SDK figures
2451
+ * out the dependency order itself (children before parents that spawn
2452
+ * them via `task.spawns.definitionRef`), idempotently writes any
2453
+ * definition whose content has changed, and reports a per-definition
2454
+ * outcome (`created` / `updated` / `unchanged`).
2455
+ *
2456
+ * Refs may point inside the batch OR at already-deployed definitions
2457
+ * in the lake — both are valid. A ref pointing at neither errors with
2458
+ * a clear message naming the missing target.
2459
+ *
2460
+ * Cycles in the dependency graph error before any write happens.
2461
+ */
2462
+ deployDefinitions: async (args) => {
2463
+ const { client, tags, workflowResource, definitions } = args;
2464
+ validateTags(tags);
2465
+ for (const def of definitions)
2466
+ validateDefinition(def);
2467
+ const ordered = await sortByDependencies(client, definitions, tags, workflowResource), results = [], prefix = canonicalTag(tags), tx = client.transaction();
2468
+ let hasWrites = !1;
2469
+ for (const def of ordered) {
2470
+ const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
2471
+ ...def,
2472
+ _id: id,
2473
+ _type: "workflow.definition",
2474
+ tags
2475
+ }, existing = await client.getDocument(id);
2476
+ if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
2477
+ results.push({ workflowId: def.workflowId, version: def.version, status: "unchanged" });
2478
+ continue;
2479
+ }
2480
+ if (existing) {
2481
+ const expectedKeys = new Set(Object.keys(expected)), toUnset = Object.keys(existing).filter(
2482
+ (k) => !k.startsWith("_") && !expectedKeys.has(k)
2483
+ ), patch = client.patch(id).set(expected).ifRevisionId(existing._rev);
2484
+ toUnset.length > 0 && patch.unset(toUnset), tx.patch(patch);
2485
+ } else
2486
+ tx.create(expected);
2487
+ hasWrites = !0, results.push({
2488
+ workflowId: def.workflowId,
2489
+ version: def.version,
2490
+ status: existing ? "updated" : "created"
2491
+ });
2492
+ }
2493
+ return hasWrites && await tx.commit(), { results };
2494
+ },
2495
+ /**
2496
+ * Spawn a new workflow instance from a deployed definition.
2497
+ *
2498
+ * Pins the snapshot at start-time, seeds `effectsContext`, enters
2499
+ * the initial stage (which builds taskStatus, queues onEnter
2500
+ * effects, auto-activates tasks). Then cascades auto-transitions
2501
+ * until stable. Returns the resulting instance.
2502
+ */
2503
+ startInstance: async (args) => {
2504
+ const {
2505
+ client,
2506
+ tags,
2507
+ workflowResource,
2508
+ workflowId,
2509
+ version,
2510
+ initialState,
2511
+ ancestors,
2512
+ effectsContext,
2513
+ instanceId,
2514
+ perspective
2515
+ } = args, { actor, clientForGdr } = await resolveOperationContext(args), definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2516
+ if (definition.stages.find((s) => s.id === definition.initialStageId) === void 0)
2517
+ throw new Error(
2518
+ `Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
2519
+ );
2520
+ const now = (/* @__PURE__ */ new Date()).toISOString(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2521
+ slotDefs: definition.state ?? [],
2522
+ initialState: initialState ?? [],
2523
+ ctx: {
2524
+ client,
2525
+ selfId: id,
2526
+ tags,
2527
+ workflowResource,
2528
+ ...perspective !== void 0 ? { perspective } : {}
2529
+ },
2530
+ randomKey
2531
+ }), effectivePerspective = perspective ?? derivePerspectiveFromState(resolvedState), base = buildInstanceBase({
2532
+ id,
2533
+ now,
2534
+ tags,
2535
+ workflowResource,
2536
+ workflowId: definition.workflowId,
2537
+ pinnedVersion: definition.version,
2538
+ definition,
2539
+ state: resolvedState,
2540
+ effectsContext: effectsContextEntries,
2541
+ ancestors: ancestors ?? [],
2542
+ perspective: effectivePerspective,
2543
+ initialStageId: definition.initialStageId,
2544
+ actor
2545
+ });
2546
+ return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr), await cascade(client, id, actor, clientForGdr), reload(client, id, tags);
2547
+ },
2548
+ /**
2549
+ * Fire an action against a task. If the task is pending, it is
2550
+ * auto-invoked first (pending → active) so callers don't have to know
2551
+ * the lifecycle. Cascades auto-transitions and propagates to ancestors
2552
+ * after the action commits.
2553
+ *
2554
+ * This is the universal "something happened" call. Editors fire it.
2555
+ * Runtimes fire it in response to webhooks, effect completions, and
2556
+ * timer firings. External signals never bypass this.
2557
+ */
2558
+ fireAction: async (args) => {
2559
+ const {
2560
+ client,
2561
+ tags,
2562
+ instanceId,
2563
+ taskId,
2564
+ action,
2565
+ params,
2566
+ idempotent,
2567
+ resourceClients
2568
+ } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), before = await reload(client, instanceId, tags), taskEntry = before.stages.find(
2569
+ (s) => s.id === before.currentStageId && s.exitedAt === void 0
2570
+ )?.tasks.find((t) => t.id === taskId);
2571
+ if (taskEntry === void 0 && idempotent === !0)
2572
+ return { instance: before, cascaded: 0, fired: !1 };
2573
+ const actionEval = (await evaluateInstance({
2574
+ client,
2575
+ tags,
2576
+ instanceId,
2577
+ access,
2578
+ ...resourceClients !== void 0 ? { resourceClients } : {}
2579
+ })).currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2580
+ if (actionEval !== void 0 && !actionEval.allowed && actionEval.disabledReason)
2581
+ throw new ActionDisabledError({
2582
+ taskId,
2583
+ action,
2584
+ reason: actionEval.disabledReason
2585
+ });
2586
+ taskEntry?.status === "pending" && await invokeTask({
2587
+ client,
2588
+ instanceId,
2589
+ taskId,
2590
+ options: { actor }
2591
+ });
2592
+ const actionResult = await fireAction({
2593
+ client,
2594
+ instanceId,
2595
+ taskId,
2596
+ action,
2597
+ ...params !== void 0 ? { params } : {},
2598
+ options: { actor }
2599
+ }), cascaded = await cascade(client, instanceId, actor, clientForGdr);
2600
+ return {
2601
+ instance: await reload(client, instanceId, tags),
2602
+ cascaded,
2603
+ fired: !0,
2604
+ ...actionResult.ranOps !== void 0 ? { ranOps: actionResult.ranOps } : {}
2605
+ };
2606
+ },
2607
+ /**
2608
+ * Report a queued effect's outcome. Drains it from `pendingEffects`,
2609
+ * appends an `effectHistory` entry, and (if `outputs` are supplied
2610
+ * and the effect succeeded) upserts those values into
2611
+ * `effectsContext` by key — so downstream effect bindings can
2612
+ * reference them. Cascades after.
2613
+ */
2614
+ completeEffect: async (args) => {
2615
+ const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2616
+ await completeEffect({
2617
+ client,
2618
+ instanceId,
2619
+ effectKey,
2620
+ status,
2621
+ ...outputs !== void 0 ? { outputs } : {},
2622
+ ...detail !== void 0 ? { detail } : {},
2623
+ ...error !== void 0 ? { error } : {},
2624
+ ...durationMs !== void 0 ? { durationMs } : {},
2625
+ ...actor !== void 0 ? { options: { actor } } : {}
2626
+ });
2627
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2628
+ return {
2629
+ instance: await reload(client, instanceId, tags),
2630
+ cascaded,
2631
+ fired: !0
2632
+ };
2633
+ },
2634
+ /**
2635
+ * Re-evaluate auto-transitions and resolve due waits until stable.
2636
+ *
2637
+ * Used by the runtime after any event that might affect the workflow
2638
+ * but isn't itself a task action: a subject doc was patched, a sibling
2639
+ * workflow completed, the clock crossed a `completeWhen` deadline, etc.
2640
+ * The runtime doesn't need to know what changed — it just nudges
2641
+ * affected instances and the engine re-evaluates.
2642
+ */
2643
+ tick: async (args) => {
2644
+ const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2645
+ await reload(client, instanceId, tags);
2646
+ const cascaded = await cascade(client, instanceId, actor, clientForGdr);
2647
+ return {
2648
+ instance: await reload(client, instanceId, tags),
2649
+ cascaded,
2650
+ fired: cascaded > 0
2651
+ };
2652
+ },
2653
+ /**
2654
+ * Admin override — force the instance into `targetStageId` regardless
2655
+ * of filters or declared transitions. ACL gating should be enforced
2656
+ * upstream; this verb performs the mechanical move.
2657
+ */
2658
+ setStage: async (args) => {
2659
+ const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args);
2660
+ await reload(client, instanceId, tags);
2661
+ const result = await setStage({
2662
+ client,
2663
+ instanceId,
2664
+ targetStageId,
2665
+ ...reason !== void 0 ? { reason } : {},
2666
+ ...actor !== void 0 ? { options: { actor } } : {}
2667
+ }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr) : 0;
2668
+ return {
2669
+ instance: await reload(client, instanceId, tags),
2670
+ cascaded,
2671
+ fired: result.fired
2672
+ };
2673
+ },
2674
+ /**
2675
+ * Fetch a workflow instance by id, scoped to the engine's tags.
2676
+ * Throws when the instance doesn't exist or isn't visible to this
2677
+ * engine.
2678
+ */
2679
+ getInstance: async (args) => {
2680
+ const { client, tags, instanceId } = args;
2681
+ return validateTags(tags), reload(client, instanceId, tags);
2682
+ },
2683
+ /**
2684
+ * Run a caller-supplied GROQ query with the engine's tags bound as
2685
+ * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
2686
+ * can't be safely tag-scoped after the fact — so the CALLER MUST
2687
+ * filter on `$engineTags` (e.g.
2688
+ * `count(tags[@ in $engineTags]) > 0`). To guard against accidental
2689
+ * cross-tenant reads, a query that never references `$engineTags` is
2690
+ * rejected before it reaches the lake. Caller is responsible for type
2691
+ * narrowing the result.
2692
+ */
2693
+ query: async (args) => {
2694
+ const { client, tags, groq, params } = args;
2695
+ if (validateTags(tags), !groq.includes("$engineTags"))
2696
+ throw new Error(
2697
+ `workflow.query: query must filter on $engineTags to stay tag-scoped (e.g. "count(tags[@ in $engineTags]) > 0"); got: ${groq}`
2698
+ );
2699
+ return client.fetch(groq, { ...params, engineTags: tags });
2700
+ },
2701
+ /**
2702
+ * Snapshot-aware GROQ — runs against the same in-memory view that
2703
+ * filters see for a given instance.
2704
+ *
2705
+ * Hydrates the instance's snapshot (instance + subject + ancestors
2706
+ * + every doc declared by a `doc.ref` / `doc.refs` slot in scope),
2707
+ * then evaluates the supplied GROQ in groq-js against that dataset.
2708
+ * Reserved params are auto-bound: `$self`, `$subject`, `$parent`,
2709
+ * `$ancestors`, `$stage`, `$now` — all in GDR URI form to match the
2710
+ * snapshot's keying.
2711
+ *
2712
+ * Use when an external consumer (a UI, a debug pane, a test) wants
2713
+ * to ask "what does the engine see for this workflow right now?"
2714
+ * without re-implementing hydration. Pure read — never writes.
2715
+ */
2716
+ queryInScope: async (args) => {
2717
+ const { client, tags, instanceId, groq, params, resourceClients } = args;
2718
+ validateTags(tags);
2719
+ const instance = await reload(client, instanceId, tags), definition = JSON.parse(instance.definitionSnapshot), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance, definition }), reserved = buildParams(instance), tree = parse(groq, { params: { ...reserved, ...params } });
2720
+ return await (await evaluate(tree, {
2721
+ dataset: snapshot.docs,
2722
+ params: { ...reserved, ...params }
2723
+ })).get();
2724
+ },
2725
+ /**
2726
+ * List every pending effect on the instance. Returns the same entries
2727
+ * the runtime would see — claimed and unclaimed alike.
2728
+ */
2729
+ listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tags)).pendingEffects,
2730
+ /**
2731
+ * Filter pending effects on the instance by criteria. `claimed`
2732
+ * filters on claim presence; `names` restricts to specific effect
2733
+ * names. Both filters compose (AND).
2734
+ */
2735
+ findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tags)).pendingEffects.filter((e) => !(args.claimed === !0 && e.claim === void 0 || args.claimed === !1 && e.claim !== void 0 || args.names !== void 0 && !args.names.includes(e.effectId))),
2736
+ /**
2737
+ * Project the instance from a given actor's perspective. Returns a
2738
+ * `WorkflowEvaluation` with per-action verdicts (`allowed` + a
2739
+ * structured `disabledReason`). Pure read; never writes.
2740
+ *
2741
+ * Used by UIs to render disabled-with-reason buttons and by
2742
+ * `fireAction` to gate writes via the same logic.
2743
+ */
2744
+ evaluate: async (args) => evaluateInstance(args),
2745
+ /**
2746
+ * Materialised spawned children of a parent instance.
2747
+ *
2748
+ * Walks `history` for `workflow.history.spawned` events — the durable
2749
+ * record. (The per-task `spawnedInstances` field on the active stage
2750
+ * only exists while that stage is current; history survives.) Strips
2751
+ * the GDR URI on each `instanceRef` to a bare `_id`, fetches the
2752
+ * instances, drops any that aren't visible to this engine's tags, and
2753
+ * returns them sorted by `startedAt` ascending.
2754
+ *
2755
+ * Pass `taskId` to restrict to a single spawning task on the parent.
2756
+ */
2757
+ children: async (args) => {
2758
+ const { client, tags, instanceId, taskId } = args;
2759
+ validateTags(tags);
2760
+ const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "workflow.history.spawned", ids = parent.history.filter(isSpawned).filter((h) => taskId === void 0 || h.taskId === taskId).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
2761
+ return ids.length === 0 ? [] : client.fetch(
2762
+ "*[_id in $ids && count(tags[@ in $engineTags]) > 0] | order(startedAt asc)",
2763
+ { ids, engineTags: tags }
2764
+ );
2765
+ },
2766
+ /**
2767
+ * Permission helpers — Sanity ACL grants evaluated against documents
2768
+ * via GROQ. Used by `workflow.evaluate` to soft-gate actions when the
2769
+ * caller supplies grants.
2770
+ */
2771
+ permissions: {
2772
+ matchesFilter,
2773
+ grantsPermissionOn,
2774
+ fetchGrants
2775
+ }
2776
+ };
2777
+ async function verifyDeployedDefinitionsInternal(args) {
2778
+ const { client, tags, effectHandlers, missingHandler, logger } = args;
2779
+ validateTags(tags);
2780
+ const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
2781
+ '*[_type == "workflow.definition" && count(tags[@ in $engineTags]) > 0] | order(workflowId asc, version asc)',
2782
+ { engineTags: tags }
2783
+ ), seen = [], missingByName = /* @__PURE__ */ new Map();
2784
+ for (const def of definitions)
2785
+ seen.push({ workflowId: def.workflowId, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
2786
+ if (effectHandlers[name] !== void 0) return;
2787
+ const key = `${name}@@${def._id}`;
2788
+ let bucket = missingByName.get(key);
2789
+ bucket === void 0 && (bucket = { definitionId: def._id, locations: /* @__PURE__ */ new Set() }, missingByName.set(key, bucket)), bucket.locations.add(location);
2790
+ });
2791
+ const missing = [];
2792
+ for (const [key, bucket] of missingByName.entries()) {
2793
+ const name = key.split("@@")[0];
2794
+ missing.push({
2795
+ name,
2796
+ definitionId: bucket.definitionId,
2797
+ locations: [...bucket.locations].toSorted()
2798
+ });
2799
+ }
2800
+ for (const m of missing) {
2801
+ const info = {
2802
+ phase: "deploy",
2803
+ name: m.name,
2804
+ locations: m.locations,
2805
+ definitionId: m.definitionId
2806
+ };
2807
+ if (await applyMissingHandler(missingHandler, info, log) === "fail")
2808
+ throw new Error(
2809
+ `verifyDeployedDefinitions: missing handler for "${m.name}" referenced by ${m.definitionId} at ${m.locations.join(", ")}`
2810
+ );
2811
+ }
2812
+ return { definitions: seen, missing };
2813
+ }
2814
+ function walkEffectNames(def, visit) {
2815
+ const visitEffects = (effects, location) => {
2816
+ for (const e of effects ?? []) visit(e.id, location);
2817
+ };
2818
+ for (const stage of def.stages ?? []) {
2819
+ visitEffects(stage.onEnter, `stage[${stage.id}].onEnter`), visitEffects(stage.onExit, `stage[${stage.id}].onExit`);
2820
+ for (const t of stage.tasks ?? []) {
2821
+ visitEffects(t.effects, `stage[${stage.id}].task[${t.id}].effects`);
2822
+ for (const a of t.actions ?? [])
2823
+ visitEffects(a.effects, `stage[${stage.id}].task[${t.id}].action[${a.id}].effects`);
2824
+ }
2825
+ for (const tr of stage.transitions ?? [])
2826
+ visitEffects(tr.effects, `stage[${stage.id}].transition[${tr.to}].effects`);
2827
+ }
2828
+ }
2829
+ async function drainEffectsInternal(args) {
2830
+ const {
2831
+ client,
2832
+ tags,
2833
+ workflowResource,
2834
+ resourceClients,
2835
+ instanceId,
2836
+ effectHandlers,
2837
+ missingHandler,
2838
+ logger
2839
+ } = args, drainerActor = args.access?.actor ?? { kind: "system", id: "engine.drainEffects" }, completionAccess = {
2840
+ actor: drainerActor,
2841
+ ...args.access?.grants !== void 0 ? { grants: args.access.grants } : {}
2842
+ };
2843
+ validateTags(tags);
2844
+ const log = logger("drainEffects"), drained = [], failed = [], skipped = [], skippedKeys = /* @__PURE__ */ new Set();
2845
+ for (; ; ) {
2846
+ const before = await reload(client, instanceId, tags), candidate = before.pendingEffects.find(
2847
+ (e) => e.claim === void 0 && !skippedKeys.has(e._key)
2848
+ );
2849
+ if (candidate === void 0) break;
2850
+ const handler = effectHandlers[candidate.effectId];
2851
+ if (handler === void 0) {
2852
+ await assertSkippableOrThrow(missingHandler, candidate, instanceId, log), skipped.push(candidate), skippedKeys.add(candidate._key);
2853
+ continue;
2854
+ }
2855
+ if (!await claimPendingEffect(client, before, candidate._key, drainerActor)) continue;
2856
+ const { outputs, dispatchError } = await dispatchEffect(handler, candidate, {
2857
+ client,
2858
+ instanceId,
2859
+ logger
2860
+ });
2861
+ await reportEffectOutcome({
2862
+ client,
2863
+ tags,
2864
+ workflowResource,
2865
+ ...resourceClients !== void 0 ? { resourceClients } : {},
2866
+ instanceId,
2867
+ effectKey: candidate._key,
2868
+ access: completionAccess,
2869
+ outputs,
2870
+ dispatchError
2871
+ }), dispatchError === void 0 ? drained.push(candidate) : failed.push(candidate);
2872
+ }
2873
+ return { drained, failed, skipped };
2874
+ }
2875
+ async function reportEffectOutcome(args) {
2876
+ const { outputs, dispatchError, resourceClients, ...rest } = args;
2877
+ await workflow.completeEffect({
2878
+ ...rest,
2879
+ ...resourceClients !== void 0 ? { resourceClients } : {},
2880
+ status: dispatchError === void 0 ? "done" : "failed",
2881
+ ...outputs !== void 0 ? { outputs } : {},
2882
+ ...dispatchError !== void 0 ? { error: dispatchError } : {}
2883
+ });
2884
+ }
2885
+ async function assertSkippableOrThrow(missingHandler, candidate, instanceId, log) {
2886
+ if (await applyMissingHandler(
2887
+ missingHandler,
2888
+ { phase: "drain", name: candidate.effectId, effectKey: candidate._key, instanceId },
2889
+ log
2890
+ ) === "fail")
2891
+ throw new Error(
2892
+ `drainEffects: no handler registered for "${candidate.effectId}" (effectKey=${candidate._key}, instanceId=${instanceId})`
2893
+ );
2894
+ }
2895
+ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
2896
+ const claim = {
2897
+ _type: "workflow.pendingEffect.claim",
2898
+ claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
2899
+ claimedBy: drainerActor
2900
+ };
2901
+ try {
2902
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
2903
+ } catch {
2904
+ return !1;
2905
+ }
2906
+ }
2907
+ async function dispatchEffect(handler, candidate, ctx) {
2908
+ try {
2909
+ const result = await handler(candidate.params, {
2910
+ client: ctx.client,
2911
+ instanceId: ctx.instanceId,
2912
+ effectKey: candidate._key,
2913
+ log: (message, extra) => ctx.logger(`effect.${candidate.effectId}`).info(message, extra)
2914
+ });
2915
+ return result?.outputs !== void 0 ? { outputs: result.outputs } : {};
2916
+ } catch (err) {
2917
+ const message = err instanceof Error ? err.message : String(err), stack = err instanceof Error && err.stack !== void 0 ? err.stack : void 0;
2918
+ return { dispatchError: stack !== void 0 ? { message, stack } : { message } };
2919
+ }
2920
+ }
2921
+ async function applyMissingHandler(policy, info, log) {
2922
+ if (policy === "fail") return "fail";
2923
+ if (policy === "skip")
2924
+ return log.warn(`Missing effect handler "${info.name}" \u2014 skipping`, { info }), "skip";
2925
+ try {
2926
+ return await policy(info), "skip";
2927
+ } catch {
2928
+ return "fail";
2929
+ }
2930
+ }
2931
+ const defaultLoggerFactory = (name) => ({
2932
+ // info/warn/error all go to stderr — these are diagnostic, not
2933
+ // program output. Keeps stdout reserved for the caller's own data
2934
+ // so progress markers (process.stdout.write) and logger lines
2935
+ // don't interleave on the same line.
2936
+ info: (message, extra) => console.error(`[${name}] ${message}`, extra !== void 0 ? extra : ""),
2937
+ warn: (message, extra) => console.warn(`[${name}] ${message}`, extra !== void 0 ? extra : ""),
2938
+ error: (message, extra) => console.error(`[${name}] ${message}`, extra !== void 0 ? extra : "")
2939
+ }), silentLogger = {
2940
+ info: () => {
2941
+ },
2942
+ warn: () => {
2943
+ },
2944
+ error: () => {
2945
+ }
2946
+ };
2947
+ function createEngine(args) {
2948
+ const { client, workflowResource, tags, resourceClients } = args;
2949
+ validateTags(tags);
2950
+ const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
2951
+ client,
2952
+ tags,
2953
+ workflowResource,
2954
+ ...resourceClients !== void 0 ? { resourceClients } : {},
2955
+ ...rest
2956
+ });
2957
+ return {
2958
+ client,
2959
+ tags,
2960
+ workflowResource,
2961
+ effectHandlers,
2962
+ missingHandler,
2963
+ logger,
2964
+ deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
2965
+ startInstance: (rest) => workflow.startInstance(bind(rest)),
2966
+ fireAction: (rest) => workflow.fireAction(bind(rest)),
2967
+ completeEffect: (rest) => workflow.completeEffect(bind(rest)),
2968
+ tick: (rest) => workflow.tick(bind(rest)),
2969
+ evaluateInstance: (rest) => evaluateInstance(bind(rest)),
2970
+ setStage: (rest) => workflow.setStage(bind(rest)),
2971
+ getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
2972
+ children: ({ instanceId, taskId }) => workflow.children({
2973
+ client,
2974
+ tags,
2975
+ workflowResource,
2976
+ instanceId,
2977
+ ...taskId !== void 0 ? { taskId } : {}
2978
+ }),
2979
+ query: ({ groq, params }) => workflow.query({
2980
+ client,
2981
+ tags,
2982
+ workflowResource,
2983
+ groq,
2984
+ ...params !== void 0 ? { params } : {}
2985
+ }),
2986
+ queryInScope: ({ instanceId, groq, params }) => workflow.queryInScope({
2987
+ client,
2988
+ tags,
2989
+ workflowResource,
2990
+ instanceId,
2991
+ groq,
2992
+ ...params !== void 0 ? { params } : {}
2993
+ }),
2994
+ listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tags, workflowResource, instanceId }),
2995
+ findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
2996
+ client,
2997
+ tags,
2998
+ workflowResource,
2999
+ instanceId,
3000
+ ...claimed !== void 0 ? { claimed } : {},
3001
+ ...names !== void 0 ? { names } : {}
3002
+ }),
3003
+ drainEffects: ({ instanceId, access }) => drainEffectsInternal({
3004
+ client,
3005
+ tags,
3006
+ workflowResource,
3007
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3008
+ instanceId,
3009
+ effectHandlers,
3010
+ missingHandler,
3011
+ logger,
3012
+ ...access !== void 0 ? { access } : {}
3013
+ }),
3014
+ verifyDeployedDefinitions: () => verifyDeployedDefinitionsInternal({
3015
+ client,
3016
+ tags,
3017
+ effectHandlers,
3018
+ missingHandler,
3019
+ logger
3020
+ })
3021
+ };
3022
+ }
3023
+ class MutationGuardDeniedError extends Error {
3024
+ denied;
3025
+ documentId;
3026
+ action;
3027
+ constructor(args) {
3028
+ const ids = args.denied.map((d) => d.guardId).join(", ");
3029
+ super(`Mutation on "${args.documentId}" (${args.action}) denied by guard(s) [${ids}]`), this.name = "MutationGuardDeniedError", this.denied = args.denied, this.documentId = args.documentId, this.action = args.action;
3030
+ }
3031
+ }
3032
+ const HISTORY_DISPLAY = {
3033
+ "workflow.history.stageEntered": {
3034
+ title: "Stage entered",
3035
+ description: "The instance crossed into a new stage."
3036
+ },
3037
+ "workflow.history.stageExited": {
3038
+ title: "Stage exited",
3039
+ description: "The instance left a stage on the way to the next one."
3040
+ },
3041
+ "workflow.history.taskActivated": {
3042
+ title: "Task activated",
3043
+ description: "A task flipped from `pending` to `active` and its onEnter effects queued."
3044
+ },
3045
+ "workflow.history.taskStatusChanged": {
3046
+ title: "Task status changed",
3047
+ description: "An action or op flipped a task's status (active \u2192 done / skipped / failed)."
3048
+ },
3049
+ "workflow.history.actionFired": {
3050
+ title: "Action fired",
3051
+ description: "An action was invoked against a task \u2014 ops ran, status may have flipped, effects queued."
3052
+ },
3053
+ "workflow.history.transitionFired": {
3054
+ title: "Transition fired",
3055
+ description: "A transition committed and the instance moved to a new stage."
3056
+ },
3057
+ "workflow.history.effectQueued": {
3058
+ title: "Effect queued",
3059
+ description: "A pending effect was added to the queue, waiting for a drainer to dispatch it."
3060
+ },
3061
+ "workflow.history.effectCompleted": {
3062
+ title: "Effect completed",
3063
+ description: "A drainer ran an effect handler to completion (done or failed) and merged its outputs into effectsContext."
3064
+ },
3065
+ "workflow.history.spawned": {
3066
+ title: "Spawned child",
3067
+ description: "A `task.spawns` declaration spawned a child workflow instance."
3068
+ },
3069
+ "workflow.history.opApplied": {
3070
+ title: "Op applied",
3071
+ description: "An inline state-mutation op (state.set / state.append / status.set / etc.) ran during an action commit."
3072
+ }
3073
+ }, STATE_SLOT_DISPLAY = {
3074
+ "workflow.state.doc.ref": {
3075
+ title: "Document reference",
3076
+ description: "Single GDR pointer at a document in this or another resource."
3077
+ },
3078
+ "workflow.state.doc.refs": {
3079
+ title: "Document references",
3080
+ description: "Ordered list of GDR pointers \u2014 multi-doc selection."
3081
+ },
3082
+ "workflow.state.query": {
3083
+ title: "Query result",
3084
+ description: "Snapshot of a GROQ projection captured at slot-resolve time; resolvedAt is stamped on the value."
3085
+ },
3086
+ "workflow.state.value.string": {
3087
+ title: "String value",
3088
+ description: "Free-form string slot."
3089
+ },
3090
+ "workflow.state.value.url": {
3091
+ title: "URL value",
3092
+ description: "Validated URL slot."
3093
+ },
3094
+ "workflow.state.value.number": {
3095
+ title: "Number value",
3096
+ description: "Numeric slot."
3097
+ },
3098
+ "workflow.state.value.boolean": {
3099
+ title: "Boolean value",
3100
+ description: "True/false slot."
3101
+ },
3102
+ "workflow.state.value.dateTime": {
3103
+ title: "Date / time value",
3104
+ description: "ISO-timestamp slot."
3105
+ },
3106
+ "workflow.state.checklist": {
3107
+ title: "Checklist",
3108
+ description: "Append-only list of checkable items; ops add/update/remove entries."
3109
+ },
3110
+ "workflow.state.notes": {
3111
+ title: "Notes log",
3112
+ description: "Append-only structured-note log (signoffs, comments, audit rows)."
3113
+ },
3114
+ "workflow.state.assignees": {
3115
+ title: "Assignees",
3116
+ description: "Ordered list of typed (user|role) assignees."
3117
+ }
3118
+ }, OP_DISPLAY = {
3119
+ "workflow.op.state.set": {
3120
+ title: "Set slot",
3121
+ description: "Overwrite a state slot's value with a resolved Source."
3122
+ },
3123
+ "workflow.op.state.append": {
3124
+ title: "Append to slot",
3125
+ description: "Push a resolved item onto an array-typed slot (notes, checklist, assignees, refs)."
3126
+ },
3127
+ "workflow.op.state.updateWhere": {
3128
+ title: "Update matching rows",
3129
+ description: "Merge fields into rows of an array-typed slot that match the predicate."
3130
+ },
3131
+ "workflow.op.state.removeWhere": {
3132
+ title: "Remove matching rows",
3133
+ description: "Drop rows of an array-typed slot that match the predicate."
3134
+ },
3135
+ "workflow.op.status.set": {
3136
+ title: "Set task status",
3137
+ description: "Flip a task's status explicitly (action.setStatus is the more common path)."
3138
+ }
3139
+ }, EFFECTS_CONTEXT_DISPLAY = {
3140
+ "effectsContext.string": {
3141
+ title: "String",
3142
+ description: "Stable string parameter for effect handlers."
3143
+ },
3144
+ "effectsContext.number": {
3145
+ title: "Number",
3146
+ description: "Stable numeric parameter for effect handlers."
3147
+ },
3148
+ "effectsContext.boolean": {
3149
+ title: "Boolean",
3150
+ description: "Stable boolean parameter for effect handlers."
3151
+ },
3152
+ "effectsContext.ref": {
3153
+ title: "GDR reference",
3154
+ description: "Stable GDR pointer parameter for effect handlers."
3155
+ },
3156
+ "effectsContext.json": {
3157
+ title: "JSON",
3158
+ description: "Stable JSON payload for effect handlers."
3159
+ }
3160
+ }, AUTHORING_DISPLAY = {
3161
+ "workflow.stage": {
3162
+ title: "Stage",
3163
+ description: "A bucket of tasks the workflow is currently in."
3164
+ },
3165
+ "workflow.task": {
3166
+ title: "Task",
3167
+ description: "A unit of work inside a stage; carries actions and may spawn children."
3168
+ },
3169
+ "workflow.action": {
3170
+ title: "Action",
3171
+ description: "An invocable transition on a task \u2014 runs ops, queues effects, may flip status."
3172
+ },
3173
+ "workflow.action.param": {
3174
+ title: "Action param",
3175
+ description: "Caller-supplied parameter on an action; validated before any commit."
3176
+ },
3177
+ "workflow.transition": {
3178
+ title: "Transition",
3179
+ description: "An edge between stages, gated by a filter."
3180
+ },
3181
+ "workflow.predicate": {
3182
+ title: "Predicate",
3183
+ description: "Named GROQ filter, reusable across transitions / tasks / actions."
3184
+ },
3185
+ "workflow.effect": {
3186
+ title: "Effect",
3187
+ description: "Externally-dispatched side effect; queued on commit, drained by a runtime, completed asynchronously."
3188
+ },
3189
+ "workflow.spawn": {
3190
+ title: "Spawn",
3191
+ description: "Declares a task that fans out one or more child workflow instances."
3192
+ },
3193
+ "workflow.pendingEffect": {
3194
+ title: "Pending effect",
3195
+ description: "An effect that has been queued but not yet dispatched."
3196
+ },
3197
+ "workflow.pendingEffect.claim": {
3198
+ title: "Claim",
3199
+ description: "A drainer's lock on a pending effect while it dispatches."
3200
+ },
3201
+ "workflow.definition": {
3202
+ title: "Workflow definition",
3203
+ description: "An immutable workflow blueprint pinned by `workflowId` + `version`."
3204
+ },
3205
+ "workflow.instance": {
3206
+ title: "Workflow instance",
3207
+ description: "A running (or finished) workflow against a subject."
3208
+ }
3209
+ }, DISPLAY = {
3210
+ ...HISTORY_DISPLAY,
3211
+ ...STATE_SLOT_DISPLAY,
3212
+ ...OP_DISPLAY,
3213
+ ...EFFECTS_CONTEXT_DISPLAY,
3214
+ ...AUTHORING_DISPLAY
3215
+ };
3216
+ function displayTitle(typeKey) {
3217
+ return typeKey ? DISPLAY[typeKey]?.title ?? typeKey : "";
3218
+ }
3219
+ function displayDescription(typeKey) {
3220
+ if (typeKey)
3221
+ return DISPLAY[typeKey]?.description;
3222
+ }
3223
+ export {
3224
+ AUTHORING_DISPLAY,
3225
+ ActionDisabledError,
3226
+ ActionParamsInvalidError,
3227
+ DISPLAY,
3228
+ EFFECTS_CONTEXT_DISPLAY,
3229
+ GUARD_LIFTED_PREDICATE,
3230
+ GUARD_OWNER,
3231
+ HISTORY_DISPLAY,
3232
+ MutationGuardDeniedError,
3233
+ OP_DISPLAY,
3234
+ STATE_SLOT_DISPLAY,
3235
+ canonicalTag,
3236
+ compileGuard,
3237
+ createEngine,
3238
+ defaultLoggerFactory,
3239
+ denyingGuards,
3240
+ deployStageGuards,
3241
+ displayDescription,
3242
+ displayTitle,
3243
+ evaluateMutationGuard,
3244
+ extractDocumentId,
3245
+ gdrFromResource,
3246
+ gdrRef,
3247
+ gdrUri,
3248
+ guardMatches,
3249
+ isGdr,
3250
+ lakeGuardId,
3251
+ parseGdr,
3252
+ reconcileStageGuards,
3253
+ refCanvas,
3254
+ refDashboard,
3255
+ refDataset,
3256
+ refMediaLibrary,
3257
+ resolveAccess,
3258
+ retractStageGuards,
3259
+ silentLogger,
3260
+ validateDefinition,
3261
+ validateTags,
3262
+ workflow
3263
+ };
3264
+ //# sourceMappingURL=index.js.map