@sanity/workflow-engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -17,59 +17,15 @@ function _interopNamespaceCompat(e) {
17
17
  }), n.default = e, Object.freeze(n);
18
18
  }
19
19
  var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
20
- function validateDefinition(definition) {
21
- const v2 = createDefinitionValidator();
22
- for (const slot of definition.state ?? [])
23
- v2.checkSlot(slot, `workflow.state "${slot.id}"`);
24
- for (const p of definition.predicates ?? [])
25
- p.groq && v2.checkFilter(p.groq, `predicate "${p.id}"`);
26
- for (const stage of definition.stages)
27
- validateStage(v2, stage);
28
- if (v2.errors.length > 0)
29
- throw new Error(
30
- `defineWorkflow("${definition.workflowId}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
31
- ` + v2.errors.join(`
32
- `)
33
- );
34
- }
35
- function createDefinitionValidator() {
36
- const errors = [], tryParse = (groq, where) => {
37
- try {
38
- groqJs.parse(groq);
39
- } catch (err) {
40
- const message = err instanceof Error ? err.message : String(err);
41
- errors.push(` \xB7 ${where}: ${message}`);
42
- }
43
- }, rejectTypeScan = (groq, where) => {
44
- /\*\s*\[\s*_type\b/.test(groq) && errors.push(
45
- ` \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.`
46
- );
47
- };
48
- return { errors, tryParse, checkFilter: (groq, where) => {
49
- tryParse(groq, where), rejectTypeScan(groq, where);
50
- }, checkSlot: (slot, slotLabel) => {
51
- slot.source.kind === "query" && tryParse(slot.source.query, `${slotLabel}.query`);
52
- } };
20
+ function getPath(value, path) {
21
+ let current = value;
22
+ for (const part of path.split(".")) {
23
+ if (current == null || typeof current != "object") return;
24
+ current = current[part];
25
+ }
26
+ return current;
53
27
  }
54
- function validateStage(v2, stage) {
55
- for (const slot of stage.state ?? [])
56
- v2.checkSlot(slot, `stage "${stage.id}".state "${slot.id}"`);
57
- typeof stage.completion == "string" && v2.tryParse(stage.completion, `stage "${stage.id}".completion`);
58
- for (const t of stage.transitions ?? [])
59
- typeof t.filter == "string" && v2.checkFilter(t.filter, `transition ${stage.id} \u2192 ${t.to} filter`);
60
- for (const task of stage.tasks ?? [])
61
- validateTask(v2, stage.id, task);
62
- }
63
- function validateTask(v2, stageId, task) {
64
- const where = `stage "${stageId}" task "${task.id}"`;
65
- for (const slot of task.state ?? [])
66
- v2.checkSlot(slot, `${where}.state "${slot.id}"`);
67
- typeof task.filter == "string" && v2.checkFilter(task.filter, `${where}.filter`), typeof task.completeWhen == "string" && v2.checkFilter(task.completeWhen, `${where}.completeWhen`);
68
- for (const a of task.actions ?? [])
69
- typeof a.filter == "string" && v2.checkFilter(a.filter, `task "${task.id}" action "${a.id}".filter`);
70
- task.spawns?.forEach?.groq && v2.tryParse(task.spawns.forEach.groq, `task "${task.id}".spawns.forEach.groq`);
71
- }
72
- const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
28
+ const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
73
29
  function parseGdr(uri) {
74
30
  const colon = uri.indexOf(":");
75
31
  if (colon < 0)
@@ -126,12 +82,15 @@ function refMediaLibrary(resourceId, documentId, type) {
126
82
  function refDashboard(resourceId, documentId, type) {
127
83
  return { id: gdrUri({ scheme: "dashboard", resourceId, documentId }), type };
128
84
  }
85
+ function datasetResourceParts(id) {
86
+ const dot = id.indexOf(".");
87
+ if (dot <= 0 || dot === id.length - 1)
88
+ throw new Error(`Invalid dataset resource id "${id}": expected "<projectId>.<dataset>".`);
89
+ return { projectId: id.slice(0, dot), dataset: id.slice(dot + 1) };
90
+ }
129
91
  function gdrFromResource(res, documentId) {
130
92
  if (res.type === "dataset") {
131
- const dot = res.id.indexOf(".");
132
- if (dot < 0)
133
- throw new Error(`Invalid dataset resource id "${res.id}": expected "<projectId>.<dataset>".`);
134
- const projectId = res.id.slice(0, dot), dataset = res.id.slice(dot + 1);
93
+ const { projectId, dataset } = datasetResourceParts(res.id);
135
94
  return gdrUri({ scheme: "dataset", projectId, dataset, documentId });
136
95
  }
137
96
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
@@ -153,191 +112,171 @@ function gdrRef(res, documentId, type) {
153
112
  function isGdr(value) {
154
113
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
155
114
  }
156
- function buildParams(instance, now, extra) {
157
- return {
158
- self: selfGdr(instance),
159
- state: stateMap(instance.state ?? []),
160
- parent: instance.ancestors.at(-1)?.id ?? null,
161
- ancestors: instance.ancestors.map((a) => a.id),
162
- /** Current stage id — bind for filters that scope to the current stage. */
163
- stage: instance.currentStageId,
164
- now,
165
- ...extra
166
- };
167
- }
168
- function stateMap(slots) {
169
- const out = {};
170
- for (const s of slots) out[s.id] = s;
171
- return out;
172
- }
173
- function paramsForLake(params) {
174
- return {
175
- ...params,
176
- self: typeof params.self == "string" ? bareId(params.self) : params.self,
177
- parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
178
- ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
179
- state: stripStateForLake(params.state)
180
- };
181
- }
182
- function stripStateForLake(value) {
183
- if (value == null) return value;
184
- if (typeof value == "string") return bareId(value);
185
- if (Array.isArray(value)) return value.map(stripStateForLake);
186
- if (typeof value == "object") {
187
- const out = {};
188
- for (const [k, v2] of Object.entries(value))
189
- out[k] = stripStateForLake(v2);
190
- return out;
191
- }
192
- return value;
115
+ function resourceOf(p) {
116
+ return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
193
117
  }
194
- function bareId(id) {
195
- return isGdrUri(id) ? extractDocumentId(id) : id;
118
+ const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
119
+ function isGuardReadExpr(expr) {
120
+ return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
196
121
  }
197
- function getPath(value, path) {
198
- let current = value;
199
- for (const part of path.split(".")) {
200
- if (current == null || typeof current != "object") return;
201
- current = current[part];
122
+ function resolveGuardRead(expr, ctx) {
123
+ if (expr === "$self") return selfGdr(ctx.instance);
124
+ if (expr === "$now") return ctx.now;
125
+ const stateRead = STATE_READ.exec(expr);
126
+ if (stateRead !== null) {
127
+ const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
128
+ return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
202
129
  }
203
- return current;
204
- }
205
- function readStateSlot(instance, scope, slotId, stageId, taskId) {
206
- if (scope === "workflow")
207
- return slotValue$1(instance.state?.find((s) => s.id === slotId));
208
- const stageEntry = instance.stages.find(
209
- (s) => s.id === (stageId ?? instance.currentStageId) && s.exitedAt === void 0
130
+ throw new Error(
131
+ `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
210
132
  );
211
- if (stageEntry === void 0) return;
212
- if (scope === "stage")
213
- return slotValue$1(stageEntry.state?.find((s) => s.id === slotId));
214
- const taskEntry = stageEntry.tasks.find((t) => t.id === taskId);
215
- return slotValue$1(taskEntry?.state?.find((s) => s.id === slotId));
216
133
  }
217
- function slotValue$1(slot) {
218
- if (slot != null && typeof slot == "object")
219
- return slot.value;
134
+ function resolveIdRefTarget(expr, ctx) {
135
+ const value = resolveGuardRead(expr, ctx);
136
+ if (typeof value == "string" && isGdrUri(value))
137
+ return { parsed: parseGdr(value) };
138
+ if (value && typeof value == "object") {
139
+ const v2 = value;
140
+ if (typeof v2.id == "string" && isGdrUri(v2.id))
141
+ return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
142
+ }
143
+ return null;
220
144
  }
221
- function resolveStaticSource(src, ctx) {
222
- switch (src.source) {
223
- case "literal":
224
- return { handled: !0, value: src.value };
225
- case "param":
226
- return { handled: !0, value: ctx.params?.[src.paramId] };
227
- case "actor":
228
- return { handled: !0, value: ctx.actor };
229
- case "now":
230
- return { handled: !0, value: ctx.now };
231
- default:
232
- return { handled: !1 };
145
+ function resolveIdRefTargets(idRefs, ctx) {
146
+ const targets = [];
147
+ for (const expr of idRefs ?? []) {
148
+ const target = resolveIdRefTarget(expr, ctx);
149
+ if (target === null) return null;
150
+ targets.push(target);
233
151
  }
152
+ return targets.length === 0 ? null : targets;
234
153
  }
235
- function resolveSource(src, ctx) {
236
- const staticValue = resolveStaticSource(src, ctx);
237
- if (staticValue.handled) return staticValue.value;
238
- switch (src.source) {
239
- case "self":
240
- return ctx.instance._id;
241
- case "stageId":
242
- return ctx.stageId ?? ctx.instance.currentStageId;
243
- case "stateRead":
244
- return resolveStateRead(src, ctx);
245
- case "effectOutput":
246
- return resolveEffectOutput(src, ctx);
247
- case "object":
248
- return resolveObject(src, ctx);
249
- case "row":
250
- case "parentState":
251
- return;
252
- default:
253
- return;
154
+ function assertSingleResource(targets) {
155
+ const resource = resourceOf(targets[0].parsed);
156
+ for (const g of targets) {
157
+ const r = resourceOf(g.parsed);
158
+ if (r.type !== resource.type || r.id !== resource.id)
159
+ throw new Error(
160
+ `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
161
+ );
254
162
  }
163
+ return resource;
255
164
  }
256
- function resolveStateRead(src, ctx) {
257
- const value = readStateSlot(ctx.instance, src.scope, src.slotId, ctx.stageId, ctx.taskId);
258
- return src.path !== void 0 ? getPath(value, src.path) : value;
165
+ function bareIdRefs(targets) {
166
+ return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
259
167
  }
260
- function resolveEffectOutput(src, ctx) {
261
- const entry = ctx.instance.effectsContext.find((e) => e.id === src.contextId);
262
- if (entry === void 0) return null;
263
- const value = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
264
- return value === void 0 ? null : value;
168
+ function resolveMatchTypes(targets, authorTypes) {
169
+ if (authorTypes !== void 0) return authorTypes;
170
+ const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
171
+ return inferred.length > 0 ? inferred : void 0;
265
172
  }
266
- function resolveObject(src, ctx) {
173
+ function resolveMetadata(metadata, ctx) {
267
174
  const out = {};
268
- for (const [field, fieldSrc] of Object.entries(src.fields))
269
- out[field] = resolveSource(fieldSrc, ctx);
175
+ for (const [k, expr] of Object.entries(metadata ?? {}))
176
+ out[k] = resolveGuardRead(expr, ctx);
270
177
  return out;
271
178
  }
272
- async function resolveBindings(args) {
273
- const { bindings, staticInput, instance, now, params, actor } = args, resolved = {};
274
- if (bindings) {
275
- const ctx = {
276
- instance,
277
- now,
278
- ...params !== void 0 ? { params } : {},
279
- ...actor !== void 0 ? { actor } : {}
280
- };
281
- for (const [key, src] of Object.entries(bindings))
282
- resolved[key] = resolveSource(src, ctx);
283
- }
284
- return { ...resolved, ...staticInput };
285
- }
286
- async function evaluateFilter(args) {
287
- const { filter, definition, snapshot, params } = args;
288
- if (filter === void 0) return !0;
289
- if (typeof filter == "string")
290
- return runGroqAgainstSnapshot(filter, params, snapshot);
291
- const predicate = (definition.predicates ?? []).find((p) => p.id === filter.ref);
292
- if (predicate === void 0)
293
- throw new Error(`Unknown predicate ref: ${filter.ref}`);
294
- validatePredicateArgs(predicate, filter.args ?? {});
295
- const merged = { ...params, ...filter.args };
296
- return runGroqAgainstSnapshot(predicate.groq, merged, snapshot);
297
- }
298
- async function runGroqAgainstSnapshot(groq, params, snapshot) {
299
- if (!groq.includes("*")) {
300
- const tree2 = groqJs.parse(groq, { params });
301
- return !!await (await groqJs.evaluate(tree2, { params })).get();
302
- }
303
- const tree = groqJs.parse(groq, { params });
304
- return !!await (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
305
- }
306
- function validatePredicateArgs(predicate, args) {
307
- for (const param of predicate.params ?? [])
308
- validatePredicateParam(predicate.id, param, args);
309
- }
310
- function validatePredicateParam(predicateId, param, args) {
311
- if (!(param.id in args))
312
- throw new Error(`Predicate "${predicateId}" requires param "${param.id}"`);
313
- const value = args[param.id];
314
- if (!matchesParamType(param.type, value))
179
+ function validateDefinition(definition) {
180
+ const v2 = createDefinitionValidator();
181
+ for (const entry of definition.state ?? [])
182
+ v2.checkEntry(entry, `workflow.state "${entry.name}"`);
183
+ for (const [name, groq] of Object.entries(definition.predicates ?? {}))
184
+ v2.checkCondition(groq, `predicate "${name}"`);
185
+ for (const stage of definition.stages)
186
+ validateStage(v2, stage);
187
+ if (v2.errors.length > 0)
315
188
  throw new Error(
316
- `Predicate "${predicateId}" param "${param.id}" must be of type ${param.type}; got ${describeValue(value)}`
189
+ `defineWorkflow("${definition.name}", v${definition.version}): ${v2.errors.length} validation error${v2.errors.length === 1 ? "" : "s"}:
190
+ ` + v2.errors.join(`
191
+ `)
317
192
  );
318
- if (param.enum && !param.enum.includes(value))
319
- throw new Error(
320
- `Predicate "${predicateId}" param "${param.id}" must be one of ${param.enum.join(", ")}; got "${String(value)}"`
193
+ }
194
+ function createDefinitionValidator() {
195
+ const errors = [], tryParse = (groq, where) => {
196
+ try {
197
+ groqJs.parse(groq);
198
+ } catch (err) {
199
+ const message = err instanceof Error ? err.message : String(err);
200
+ errors.push(` \xB7 ${where}: ${message}`);
201
+ }
202
+ }, rejectTypeScan = (groq, where) => {
203
+ /\*\s*\[\s*_type\b/.test(groq) && errors.push(
204
+ ` \xB7 ${where}: condition scans by \`_type\` \u2014 that's a discovery query, not a predicate. Conditions evaluate against the in-memory snapshot (instance + ancestors + state-declared docs). To bring extra docs in scope, declare a \`doc.ref\` (or \`doc.refs\`) state entry on the workflow or this stage. For lake scans like "all articles in this release", use \`subworkflows.forEach\` instead.`
205
+ );
206
+ };
207
+ return { errors, tryParse, checkCondition: (groq, where) => {
208
+ tryParse(groq, where), rejectTypeScan(groq, where);
209
+ }, checkEntry: (entry, label) => {
210
+ entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
211
+ }, checkGuardRead: (expr, where) => {
212
+ isGuardReadExpr(expr) || errors.push(
213
+ ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
321
214
  );
215
+ } };
322
216
  }
323
- function matchesParamType(type, value) {
324
- switch (type) {
325
- case "string":
326
- return typeof value == "string";
327
- case "number":
328
- return typeof value == "number";
329
- case "boolean":
330
- return typeof value == "boolean";
331
- case "string[]":
332
- return Array.isArray(value) && value.every((v2) => typeof v2 == "string");
333
- case "reference":
334
- return isGdr(value);
217
+ function validateStage(v2, stage) {
218
+ for (const entry of stage.state ?? [])
219
+ v2.checkEntry(entry, `stage "${stage.name}".state "${entry.name}"`);
220
+ for (const guard of stage.guards ?? [])
221
+ validateGuardReads(v2, stage.name, guard);
222
+ for (const t of stage.transitions ?? []) {
223
+ v2.checkCondition(t.filter, `transition "${t.name}" (${stage.name} \u2192 ${t.to}) filter`);
224
+ for (const [key, groq] of effectBindings(t.effects))
225
+ v2.tryParse(groq, `transition "${t.name}" effect binding "${key}"`);
335
226
  }
227
+ for (const task of stage.tasks ?? [])
228
+ validateTask(v2, stage.name, task);
229
+ }
230
+ function validateGuardReads(v2, stageName, guard) {
231
+ const where = `stage "${stageName}" guard "${guard.name}"`;
232
+ for (const expr of guard.match.idRefs ?? [])
233
+ v2.checkGuardRead(expr, `${where} match.idRefs`);
234
+ for (const [key, expr] of Object.entries(guard.metadata ?? {}))
235
+ v2.checkGuardRead(expr, `${where} metadata "${key}"`);
236
+ }
237
+ function validateTask(v2, stageName, task) {
238
+ const where = `stage "${stageName}" task "${task.name}"`;
239
+ for (const entry of task.state ?? [])
240
+ v2.checkEntry(entry, `${where}.state "${entry.name}"`);
241
+ task.filter !== void 0 && v2.checkCondition(task.filter, `${where}.filter`), task.completeWhen !== void 0 && v2.checkCondition(task.completeWhen, `${where}.completeWhen`), task.failWhen !== void 0 && v2.checkCondition(task.failWhen, `${where}.failWhen`);
242
+ for (const [key, groq] of effectBindings(task.effects))
243
+ v2.tryParse(groq, `${where} effect binding "${key}"`);
244
+ validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
245
+ }
246
+ function validateTaskActions(v2, task) {
247
+ for (const a of task.actions ?? []) {
248
+ a.filter !== void 0 && v2.checkCondition(a.filter, `task "${task.name}" action "${a.name}".filter`);
249
+ for (const [key, groq] of effectBindings(a.effects))
250
+ v2.tryParse(groq, `task "${task.name}" action "${a.name}" effect binding "${key}"`);
251
+ }
252
+ }
253
+ function validateSubworkflows(v2, where, sub) {
254
+ v2.tryParse(sub.forEach, `${where}.subworkflows.forEach`);
255
+ for (const [key, groq] of Object.entries(sub.with ?? {}))
256
+ v2.tryParse(groq, `${where}.subworkflows.with "${key}"`);
257
+ for (const [key, groq] of Object.entries(sub.context ?? {}))
258
+ v2.tryParse(groq, `${where}.subworkflows.context "${key}"`);
259
+ }
260
+ function effectBindings(effects) {
261
+ return (effects ?? []).flatMap(
262
+ (e) => Object.entries(e.bindings ?? {}).map(([k, groq]) => [`${e.name}.${k}`, groq])
263
+ );
264
+ }
265
+ function actionVerdict(task, action) {
266
+ return {
267
+ task: task.task.name,
268
+ taskStatus: task.status,
269
+ action: action.action.name,
270
+ title: action.action.title,
271
+ allowed: action.allowed,
272
+ disabledReason: action.disabledReason,
273
+ params: action.action.params ?? []
274
+ };
336
275
  }
337
- function describeValue(value) {
338
- return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
276
+ function availableActions(tasks) {
277
+ return tasks.flatMap((t) => t.actions.map((a) => actionVerdict(t, a)));
339
278
  }
340
- const GUARD_DOC_TYPE = "temp.system.guard";
279
+ const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), GUARD_DOC_TYPE = "temp.system.guard";
341
280
  class MutationGuardDeniedError extends Error {
342
281
  denied;
343
282
  documentId;
@@ -346,9 +285,24 @@ class MutationGuardDeniedError extends Error {
346
285
  const ids = args.denied.map((d) => d.guardId).join(", ");
347
286
  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;
348
287
  }
288
+ /**
289
+ * Build the error straight from the denying guard docs — the one mapping
290
+ * from {@link MutationGuardDoc} to the structured `denied` payload, shared
291
+ * by every enforcement site (engine pre-flight, bench write seam).
292
+ */
293
+ static fromGuards(args) {
294
+ return new MutationGuardDeniedError({
295
+ documentId: args.documentId,
296
+ action: args.action,
297
+ denied: args.guards.map((g) => ({
298
+ guardId: g._id,
299
+ ...g.name !== void 0 ? { name: g.name } : {}
300
+ }))
301
+ });
302
+ }
349
303
  }
350
304
  function lakeGuardId(args) {
351
- return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
305
+ return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.guardName}`;
352
306
  }
353
307
  function compileGuard(args) {
354
308
  return {
@@ -358,8 +312,8 @@ function compileGuard(args) {
358
312
  resourceId: args.resourceId,
359
313
  owner: args.owner,
360
314
  sourceInstanceId: args.sourceInstanceId,
361
- sourceDefinitionId: args.sourceDefinitionId,
362
- sourceStageId: args.sourceStageId,
315
+ sourceDefinition: args.sourceDefinition,
316
+ sourceStage: args.sourceStage,
363
317
  ...args.name !== void 0 ? { name: args.name } : {},
364
318
  ...args.description !== void 0 ? { description: args.description } : {},
365
319
  match: args.match,
@@ -407,112 +361,172 @@ async function denyingGuards(args) {
407
361
  guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
408
362
  return denied;
409
363
  }
410
- function resourceOf(p) {
411
- return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
412
- }
413
- function resolveIdRefTarget(src, ctx) {
414
- const value = resolveSource(src, ctx);
415
- if (typeof value == "string" && isGdrUri(value))
416
- return { parsed: parseGdr(value) };
417
- if (value && typeof value == "object") {
418
- const v2 = value;
419
- if (typeof v2.id == "string" && isGdrUri(v2.id))
420
- return typeof v2.type == "string" ? { parsed: parseGdr(v2.id), type: v2.type } : { parsed: parseGdr(v2.id) };
421
- }
422
- return null;
423
- }
424
- function resolveIdRefTargets(idRefs, ctx) {
425
- const targets = [];
426
- for (const src of idRefs ?? []) {
427
- const target = resolveIdRefTarget(src, ctx);
428
- if (target === null) return null;
429
- targets.push(target);
430
- }
431
- return targets.length === 0 ? null : targets;
364
+ async function instanceWriteDenials(args) {
365
+ const { instance, guards, identity } = args, enforceable = guards.filter(
366
+ (g) => g.resourceType === instance.workflowResource.type && g.resourceId === instance.workflowResource.id
367
+ );
368
+ if (enforceable.length === 0) return [];
369
+ const doc = instance;
370
+ return denyingGuards({
371
+ guards: enforceable,
372
+ doc: { id: instance._id, type: instance._type },
373
+ context: {
374
+ action: "update",
375
+ before: doc,
376
+ after: doc,
377
+ ...identity !== void 0 ? { identity } : {}
378
+ }
379
+ });
432
380
  }
433
- function assertSingleResource(targets) {
434
- const resource = resourceOf(targets[0].parsed);
435
- for (const g of targets) {
436
- const r = resourceOf(g.parsed);
437
- if (r.type !== resource.type || r.id !== resource.id)
438
- throw new Error(
439
- `Guard targets span multiple resources (${resource.type}:${resource.id} vs ${r.type}:${r.id}); a guard is single-resource.`
440
- );
441
- }
442
- return resource;
381
+ async function assertInstanceWriteAllowed(args) {
382
+ const denied = await instanceWriteDenials(args);
383
+ if (denied.length !== 0)
384
+ throw MutationGuardDeniedError.fromGuards({
385
+ documentId: args.instance._id,
386
+ action: "update",
387
+ guards: denied
388
+ });
443
389
  }
444
- function bareIdRefs(targets) {
445
- return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
390
+ function findOpenStageEntry(host) {
391
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
446
392
  }
447
- function resolveMatchTypes(targets, authorTypes) {
448
- if (authorTypes !== void 0) return authorTypes;
449
- const inferred = [...new Set(targets.map((g) => g.type).filter((t) => !!t))];
450
- return inferred.length > 0 ? inferred : void 0;
393
+ function buildParams(args) {
394
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
395
+ return {
396
+ self: selfGdr(instance),
397
+ state: renderedState(instance.state ?? [], snapshot),
398
+ parent: instance.ancestors.at(-1)?.id ?? null,
399
+ ancestors: instance.ancestors.map((a) => a.id),
400
+ stage: instance.currentStage,
401
+ now,
402
+ /** `$effects` — parent context handoff by entry name, completed-effect
403
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
404
+ effects: effectsContextMap(instance),
405
+ /** `$tasks` — the current stage's task rows, statuses included. */
406
+ tasks: currentTasks2,
407
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
408
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
409
+ ...extra
410
+ };
451
411
  }
452
- function resolveMetadata(metadata, ctx) {
412
+ function renderedState(entries, snapshot) {
453
413
  const out = {};
454
- for (const [k, src] of Object.entries(metadata ?? {}))
455
- out[k] = resolveSource(src, ctx);
414
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
456
415
  return out;
457
416
  }
458
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
459
- function resolveGuard(guard, index, instance, stageId, now) {
460
- const ctx = { instance, stageId, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
461
- if (targets === null) return null;
462
- const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
463
- return { doc: compileGuard({
464
- id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
465
- resourceType: resource.type,
466
- resourceId: resource.id,
467
- owner: GUARD_OWNER,
468
- sourceInstanceId: instance._id,
469
- sourceDefinitionId: instance.workflowId,
470
- sourceStageId: stageId,
471
- ...guard.name !== void 0 ? { name: guard.name } : {},
472
- ...guard.description !== void 0 ? { description: guard.description } : {},
473
- match: {
474
- ...types !== void 0 ? { types } : {},
475
- idRefs: bareIdRefs(targets),
476
- ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
477
- actions: guard.match.actions
478
- },
479
- predicate: guard.predicate ?? "",
480
- metadata: resolveMetadata(guard.metadata, ctx)
481
- }), routeGdr: targets[0].parsed };
417
+ function renderedValue(entry, snapshot) {
418
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
482
419
  }
483
- async function upsertGuard(client, doc) {
484
- if (!await client.getDocument(doc._id)) {
485
- await client.create(doc);
486
- return;
487
- }
488
- const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
489
- await client.patch(doc._id).set(body).commit();
420
+ function scopedStateOverlay(instance, snapshot, taskName) {
421
+ const stageEntry = findOpenStageEntry(instance);
422
+ if (stageEntry === void 0) return {};
423
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
424
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
490
425
  }
491
- function resolvedStageGuards(args) {
492
- const stage = args.definition.stages.find((s) => s.id === args.stageId), out = [];
493
- for (const [index, guard] of (stage?.guards ?? []).entries()) {
494
- const resolved = resolveGuard(guard, index, args.instance, args.stageId, args.now);
495
- resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
496
- }
426
+ function assignedFor(instance, taskName, actor) {
427
+ if (actor === void 0) return !1;
428
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
429
+ return entry === void 0 ? !1 : entry.value.some(
430
+ (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
431
+ );
432
+ }
433
+ function effectsContextMap(instance) {
434
+ const out = {};
435
+ for (const entry of instance.effectsContext)
436
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
497
437
  return out;
498
438
  }
499
- async function committedStageId(args) {
500
- return (await args.client.getDocument(args.instance._id))?.currentStageId;
439
+ function parseJsonContextEntry(entry) {
440
+ try {
441
+ return JSON.parse(entry.value);
442
+ } catch (err) {
443
+ throw new Error(
444
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
445
+ { cause: err }
446
+ );
447
+ }
501
448
  }
502
- async function deployStageGuards(args) {
503
- if (await committedStageId(args) === args.stageId)
504
- for (const { client, doc } of resolvedStageGuards(args))
505
- await upsertGuard(client, doc);
449
+ function paramsForLake(params) {
450
+ return {
451
+ ...params,
452
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
453
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
454
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
455
+ state: stripStateForLake(params.state)
456
+ };
506
457
  }
507
- async function retractStageGuards(args) {
508
- const live = await committedStageId(args);
509
- if (!(live === void 0 || live === args.stageId))
510
- for (const { client, doc } of resolvedStageGuards(args))
511
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
458
+ function stripStateForLake(value) {
459
+ if (value == null) return value;
460
+ if (typeof value == "string") return bareId(value);
461
+ if (Array.isArray(value)) return value.map(stripStateForLake);
462
+ if (typeof value == "object") {
463
+ const out = {};
464
+ for (const [k, v2] of Object.entries(value))
465
+ out[k] = stripStateForLake(v2);
466
+ return out;
467
+ }
468
+ return value;
512
469
  }
513
- function randomKey(length = 12) {
514
- const bytes = new Uint8Array(length);
515
- return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
470
+ function bareId(id) {
471
+ return isGdrUri(id) ? extractDocumentId(id) : id;
472
+ }
473
+ function abortReason(instance) {
474
+ return instance.history.find(
475
+ (h) => h._type === "aborted"
476
+ )?.reason;
477
+ }
478
+ function diagnoseInputFromEvaluation(evaluation) {
479
+ return {
480
+ instance: evaluation.instance,
481
+ tasks: evaluation.currentStage.tasks,
482
+ transitions: evaluation.currentStage.transitions
483
+ };
484
+ }
485
+ function openStage(instance) {
486
+ return findOpenStageEntry(instance);
487
+ }
488
+ function assigneesOf(stage, taskName) {
489
+ return (stage?.tasks.find((t) => t.name === taskName)?.state ?? []).filter((s) => s._type === "assignees").flatMap((s) => s.value);
490
+ }
491
+ function isResolved(status) {
492
+ return status === "done" || status === "skipped";
493
+ }
494
+ function failedEffectCause(input) {
495
+ const stalled = new Set(input.tasks.filter((t) => !isResolved(t.status)).map((t) => t.task.name)), effect = [...input.instance.effectHistory].reverse().find((e) => e.status === "failed" && e.origin.kind === "task" && stalled.has(e.origin.name));
496
+ return effect ? { kind: "failed-effect", effect } : void 0;
497
+ }
498
+ function hungEffectCause(input) {
499
+ const effect = input.instance.pendingEffects.find((e) => e.claim !== void 0);
500
+ return effect ? { kind: "hung-effect", effect } : void 0;
501
+ }
502
+ function failedTaskCause(input) {
503
+ const failed = input.tasks.find((t) => t.status === "failed");
504
+ return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
505
+ }
506
+ function waitingState(input) {
507
+ const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
508
+ if (active !== void 0)
509
+ return {
510
+ state: "waiting",
511
+ task: active.task.name,
512
+ assignees: assigneesOf(openStage(input.instance), active.task.name),
513
+ actions: (active.task.actions ?? []).map((a) => a.name)
514
+ };
515
+ }
516
+ function noTransitionFiresCause(input) {
517
+ if (input.transitions.length !== 0 && !input.transitions.some((t) => t.filterSatisfied) && input.tasks.every((t) => isResolved(t.status)))
518
+ return { kind: "no-transition-fires" };
519
+ }
520
+ function diagnoseInstance(input) {
521
+ const { instance } = input;
522
+ if (instance.abortedAt !== void 0) {
523
+ const reason = abortReason(instance);
524
+ return { state: "aborted", at: instance.abortedAt, ...reason !== void 0 ? { reason } : {} };
525
+ }
526
+ if (instance.completedAt !== void 0)
527
+ return { state: "completed", at: instance.completedAt };
528
+ const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
529
+ return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
516
530
  }
517
531
  function buildSnapshot(args) {
518
532
  const docs = [], knownIds = /* @__PURE__ */ new Set();
@@ -534,48 +548,59 @@ function rewriteRefsRecursive(value, resource) {
534
548
  k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
535
549
  return out;
536
550
  }
537
- function findOpenStageEntry(host) {
538
- return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
551
+ const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
552
+ function parseDefinitionSnapshot(instance) {
553
+ try {
554
+ return JSON.parse(instance.definitionSnapshot);
555
+ } catch (err) {
556
+ throw new Error(
557
+ `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
558
+ { cause: err }
559
+ );
560
+ }
539
561
  }
540
562
  function collectWatchRefs(instance) {
541
563
  const stage = findOpenStageEntry(instance);
542
564
  return [
543
565
  gdrRef(instance.workflowResource, instance._id, instance._type),
544
566
  ...instance.ancestors,
545
- ...slotDocRefs(instance.state),
546
- ...slotDocRefs(stage?.state),
547
- ...slotReleaseRefs(instance.state),
548
- ...slotReleaseRefs(stage?.state)
567
+ ...entryDocRefs(instance.state),
568
+ ...entryDocRefs(stage?.state),
569
+ ...entryReleaseRefs(instance.state),
570
+ ...entryReleaseRefs(stage?.state)
549
571
  ];
550
572
  }
551
573
  function readsRaw(ref) {
552
- return ref.type === "workflow.instance" || ref.type === "system.release";
574
+ return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
553
575
  }
554
576
  function contentReleaseName(args) {
555
577
  const { ref, perspective } = args;
556
578
  if (!readsRaw(ref) && Array.isArray(perspective))
557
579
  return perspective.find((entry) => entry !== "drafts" && entry !== "published" && entry !== "raw");
558
580
  }
581
+ function subscriptionDocument(ref) {
582
+ return { ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type };
583
+ }
559
584
  function subscriptionDocumentsForInstance(instance) {
560
585
  const seen = /* @__PURE__ */ new Set(), documents = [];
561
586
  for (const ref of collectWatchRefs(instance))
562
- !isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push({ ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type }));
587
+ !isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push(subscriptionDocument(ref)));
563
588
  return {
564
589
  documents,
565
590
  ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
566
591
  };
567
592
  }
568
- function slotDocRefs(slots) {
569
- return slotEntries(slots).flatMap((slot) => slot._type === "workflow.state.doc.ref" ? isGdr(slot.value) ? [slot.value] : [] : slot._type === "workflow.state.doc.refs" ? Array.isArray(slot.value) ? slot.value.filter(isGdr) : [] : []);
593
+ function entryDocRefs(state) {
594
+ return stateEntries(state).flatMap((entry) => entry._type === "doc.ref" ? isGdr(entry.value) ? [entry.value] : [] : entry._type === "doc.refs" ? Array.isArray(entry.value) ? entry.value.filter(isGdr) : [] : []);
570
595
  }
571
- function slotReleaseRefs(slots) {
572
- return slotEntries(slots).flatMap(
573
- (slot) => slot._type === "workflow.state.release.ref" && isGdr(slot.value) ? [slot.value] : []
596
+ function entryReleaseRefs(state) {
597
+ return stateEntries(state).flatMap(
598
+ (entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
574
599
  );
575
600
  }
576
- function slotEntries(slots) {
577
- return Array.isArray(slots) ? slots.filter(
578
- (slot) => !!slot && typeof slot == "object"
601
+ function stateEntries(state) {
602
+ return Array.isArray(state) ? state.filter(
603
+ (entry) => !!entry && typeof entry == "object"
579
604
  ) : [];
580
605
  }
581
606
  async function hydrateSnapshot(args) {
@@ -617,17 +642,219 @@ async function readDoc(client, id, perspective) {
617
642
  function resourceFromParsed(parsed) {
618
643
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
619
644
  }
620
- function collectSlotDocUris(resolvedStateSlots) {
621
- return slotDocRefs(resolvedStateSlots).map((ref) => ref.id);
645
+ function collectEntryDocUris(resolvedStateEntries) {
646
+ return entryDocRefs(resolvedStateEntries).map((ref) => ref.id);
647
+ }
648
+ const resourceKey = (r) => `${r.type}.${r.id}`;
649
+ function guardsForResource(client) {
650
+ return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
651
+ }
652
+ function instanceGuardQuery(instanceId) {
653
+ return {
654
+ query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
655
+ params: { guardType: GUARD_DOC_TYPE, instanceId }
656
+ };
657
+ }
658
+ function verdictGuardsForInstance(client, instanceId) {
659
+ const { query, params } = instanceGuardQuery(instanceId);
660
+ return client.fetch(query, params);
661
+ }
662
+ async function guardsForInstance(args) {
663
+ const { client, clientForGdr, instance } = args, stateUris = [
664
+ ...collectEntryDocUris(instance.state),
665
+ ...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
666
+ ], clients = resourceClientMap(
667
+ instance.workflowResource,
668
+ client,
669
+ clientForGdr,
670
+ stateUris.map((uri) => tryParseGdr(uri))
671
+ ), { query, params } = instanceGuardQuery(instance._id);
672
+ return queryGuardsAcross(clients.values(), query, params);
673
+ }
674
+ async function guardsForDefinition(args) {
675
+ const perClient = await guardsForDefinitionByClient(args);
676
+ return dedupById(perClient.flatMap((entry) => entry.guards));
677
+ }
678
+ async function guardsForDefinitionByClient(args) {
679
+ const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
680
+ (def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
681
+ ), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs), query = "*[_type == $t && sourceDefinition == $definition]", params = { t: GUARD_DOC_TYPE, definition };
682
+ return Promise.all(
683
+ [...clients.values()].map(async (resourceClient) => ({
684
+ client: resourceClient,
685
+ guards: await resourceClient.fetch(query, params)
686
+ }))
687
+ );
688
+ }
689
+ function guardIdRefs(definition) {
690
+ return definition.stages.flatMap(
691
+ (stage) => (stage.guards ?? []).flatMap(
692
+ (guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
693
+ )
694
+ );
695
+ }
696
+ function staticIdRefGdr(idRef, stage, definition) {
697
+ const read = /^\$state\.([\w-]+)/.exec(idRef);
698
+ if (read === null) return;
699
+ const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
700
+ return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
701
+ }
702
+ function entriesInScope(stage, definition) {
703
+ return [
704
+ ...definition.state ?? [],
705
+ ...stage.state ?? [],
706
+ ...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
707
+ ];
708
+ }
709
+ function gdrFromValue(value) {
710
+ if (typeof value == "string") return tryParseGdr(value);
711
+ if (typeof value == "object" && value !== null && "id" in value) {
712
+ const id = value.id;
713
+ return typeof id == "string" ? tryParseGdr(id) : void 0;
714
+ }
715
+ }
716
+ function tryParseGdr(uri) {
717
+ try {
718
+ return parseGdr(uri);
719
+ } catch {
720
+ return;
721
+ }
722
+ }
723
+ function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
724
+ const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
725
+ for (const parsed of gdrs) {
726
+ if (parsed === void 0) continue;
727
+ const key = resourceKey(resourceFromParsed(parsed));
728
+ map.has(key) || map.set(key, clientForGdr(parsed));
729
+ }
730
+ return map;
731
+ }
732
+ async function queryGuardsAcross(clients, query, params) {
733
+ const perResource = await Promise.all(
734
+ [...clients].map((c) => c.fetch(query, params))
735
+ );
736
+ return dedupById(perResource.flat());
737
+ }
738
+ function dedupById(guards) {
739
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
740
+ }
741
+ const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
742
+ function validateTags(tags) {
743
+ if (tags.length === 0)
744
+ throw new Error("tags: must be a non-empty array");
745
+ for (const tag of tags)
746
+ if (!TAG_RE.test(tag))
747
+ throw new Error(
748
+ `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
749
+ );
750
+ }
751
+ function canonicalTag(tags) {
752
+ return tags[0];
753
+ }
754
+ function tagScopeFilter(param = "engineTags") {
755
+ return `count(tags[@ in $${param}]) > 0`;
756
+ }
757
+ const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
758
+ function resolveGuard(guard, instance, stageName, now) {
759
+ const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
760
+ if (targets === null) return null;
761
+ const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
762
+ return { doc: compileGuard({
763
+ id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
764
+ resourceType: resource.type,
765
+ resourceId: resource.id,
766
+ owner: GUARD_OWNER,
767
+ sourceInstanceId: instance._id,
768
+ sourceDefinition: instance.definition,
769
+ sourceStage: stageName,
770
+ name: guard.name,
771
+ ...guard.description !== void 0 ? { description: guard.description } : {},
772
+ match: {
773
+ ...types !== void 0 ? { types } : {},
774
+ idRefs: bareIdRefs(targets),
775
+ ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
776
+ actions: guard.match.actions
777
+ },
778
+ predicate: guard.predicate ?? "",
779
+ metadata: resolveMetadata(guard.metadata, ctx)
780
+ }), routeGdr: targets[0].parsed };
781
+ }
782
+ async function upsertGuard(client, doc) {
783
+ if (!await client.getDocument(doc._id)) {
784
+ await client.create(doc);
785
+ return;
786
+ }
787
+ const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
788
+ await client.patch(doc._id).set(body).commit();
789
+ }
790
+ function resolvedStageGuards(args) {
791
+ const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
792
+ for (const guard of stage?.guards ?? []) {
793
+ const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
794
+ resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
795
+ }
796
+ return out;
797
+ }
798
+ async function committedInstance(args) {
799
+ return await args.client.getDocument(args.instance._id) ?? void 0;
800
+ }
801
+ async function deployStageGuards(args) {
802
+ const live = await committedInstance(args);
803
+ if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
804
+ for (const { client, doc } of resolvedStageGuards(args))
805
+ await upsertGuard(client, doc);
806
+ }
807
+ async function retractStageGuards(args) {
808
+ const live = await committedInstance(args);
809
+ if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
810
+ for (const { client, doc } of resolvedStageGuards(args))
811
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
812
+ }
813
+ async function deleteOrphanedDefinitionGuards(args) {
814
+ const { client, clientForGdr, workflowResource, definition, definitions, tags } = args, perClient = await guardsForDefinitionByClient({
815
+ client,
816
+ clientForGdr,
817
+ workflowResource,
818
+ definition,
819
+ definitions
820
+ }), ownPartitionPrefix = `${canonicalTag(tags)}.`;
821
+ let count = 0;
822
+ for (const { client: resourceClient, guards } of perClient) {
823
+ const own = guards.filter((guard) => guard.sourceInstanceId.startsWith(ownPartitionPrefix));
824
+ if (own.length === 0) continue;
825
+ const tx = resourceClient.transaction();
826
+ for (const guard of own) tx.delete(guard._id);
827
+ await tx.commit(), count += own.length;
828
+ }
829
+ return count;
830
+ }
831
+ function randomKey(length = 12) {
832
+ const bytes = new Uint8Array(length);
833
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
834
+ }
835
+ async function evaluateCondition(args) {
836
+ const { condition, snapshot, params } = args;
837
+ return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
622
838
  }
623
- const WORKFLOW_INSTANCE_TYPE = "workflow.instance";
624
- class SlotValueShapeError extends Error {
625
- slotType;
626
- slotId;
839
+ async function evaluatePredicates(args) {
840
+ const out = {};
841
+ for (const [name, groq] of Object.entries(args.predicates ?? {}))
842
+ out[name] = !!await runGroq(groq, args.params, args.snapshot);
843
+ return out;
844
+ }
845
+ async function runGroq(groq, params, snapshot) {
846
+ const tree = groqJs.parse(groq, { params });
847
+ return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
848
+ }
849
+ class StateValueShapeError extends Error {
850
+ entryType;
851
+ entryName;
627
852
  issues;
628
853
  constructor(args) {
629
854
  const issueText = args.issues.join("; ");
630
- super(`Slot ${args.mode} shape invalid for "${args.slotId}" (${args.slotType}): ${issueText}`), this.name = "SlotValueShapeError", this.slotType = args.slotType, this.slotId = args.slotId, this.issues = args.issues;
855
+ super(
856
+ `State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
857
+ ), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
631
858
  }
632
859
  }
633
860
  const GdrShape = v__namespace.looseObject({
@@ -649,8 +876,8 @@ const GdrShape = v__namespace.looseObject({
649
876
  roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
650
877
  onBehalfOf: v__namespace.optional(v__namespace.string())
651
878
  }), AssigneeShape = v__namespace.union([
652
- v__namespace.looseObject({ kind: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
653
- v__namespace.looseObject({ kind: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
879
+ v__namespace.looseObject({ type: v__namespace.literal("user"), id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) }),
880
+ v__namespace.looseObject({ type: v__namespace.literal("role"), role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)) })
654
881
  ]), ChecklistItemShape = v__namespace.looseObject({
655
882
  label: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
656
883
  done: v__namespace.boolean(),
@@ -672,59 +899,59 @@ const GdrShape = v__namespace.looseObject({
672
899
  v__namespace.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
673
900
  )
674
901
  ]), NullableUrl = NullableString, valueSchemas = {
675
- "workflow.state.doc.ref": v__namespace.union([v__namespace.null(), GdrShape]),
676
- "workflow.state.doc.refs": v__namespace.array(GdrShape),
677
- "workflow.state.release.ref": v__namespace.union([v__namespace.null(), ReleaseRefShape]),
678
- "workflow.state.query": v__namespace.any(),
679
- "workflow.state.value.string": NullableString,
680
- "workflow.state.value.url": NullableUrl,
681
- "workflow.state.value.number": NullableNumber,
682
- "workflow.state.value.boolean": NullableBoolean,
683
- "workflow.state.value.dateTime": NullableDateTime,
684
- "workflow.state.value.actor": v__namespace.union([v__namespace.null(), ActorShape]),
685
- "workflow.state.checklist": v__namespace.array(ChecklistItemShape),
686
- "workflow.state.notes": v__namespace.array(NoteItemShape),
687
- "workflow.state.assignees": v__namespace.array(AssigneeShape)
902
+ "doc.ref": v__namespace.union([v__namespace.null(), GdrShape]),
903
+ "doc.refs": v__namespace.array(GdrShape),
904
+ "release.ref": v__namespace.union([v__namespace.null(), ReleaseRefShape]),
905
+ query: v__namespace.any(),
906
+ "value.string": NullableString,
907
+ "value.url": NullableUrl,
908
+ "value.number": NullableNumber,
909
+ "value.boolean": NullableBoolean,
910
+ "value.dateTime": NullableDateTime,
911
+ "value.actor": v__namespace.union([v__namespace.null(), ActorShape]),
912
+ checklist: v__namespace.array(ChecklistItemShape),
913
+ notes: v__namespace.array(NoteItemShape),
914
+ assignees: v__namespace.array(AssigneeShape)
688
915
  }, itemSchemas = {
689
- "workflow.state.doc.refs": GdrShape,
690
- "workflow.state.checklist": ChecklistItemShape,
691
- "workflow.state.notes": NoteItemShape,
692
- "workflow.state.assignees": AssigneeShape
916
+ "doc.refs": GdrShape,
917
+ checklist: ChecklistItemShape,
918
+ notes: NoteItemShape,
919
+ assignees: AssigneeShape
693
920
  };
694
- function isAppendable(slotType) {
695
- return slotType in itemSchemas;
921
+ function isAppendable(entryType) {
922
+ return entryType in itemSchemas;
696
923
  }
697
- function validateSlotValue(args) {
698
- const schema2 = valueSchemas[args.slotType];
924
+ function validateStateValue(args) {
925
+ const schema2 = valueSchemas[args.entryType];
699
926
  if (schema2 === void 0)
700
- throw new SlotValueShapeError({
701
- slotType: args.slotType,
702
- slotId: args.slotId,
703
- issues: [`unknown slot type ${args.slotType}`],
927
+ throw new StateValueShapeError({
928
+ entryType: args.entryType,
929
+ entryName: args.entryName,
930
+ issues: [`unknown state entry type ${args.entryType}`],
704
931
  mode: "value"
705
932
  });
706
933
  const result = v__namespace.safeParse(schema2, args.value);
707
934
  if (!result.success)
708
- throw new SlotValueShapeError({
709
- slotType: args.slotType,
710
- slotId: args.slotId,
935
+ throw new StateValueShapeError({
936
+ entryType: args.entryType,
937
+ entryName: args.entryName,
711
938
  issues: formatIssues(result.issues),
712
939
  mode: "value"
713
940
  });
714
941
  }
715
- function validateSlotAppendItem(args) {
716
- if (!isAppendable(args.slotType))
717
- throw new SlotValueShapeError({
718
- slotType: args.slotType,
719
- slotId: args.slotId,
720
- issues: [`slot type ${args.slotType} does not support append`],
942
+ function validateStateAppendItem(args) {
943
+ if (!isAppendable(args.entryType))
944
+ throw new StateValueShapeError({
945
+ entryType: args.entryType,
946
+ entryName: args.entryName,
947
+ issues: [`state entry type ${args.entryType} does not support append`],
721
948
  mode: "item"
722
949
  });
723
- const schema2 = itemSchemas[args.slotType], result = v__namespace.safeParse(schema2, args.item);
950
+ const schema2 = itemSchemas[args.entryType], result = v__namespace.safeParse(schema2, args.item);
724
951
  if (!result.success)
725
- throw new SlotValueShapeError({
726
- slotType: args.slotType,
727
- slotId: args.slotId,
952
+ throw new StateValueShapeError({
953
+ entryType: args.entryType,
954
+ entryName: args.entryName,
728
955
  issues: formatIssues(result.issues),
729
956
  mode: "item"
730
957
  });
@@ -735,115 +962,115 @@ function formatIssues(issues) {
735
962
  return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
736
963
  });
737
964
  }
738
- function derivePerspectiveFromState(slots) {
739
- if (slots !== void 0) {
740
- for (const slot of slots)
741
- if (slot._type === "workflow.state.release.ref" && slot.value !== null) {
742
- const releaseName = slot.value.releaseName;
965
+ function derivePerspectiveFromState(entries) {
966
+ if (entries !== void 0) {
967
+ for (const entry of entries)
968
+ if (entry._type === "release.ref" && entry.value !== null) {
969
+ const releaseName = entry.value.releaseName;
743
970
  if (typeof releaseName == "string" && releaseName.length > 0)
744
971
  return [releaseName];
745
972
  }
746
973
  }
747
974
  }
748
975
  async function resolveDeclaredState(args) {
749
- const { slotDefs, initialState, ctx, randomKey: randomKey2 } = args;
750
- if (slotDefs === void 0 || slotDefs.length === 0) return [];
976
+ const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
977
+ if (entryDefs === void 0 || entryDefs.length === 0) return [];
751
978
  const out = [];
752
- for (const slot of slotDefs) {
979
+ for (const entry of entryDefs) {
753
980
  const scopedCtx = { ...ctx, resolvedState: out };
754
- out.push(await resolveOneSlot(slot, initialState, scopedCtx, randomKey2));
981
+ out.push(await resolveOneEntry(entry, initialState, scopedCtx, randomKey2));
755
982
  }
756
983
  return out;
757
984
  }
758
985
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
759
- "workflow.state.doc.refs",
760
- "workflow.state.checklist",
761
- "workflow.state.notes",
762
- "workflow.state.assignees"
986
+ "doc.refs",
987
+ "checklist",
988
+ "notes",
989
+ "assignees"
763
990
  ]);
764
- function defaultSlotValue(slotType) {
765
- return ARRAY_SLOT_TYPES.has(slotType) ? [] : null;
991
+ function defaultEntryValue(entryType) {
992
+ return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
766
993
  }
767
- function resolveInitValue(slot, initialState, defaultValue) {
768
- const initMatch = initialState.find((s) => s.id === slot.id && s.type === slot.type);
769
- return initMatch === void 0 ? defaultValue : (assertInitValueShape(slot, initMatch.value), initMatch.value);
994
+ function resolveInitValue(entry, initialState, defaultValue) {
995
+ const initMatch = initialState.find((s) => s.name === entry.name && s.type === entry.type);
996
+ return initMatch === void 0 ? defaultValue : (assertInitValueShape(entry, initMatch.value), initMatch.value);
770
997
  }
771
998
  function resolveStateReadValue(source, ctx, defaultValue) {
772
- const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.id === source.slotId), targetValue = target === void 0 ? void 0 : target.value;
999
+ const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.name === source.state), targetValue = target === void 0 ? void 0 : target.value;
773
1000
  return (source.path !== void 0 ? getPath(targetValue, source.path) : targetValue) ?? defaultValue;
774
1001
  }
775
- async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
1002
+ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
776
1003
  const params = paramsForLake({
777
1004
  self: ctx.selfId ?? null,
778
1005
  state: stateMapFromResolved(ctx.resolvedState ?? []),
779
- stage: ctx.stageId ?? null,
780
- task: ctx.taskId ?? null,
1006
+ stage: ctx.stageName ?? null,
1007
+ task: ctx.taskName ?? null,
781
1008
  now: ctx.now,
782
1009
  engineTags: ctx.tags ?? []
783
1010
  });
784
1011
  try {
785
1012
  const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
786
- return normalizeQueryResult(slot.type, result, ctx.workflowResource) ?? defaultValue;
1013
+ return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
787
1014
  } catch (err) {
788
1015
  throw new Error(
789
- `Failed to resolve query slot "${slot.id}" (${slot.type}): ${err instanceof Error ? err.message : String(err)}`,
1016
+ `Failed to resolve query entry "${entry.name}" (${entry.type}): ${err instanceof Error ? err.message : String(err)}`,
790
1017
  { cause: err }
791
1018
  );
792
1019
  }
793
1020
  }
794
- async function resolveSlotValue(slot, initialState, ctx, defaultValue) {
795
- const source = slot.source;
796
- return source.kind === "init" ? resolveInitValue(slot, initialState, defaultValue) : source.kind === "literal" ? source.value ?? defaultValue : source.kind === "stateRead" ? resolveStateReadValue(source, ctx, defaultValue) : source.kind === "query" && ctx.client !== void 0 ? resolveQueryValue(slot, source, ctx, ctx.client, defaultValue) : defaultValue;
1021
+ async function resolveEntryValue(entry, initialState, ctx, defaultValue) {
1022
+ const source = entry.source;
1023
+ return source.type === "init" ? resolveInitValue(entry, initialState, defaultValue) : source.type === "literal" ? source.value ?? defaultValue : source.type === "stateRead" ? resolveStateReadValue(source, ctx, defaultValue) : source.type === "query" && ctx.client !== void 0 ? resolveQueryValue(entry, source, ctx, ctx.client, defaultValue) : defaultValue;
797
1024
  }
798
- function buildResolvedSlot(slot, value, _key, now) {
799
- const titleProp = slot.title !== void 0 ? { title: slot.title } : {}, descriptionProp = slot.description !== void 0 ? { description: slot.description } : {};
800
- return slot.type === "workflow.state.query" ? {
1025
+ function buildResolvedEntry(entry, value, _key, now) {
1026
+ const titleProp = entry.title !== void 0 ? { title: entry.title } : {}, descriptionProp = entry.description !== void 0 ? { description: entry.description } : {};
1027
+ return entry.type === "query" ? {
801
1028
  _key,
802
- _type: "workflow.state.query",
803
- id: slot.id,
1029
+ _type: "query",
1030
+ name: entry.name,
804
1031
  ...titleProp,
805
1032
  ...descriptionProp,
806
1033
  value,
807
1034
  resolvedAt: now
808
1035
  } : {
809
1036
  _key,
810
- _type: slot.type,
811
- id: slot.id,
1037
+ _type: entry.type,
1038
+ name: entry.name,
812
1039
  ...titleProp,
813
1040
  ...descriptionProp,
814
1041
  value
815
1042
  };
816
1043
  }
817
- async function resolveOneSlot(slot, initialState, ctx, randomKey2) {
818
- const defaultValue = defaultSlotValue(slot.type), value = await resolveSlotValue(slot, initialState, ctx, defaultValue);
819
- return validateSlotValue({ slotType: slot.type, slotId: slot.id, value }), buildResolvedSlot(slot, value, randomKey2(), ctx.now);
1044
+ async function resolveOneEntry(entry, initialState, ctx, randomKey2) {
1045
+ const defaultValue = defaultEntryValue(entry.type), value = await resolveEntryValue(entry, initialState, ctx, defaultValue);
1046
+ return validateStateValue({ entryType: entry.type, entryName: entry.name, value }), buildResolvedEntry(entry, value, randomKey2(), ctx.now);
820
1047
  }
821
- function stateMapFromResolved(slots) {
1048
+ function stateMapFromResolved(entries) {
822
1049
  const out = {};
823
- for (const s of slots) out[s.id] = s;
1050
+ for (const s of entries) out[s.name] = s.value;
824
1051
  return out;
825
1052
  }
826
- function assertInitValueShape(slot, value) {
827
- if (slot.type === "workflow.state.doc.ref") {
828
- assertGdrShape(value, `state slot "${slot.id}" (workflow.state.doc.ref)`);
1053
+ function assertInitValueShape(entry, value) {
1054
+ if (entry.type === "doc.ref") {
1055
+ assertGdrShape(value, `state entry "${entry.name}" (doc.ref)`);
829
1056
  return;
830
1057
  }
831
- if (slot.type === "workflow.state.release.ref") {
832
- assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
1058
+ if (entry.type === "release.ref") {
1059
+ assertGdrShape(value, `state entry "${entry.name}" (release.ref)`);
833
1060
  const v2 = value;
834
1061
  if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
835
1062
  throw new Error(
836
- `Invalid init value for state slot "${slot.id}" (workflow.state.release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
1063
+ `Invalid init value for state entry "${entry.name}" (release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
837
1064
  );
838
1065
  return;
839
1066
  }
840
- if (slot.type === "workflow.state.doc.refs") {
1067
+ if (entry.type === "doc.refs") {
841
1068
  if (!Array.isArray(value))
842
1069
  throw new Error(
843
- `Invalid init value for state slot "${slot.id}" (workflow.state.doc.refs): expected an array of GDRs, got ${typeof value}.`
1070
+ `Invalid init value for state entry "${entry.name}" (doc.refs): expected an array of GDRs, got ${typeof value}.`
844
1071
  );
845
1072
  for (const [i, item] of value.entries())
846
- assertGdrShape(item, `state slot "${slot.id}" (workflow.state.doc.refs) item [${i}]`);
1073
+ assertGdrShape(item, `state entry "${entry.name}" (doc.refs) item [${i}]`);
847
1074
  }
848
1075
  }
849
1076
  function assertGdrShape(value, context) {
@@ -861,8 +1088,8 @@ function assertGdrShape(value, context) {
861
1088
  `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
862
1089
  );
863
1090
  }
864
- function normalizeQueryResult(slotType, raw, workflowResource) {
865
- return raw == null ? raw : slotType === "workflow.state.doc.ref" ? coerceToGdr(raw, workflowResource) : slotType === "workflow.state.doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v2) => v2 !== null) : [] : raw;
1091
+ function normalizeQueryResult(entryType, raw, workflowResource) {
1092
+ return raw == null ? raw : entryType === "doc.ref" ? coerceToGdr(raw, workflowResource) : entryType === "doc.refs" ? Array.isArray(raw) ? raw.map((item) => coerceToGdr(item, workflowResource)).filter((v2) => v2 !== null) : [] : raw;
866
1093
  }
867
1094
  function toGdrUri(docId, workflowResource) {
868
1095
  return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
@@ -887,7 +1114,7 @@ async function loadContext(client, instanceId, options) {
887
1114
  const instance = await client.getDocument(instanceId);
888
1115
  if (!instance)
889
1116
  throw new Error(`Workflow instance ${instanceId} not found`);
890
- const definition = JSON.parse(instance.definitionSnapshot), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
1117
+ const definition = parseDefinitionSnapshot(instance), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
891
1118
  client,
892
1119
  clientForGdr,
893
1120
  instance,
@@ -895,12 +1122,28 @@ async function loadContext(client, instanceId, options) {
895
1122
  });
896
1123
  return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
897
1124
  }
898
- function ctxEvaluateFilter(ctx, filter) {
899
- return evaluateFilter({
900
- filter,
901
- definition: ctx.definition,
1125
+ async function ctxConditionParams(ctx, opts) {
1126
+ const base = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), state = {
1127
+ ...base.state,
1128
+ ...scopedStateOverlay(ctx.instance, ctx.snapshot, opts?.taskName)
1129
+ }, params = {
1130
+ ...base,
1131
+ state,
1132
+ actor: opts?.actor,
1133
+ assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
1134
+ ...opts?.vars
1135
+ };
1136
+ return { ...await evaluatePredicates({
1137
+ predicates: ctx.definition.predicates,
1138
+ snapshot: ctx.snapshot,
1139
+ params
1140
+ }), ...params };
1141
+ }
1142
+ async function ctxEvaluateCondition(ctx, condition, opts) {
1143
+ return condition === void 0 ? !0 : evaluateCondition({
1144
+ condition,
902
1145
  snapshot: ctx.snapshot,
903
- params: buildParams(ctx.instance, ctx.now)
1146
+ params: await ctxConditionParams(ctx, opts)
904
1147
  });
905
1148
  }
906
1149
  async function buildEngineContext(args) {
@@ -915,129 +1158,142 @@ async function buildEngineContext(args) {
915
1158
  snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
916
1159
  };
917
1160
  }
918
- function findStage(definition, stageId) {
919
- const stage = definition.stages.find((s) => s.id === stageId);
1161
+ function findStage(definition, stageName) {
1162
+ const stage = definition.stages.find((s) => s.name === stageName);
920
1163
  if (stage === void 0)
921
- throw new Error(`Stage "${stageId}" not found in definition ${definition.workflowId}`);
1164
+ throw new Error(`Stage "${stageName}" not found in definition ${definition.name}`);
922
1165
  return stage;
923
1166
  }
924
- async function resolveStageStateSlots(args) {
1167
+ async function resolveStageStateEntries(args) {
925
1168
  const { client, instance, stage, now, initialState } = args;
926
1169
  return resolveDeclaredState({
927
- slotDefs: stage.state,
1170
+ entryDefs: stage.state,
928
1171
  initialState: initialState ?? [],
929
1172
  ctx: {
930
1173
  client,
931
1174
  now,
932
1175
  selfId: instance._id,
933
1176
  tags: instance.tags ?? [],
934
- stageId: stage.id,
1177
+ stageName: stage.name,
935
1178
  workflowResource: instance.workflowResource,
936
- // Allow stage-scope slots' `source: { kind: "stateRead", scope:
937
- // "workflow", ... }` to see the already-resolved workflow-scope
938
- // state.
1179
+ // Allow stage-scope entries' `source: { type: "stateRead", scope:
1180
+ // "workflow", ... }` to see the already-resolved workflow-scope state.
939
1181
  workflowState: instance.state ?? [],
940
1182
  ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
941
1183
  },
942
1184
  randomKey
943
1185
  });
944
1186
  }
945
- async function resolveTaskStateSlots(args) {
1187
+ async function resolveTaskStateEntries(args) {
946
1188
  const { client, instance, stage, task, now } = args;
947
1189
  return resolveDeclaredState({
948
- slotDefs: task.state,
1190
+ entryDefs: task.state,
949
1191
  initialState: [],
950
1192
  ctx: {
951
1193
  client,
952
1194
  now,
953
1195
  selfId: instance._id,
954
1196
  tags: instance.tags ?? [],
955
- stageId: stage.id,
1197
+ stageName: stage.name,
956
1198
  workflowResource: instance.workflowResource,
957
- taskId: task.id,
1199
+ taskName: task.name,
958
1200
  workflowState: instance.state ?? [],
959
1201
  ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
960
1202
  },
961
1203
  randomKey
962
1204
  });
963
1205
  }
964
- function effectsContextEntry(id, value) {
965
- const _key = randomKey();
966
- return typeof value == "string" ? { _key, _type: "effectsContext.string", id, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", id, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", id, value } : { _key, _type: "effectsContext.ref", id, value };
1206
+ class ActionParamsInvalidError extends Error {
1207
+ action;
1208
+ task;
1209
+ issues;
1210
+ constructor(args) {
1211
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1212
+ `);
1213
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1214
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1215
+ }
967
1216
  }
968
- function buildInstanceBase(args) {
969
- const { id, now, actor, perspective } = args;
970
- return {
971
- _id: id,
972
- _type: WORKFLOW_INSTANCE_TYPE,
973
- _rev: "",
974
- _createdAt: now,
975
- _updatedAt: now,
976
- tags: args.tags,
977
- workflowResource: args.workflowResource,
978
- workflowId: args.workflowId,
979
- pinnedVersion: args.pinnedVersion,
980
- definitionSnapshot: JSON.stringify(args.definition),
981
- state: args.state,
982
- effectsContext: args.effectsContext,
983
- ancestors: args.ancestors,
984
- ...perspective !== void 0 ? { perspective } : {},
985
- currentStageId: args.initialStageId,
986
- stages: [],
987
- pendingEffects: [],
988
- effectHistory: [],
989
- history: [
990
- {
991
- _key: randomKey(),
992
- _type: "workflow.history.stageEntered",
993
- at: now,
994
- stageId: args.initialStageId,
995
- ...actor !== void 0 ? { actor } : {}
996
- }
997
- ],
998
- startedAt: now,
999
- lastChangedAt: now
1000
- };
1217
+ class WorkflowStateDivergedError extends Error {
1218
+ instanceId;
1219
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
1220
+ guardError;
1221
+ /** The rollback failure, when rollback was attempted and itself failed. */
1222
+ rollbackError;
1223
+ constructor(args) {
1224
+ super(
1225
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1226
+ { cause: args.guardError }
1227
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1228
+ }
1001
1229
  }
1002
- function startMutation(instance) {
1003
- return {
1004
- currentStageId: instance.currentStageId,
1005
- // Deep-ish copy. Per-scope state slot arrays need their own copies
1006
- // so ops can mutate without leaking into the source.
1007
- state: (instance.state ?? []).map((s) => ({ ...s })),
1008
- stages: instance.stages.map((s) => ({
1009
- ...s,
1010
- state: (s.state ?? []).map((slot) => ({ ...slot })),
1011
- tasks: s.tasks.map((t) => ({
1012
- ...t,
1013
- ...t.state !== void 0 ? { state: t.state.map((slot) => ({ ...slot })) } : {}
1014
- }))
1015
- })),
1016
- pendingEffects: [...instance.pendingEffects],
1017
- effectHistory: [...instance.effectHistory],
1018
- effectsContext: [...instance.effectsContext],
1019
- history: [...instance.history],
1020
- lastChangedAt: instance.lastChangedAt,
1021
- ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1022
- pendingCreates: []
1023
- };
1230
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1231
+ class ConcurrentFireActionError extends Error {
1232
+ instanceId;
1233
+ task;
1234
+ action;
1235
+ attempts;
1236
+ constructor(args) {
1237
+ super(
1238
+ `Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
1239
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1240
+ }
1024
1241
  }
1025
- function taskStatusChangedEntry(args) {
1026
- return {
1027
- _key: randomKey(),
1028
- _type: "workflow.history.taskStatusChanged",
1029
- at: args.at,
1030
- stageId: args.stageId,
1031
- taskId: args.taskId,
1032
- from: args.from,
1033
- to: args.to,
1034
- ...args.actor !== void 0 ? { actor: args.actor } : {}
1035
- };
1242
+ function isRevisionConflict(error) {
1243
+ if (typeof error != "object" || error === null) return !1;
1244
+ const { statusCode, message } = error;
1245
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1246
+ }
1247
+ class CascadeLimitError extends Error {
1248
+ instanceId;
1249
+ limit;
1250
+ constructor(args) {
1251
+ super(
1252
+ `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
1253
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1254
+ }
1255
+ }
1256
+ async function deployOrRollback(args) {
1257
+ try {
1258
+ await args.deploy();
1259
+ } catch (guardError) {
1260
+ if (!args.reversible)
1261
+ throw new WorkflowStateDivergedError({
1262
+ instanceId: args.instanceId,
1263
+ guardError,
1264
+ reason: "the move created child instances a parent-only rollback cannot undo"
1265
+ });
1266
+ try {
1267
+ let patch = args.client.patch(args.instanceId).set(args.restore);
1268
+ args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1269
+ } catch (rollbackError) {
1270
+ throw new WorkflowStateDivergedError({
1271
+ instanceId: args.instanceId,
1272
+ guardError,
1273
+ rollbackError,
1274
+ reason: "rollback of the committed move failed"
1275
+ });
1276
+ }
1277
+ throw guardError;
1278
+ }
1279
+ }
1280
+ function applyTaskStatusChange(args) {
1281
+ const { entry, history, stage, to, at, actor } = args, from = entry.status;
1282
+ entry.status = to, schema.isTerminalTaskStatus(to) && (entry.completedAt = at, actor !== void 0 && (entry.completedBy = actor.id)), history.push({
1283
+ _key: randomKey(),
1284
+ _type: "taskStatusChanged",
1285
+ at,
1286
+ stage,
1287
+ task: entry.name,
1288
+ from,
1289
+ to,
1290
+ ...actor !== void 0 ? { actor } : {}
1291
+ });
1036
1292
  }
1037
1293
  function stageTransitionHistory(args) {
1038
- const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1294
+ const { fromStage, toStage, at, transition, actor, via, reason } = args, shared = {
1039
1295
  at,
1040
- ...transitionId !== void 0 ? { transitionId } : {},
1296
+ ...transition !== void 0 ? { transition } : {},
1041
1297
  ...actor !== void 0 ? { actor } : {},
1042
1298
  ...via !== void 0 ? { via } : {},
1043
1299
  ...reason !== void 0 ? { reason } : {}
@@ -1045,30 +1301,54 @@ function stageTransitionHistory(args) {
1045
1301
  return [
1046
1302
  {
1047
1303
  _key: randomKey(),
1048
- _type: "workflow.history.stageExited",
1049
- stageId: fromStageId,
1050
- toStageId,
1304
+ _type: "stageExited",
1305
+ stage: fromStage,
1306
+ toStage,
1051
1307
  ...shared
1052
1308
  },
1053
1309
  {
1054
1310
  _key: randomKey(),
1055
- _type: "workflow.history.transitionFired",
1056
- fromStageId,
1057
- toStageId,
1311
+ _type: "transitionFired",
1312
+ fromStage,
1313
+ toStage,
1058
1314
  ...shared
1059
1315
  },
1060
1316
  {
1061
1317
  _key: randomKey(),
1062
- _type: "workflow.history.stageEntered",
1063
- stageId: toStageId,
1064
- fromStageId,
1318
+ _type: "stageEntered",
1319
+ stage: toStage,
1320
+ fromStage,
1065
1321
  ...shared
1066
1322
  }
1067
1323
  ];
1068
1324
  }
1325
+ function startMutation(instance) {
1326
+ return {
1327
+ currentStage: instance.currentStage,
1328
+ // Deep-ish copy. Per-scope state entry arrays need their own copies
1329
+ // so ops can mutate without leaking into the source.
1330
+ state: (instance.state ?? []).map((s) => ({ ...s })),
1331
+ stages: instance.stages.map((s) => ({
1332
+ ...s,
1333
+ state: (s.state ?? []).map((entry) => ({ ...entry })),
1334
+ tasks: s.tasks.map((t) => ({
1335
+ ...t,
1336
+ ...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
1337
+ }))
1338
+ })),
1339
+ pendingEffects: [...instance.pendingEffects],
1340
+ effectHistory: [...instance.effectHistory],
1341
+ effectsContext: [...instance.effectsContext],
1342
+ history: [...instance.history],
1343
+ lastChangedAt: instance.lastChangedAt,
1344
+ ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1345
+ ...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
1346
+ pendingCreates: []
1347
+ };
1348
+ }
1069
1349
  function instanceStateFields(src) {
1070
1350
  const fields = {
1071
- currentStageId: src.currentStageId,
1351
+ currentStage: src.currentStage,
1072
1352
  state: src.state,
1073
1353
  stages: src.stages,
1074
1354
  pendingEffects: src.pendingEffects,
@@ -1076,12 +1356,12 @@ function instanceStateFields(src) {
1076
1356
  effectsContext: src.effectsContext,
1077
1357
  history: src.history
1078
1358
  };
1079
- return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
1359
+ return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
1080
1360
  }
1081
1361
  function materializeInstance(base, mutation) {
1082
1362
  return {
1083
1363
  ...base,
1084
- currentStageId: mutation.currentStageId,
1364
+ currentStage: mutation.currentStage,
1085
1365
  state: mutation.state,
1086
1366
  stages: mutation.stages
1087
1367
  };
@@ -1108,25 +1388,25 @@ function currentStageEntry(mutation) {
1108
1388
  const entry = findOpenStageEntry(mutation);
1109
1389
  if (entry === void 0)
1110
1390
  throw new Error(
1111
- `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
1391
+ `Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
1112
1392
  );
1113
1393
  return entry;
1114
1394
  }
1115
1395
  function currentTasks(mutation) {
1116
1396
  return currentStageEntry(mutation).tasks;
1117
1397
  }
1118
- function findTaskInCurrentStage(ctx, taskId) {
1119
- const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1398
+ function findTaskInCurrentStage(ctx, taskName) {
1399
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
1120
1400
  if (task === void 0)
1121
1401
  throw new Error(
1122
- `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1402
+ `Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
1123
1403
  );
1124
1404
  return { stage, task };
1125
1405
  }
1126
- function requireMutationTaskEntry(mutation, taskId) {
1127
- const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1406
+ function requireMutationTaskEntry(mutation, task) {
1407
+ const mutEntry = currentTasks(mutation).find((t) => t.name === task);
1128
1408
  if (mutEntry === void 0)
1129
- throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1409
+ throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
1130
1410
  return mutEntry;
1131
1411
  }
1132
1412
  function findCurrentStageEntry(instance) {
@@ -1135,9 +1415,251 @@ function findCurrentStageEntry(instance) {
1135
1415
  function findCurrentTasks(instance) {
1136
1416
  return findCurrentStageEntry(instance)?.tasks ?? [];
1137
1417
  }
1418
+ function definitionLookupGroq(explicit) {
1419
+ const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
1420
+ return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
1421
+ }
1422
+ async function discoverItems(args) {
1423
+ const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
1424
+ return client.fetch(groq, paramsForLake(params), fetchOptions);
1425
+ }
1426
+ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
1427
+ if (!subjectGdrUri || !subjectGdrUri.includes(":")) return defaultClient;
1428
+ try {
1429
+ return clientForGdr(parseGdr(subjectGdrUri));
1430
+ } catch {
1431
+ return defaultClient;
1432
+ }
1433
+ }
1434
+ function effectsContextEntry(name, value) {
1435
+ const _key = randomKey();
1436
+ return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
1437
+ }
1438
+ function effectsContextJsonEntry(name, value) {
1439
+ return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
1440
+ }
1441
+ function buildInstanceBase(args) {
1442
+ const { id, now, actor, perspective } = args;
1443
+ return {
1444
+ _id: id,
1445
+ _type: WORKFLOW_INSTANCE_TYPE,
1446
+ _rev: "",
1447
+ _createdAt: now,
1448
+ _updatedAt: now,
1449
+ tags: args.tags,
1450
+ workflowResource: args.workflowResource,
1451
+ definition: args.definitionName,
1452
+ pinnedVersion: args.pinnedVersion,
1453
+ definitionSnapshot: JSON.stringify(args.definition),
1454
+ state: args.state,
1455
+ effectsContext: args.effectsContext,
1456
+ ancestors: args.ancestors,
1457
+ ...perspective !== void 0 ? { perspective } : {},
1458
+ currentStage: args.initialStage,
1459
+ stages: [],
1460
+ pendingEffects: [],
1461
+ effectHistory: [],
1462
+ history: [
1463
+ {
1464
+ _key: randomKey(),
1465
+ _type: "stageEntered",
1466
+ at: now,
1467
+ stage: args.initialStage,
1468
+ ...actor !== void 0 ? { actor } : {}
1469
+ }
1470
+ ],
1471
+ startedAt: now,
1472
+ lastChangedAt: now
1473
+ };
1474
+ }
1475
+ const MAX_SPAWN_DEPTH = 6, SUBWORKFLOWS_ALL_DONE = "count($subworkflows[status != 'done']) == 0", FAIL_WHEN_SYSTEM_ID = "engine.failWhen", COMPLETE_WHEN_SYSTEM_ID = "engine.completeWhen";
1476
+ function gateActor(outcome) {
1477
+ return {
1478
+ kind: "system",
1479
+ id: outcome === "failed" ? FAIL_WHEN_SYSTEM_ID : COMPLETE_WHEN_SYSTEM_ID
1480
+ };
1481
+ }
1482
+ function effectiveCompleteWhen(task) {
1483
+ return task.completeWhen ?? (task.subworkflows !== void 0 ? SUBWORKFLOWS_ALL_DONE : void 0);
1484
+ }
1485
+ async function evaluateResolutionGate(ctx, task, vars) {
1486
+ const scope = { taskName: task.name, vars };
1487
+ if (task.failWhen !== void 0 && await ctxEvaluateCondition(ctx, task.failWhen, scope))
1488
+ return "failed";
1489
+ const completeWhen = effectiveCompleteWhen(task);
1490
+ if (completeWhen !== void 0 && await ctxEvaluateCondition(ctx, completeWhen, scope))
1491
+ return "done";
1492
+ }
1493
+ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1494
+ if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
1495
+ const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
1496
+ throw new Error(
1497
+ `Subworkflow depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.name}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
1498
+ );
1499
+ }
1500
+ const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tags ?? []);
1501
+ if (definition === null) {
1502
+ const versionLabel = sub.definition.version === void 0 || sub.definition.version === "latest" ? "latest" : `v${sub.definition.version}`;
1503
+ throw new Error(
1504
+ `Subworkflow definition "${sub.definition.name}" ${versionLabel} not deployed (parent ${ctx.instance._id}, task ${task.name})`
1505
+ );
1506
+ }
1507
+ const parentScope = await ctxConditionParams(ctx, {
1508
+ taskName: task.name,
1509
+ ...actor ? { actor } : {}
1510
+ }), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1511
+ for (const row of rows) {
1512
+ const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
1513
+ client: ctx.client,
1514
+ parent: ctx.instance,
1515
+ definition,
1516
+ initialState,
1517
+ effectsContext,
1518
+ actor,
1519
+ now
1520
+ });
1521
+ refs.push(ref), mutation.pendingCreates.push(body);
1522
+ }
1523
+ return refs;
1524
+ }
1525
+ async function resolveSpawnRows(ctx, sub) {
1526
+ const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
1527
+ let value;
1528
+ if (groq.includes("*")) {
1529
+ const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1530
+ value = await discoverItems({
1531
+ client,
1532
+ groq,
1533
+ params,
1534
+ ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
1535
+ });
1536
+ } else {
1537
+ const tree = groqJs.parse(groq, { params });
1538
+ value = await (await groqJs.evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
1539
+ }
1540
+ return Array.isArray(value) ? value : value == null ? [] : [value];
1541
+ }
1542
+ function firstDocRefGdrUri(entries) {
1543
+ if (entries) {
1544
+ for (const s of entries)
1545
+ if (s._type === "doc.ref" && s.value !== null) return s.value.id;
1546
+ }
1547
+ }
1548
+ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1549
+ const out = [];
1550
+ for (const [name, groq] of Object.entries(sub.with ?? {})) {
1551
+ const declared = (childDefinition.state ?? []).find((e) => e.name === name);
1552
+ if (declared === void 0)
1553
+ throw new Error(
1554
+ `subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
1555
+ );
1556
+ const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
1557
+ value != null && out.push({
1558
+ type: declared.type,
1559
+ name,
1560
+ value
1561
+ });
1562
+ }
1563
+ return out;
1564
+ }
1565
+ async function resolveContextHandoff(ctx, sub, parentScope) {
1566
+ const out = {};
1567
+ for (const [name, groq] of Object.entries(sub.context ?? {})) {
1568
+ const v2 = await runGroq(groq, parentScope, ctx.snapshot);
1569
+ v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (out[name] = v2);
1570
+ }
1571
+ return out;
1572
+ }
1573
+ function canonicaliseSpawnValue(kind, value, workflowResource) {
1574
+ return kind === "doc.ref" ? coerceSpawnGdr(value, workflowResource) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
1575
+ }
1576
+ function coerceSpawnGdr(raw, workflowResource) {
1577
+ const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1578
+ if (raw == null) return null;
1579
+ if (typeof raw == "object" && "id" in raw && "type" in raw) {
1580
+ const r = raw;
1581
+ if (typeof r.id == "string" && typeof r.type == "string")
1582
+ return { id: toUri(r.id), type: r.type };
1583
+ }
1584
+ if (typeof raw == "object" && "_ref" in raw) {
1585
+ const r = raw;
1586
+ if (typeof r._ref == "string") return { id: toUri(r._ref), type: "document" };
1587
+ }
1588
+ return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1589
+ }
1590
+ async function resolveDefinitionRef(client, ref, tags) {
1591
+ const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
1592
+ return wantsExplicit && (params.version = ref.version), await client.fetch(
1593
+ definitionLookupGroq(wantsExplicit),
1594
+ params
1595
+ ) ?? null;
1596
+ }
1597
+ async function prepareChildInstance(args) {
1598
+ const { client, parent, definition, initialState, effectsContext, actor, now } = args, childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
1599
+ ...parent.ancestors,
1600
+ {
1601
+ id: selfGdr(parent),
1602
+ type: WORKFLOW_INSTANCE_TYPE
1603
+ }
1604
+ ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1605
+ entryDefs: definition.state,
1606
+ initialState,
1607
+ ctx: {
1608
+ client,
1609
+ now,
1610
+ selfId: childDocId,
1611
+ tags: childTags,
1612
+ workflowResource,
1613
+ ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1614
+ },
1615
+ randomKey
1616
+ }), childPerspective = derivePerspectiveFromState(childState) ?? inheritedPerspective, effectsContextEntries = Object.entries(effectsContext).map(
1617
+ ([key, value]) => {
1618
+ if (typeof value == "object" && value !== null && "id" in value && "type" in value) {
1619
+ if (!isGdrUri(value.id))
1620
+ throw new Error(
1621
+ `subworkflows.context["${key}"]: GDR id must be a "<scheme>:..." URI, got ${JSON.stringify(value.id)}.`
1622
+ );
1623
+ return effectsContextEntry(key, { id: value.id, type: value.type });
1624
+ }
1625
+ return effectsContextEntry(key, value);
1626
+ }
1627
+ ), base = buildInstanceBase({
1628
+ id: childDocId,
1629
+ now,
1630
+ tags: childTags,
1631
+ workflowResource,
1632
+ definitionName: definition.name,
1633
+ pinnedVersion: definition.version,
1634
+ definition,
1635
+ state: childState,
1636
+ effectsContext: effectsContextEntries,
1637
+ ancestors,
1638
+ perspective: childPerspective,
1639
+ initialStage: definition.initialStage,
1640
+ actor
1641
+ });
1642
+ return { ref: childRef, body: base };
1643
+ }
1644
+ async function subworkflowRows(ctx, refs) {
1645
+ if (refs.length === 0) return [];
1646
+ const ids = refs.map((r) => gdrToBareId(r.id));
1647
+ return (await ctx.client.fetch("*[_id in $ids]{_id, currentStage, completedAt}", { ids })).map((c) => ({
1648
+ _id: c._id,
1649
+ stage: c.currentStage,
1650
+ status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
1651
+ }));
1652
+ }
1653
+ async function resolveBindings(args) {
1654
+ const resolved = {};
1655
+ for (const [key, groq] of Object.entries(args.bindings ?? {}))
1656
+ resolved[key] = await runGroq(groq, args.params, args.snapshot);
1657
+ return { ...resolved, ...args.staticInput };
1658
+ }
1138
1659
  async function completeEffect(args) {
1139
1660
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1140
- ...options?.clock ? { clock: options.clock } : {}
1661
+ ...options?.clock ? { clock: options.clock } : {},
1662
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1141
1663
  });
1142
1664
  return commitCompleteEffect(
1143
1665
  ctx,
@@ -1154,7 +1676,7 @@ function buildEffectHistoryEntry(pending, outcome) {
1154
1676
  const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
1155
1677
  return {
1156
1678
  _key: pending._key,
1157
- effectId: pending.effectId,
1679
+ name: pending.name,
1158
1680
  ...pending.title !== void 0 ? { title: pending.title } : {},
1159
1681
  ...pending.description !== void 0 ? { description: pending.description } : {},
1160
1682
  params: pending.params,
@@ -1168,11 +1690,9 @@ function buildEffectHistoryEntry(pending, outcome) {
1168
1690
  ...outputs !== void 0 ? { outputs } : {}
1169
1691
  };
1170
1692
  }
1171
- function upsertEffectsContext(mutation, outputs) {
1172
- for (const [id, value] of Object.entries(outputs)) {
1173
- const existingIndex = mutation.effectsContext.findIndex((e) => e.id === id), entry = effectsContextEntry(id, value);
1174
- existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
1175
- }
1693
+ function upsertEffectsContext(mutation, effectName, outputs) {
1694
+ const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
1695
+ existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
1176
1696
  }
1177
1697
  async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1178
1698
  const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
@@ -1183,12 +1703,12 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1183
1703
  const ranAt = ctx.now;
1184
1704
  return mutation.effectHistory.push(
1185
1705
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1186
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1706
+ ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1187
1707
  _key: randomKey(),
1188
- _type: "workflow.history.effectCompleted",
1708
+ _type: "effectCompleted",
1189
1709
  at: ranAt,
1190
1710
  effectKey,
1191
- effectId: pending.effectId,
1711
+ effect: pending.name,
1192
1712
  status,
1193
1713
  ...outputs !== void 0 ? { outputs } : {},
1194
1714
  ...detail !== void 0 ? { detail } : {},
@@ -1198,8 +1718,8 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1198
1718
  function buildQueuedEffect(effect, origin, params, actor, now) {
1199
1719
  const key = randomKey(), pending = {
1200
1720
  _key: key,
1201
- _type: "workflow.pendingEffect",
1202
- effectId: effect.id,
1721
+ _type: "pendingEffect",
1722
+ name: effect.name,
1203
1723
  ...effect.title !== void 0 ? { title: effect.title } : {},
1204
1724
  ...effect.description !== void 0 ? { description: effect.description } : {},
1205
1725
  ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
@@ -1209,370 +1729,280 @@ function buildQueuedEffect(effect, origin, params, actor, now) {
1209
1729
  queuedAt: now
1210
1730
  }, history = {
1211
1731
  _key: randomKey(),
1212
- _type: "workflow.history.effectQueued",
1732
+ _type: "effectQueued",
1213
1733
  at: now,
1214
1734
  effectKey: key,
1215
- effectId: effect.id,
1735
+ effect: effect.name,
1216
1736
  origin
1217
1737
  };
1218
1738
  return { pending, history };
1219
1739
  }
1220
- async function queueEffects(ctx, mutation, effects, origin, actor, callerParams) {
1221
- if (!effects) return;
1222
- const now = ctx.now, liveInstance = {
1223
- ...ctx.instance,
1224
- state: mutation.state,
1225
- stages: mutation.stages,
1226
- effectsContext: mutation.effectsContext
1227
- };
1740
+ async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
1741
+ if (!effects || effects.length === 0) return;
1742
+ const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
1743
+ ...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
1744
+ ...actor !== void 0 ? { actor } : {},
1745
+ // Caller-supplied action params, readable in bindings as `$params.<name>`.
1746
+ vars: { params: opts?.callerParams ?? {} }
1747
+ });
1228
1748
  for (const effect of effects) {
1229
- const params = await resolveBindings({
1749
+ const resolved = await resolveBindings({
1230
1750
  bindings: effect.bindings,
1231
1751
  staticInput: effect.input,
1232
- instance: liveInstance,
1233
- now,
1234
- ...callerParams !== void 0 ? { params: callerParams } : {},
1235
- ...actor !== void 0 ? { actor } : {}
1236
- }), { pending, history } = buildQueuedEffect(effect, origin, params, actor, now);
1752
+ snapshot: ctx.snapshot,
1753
+ params
1754
+ }), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
1237
1755
  mutation.pendingEffects.push(pending), mutation.history.push(history);
1238
1756
  }
1239
1757
  }
1240
- class ActionParamsInvalidError extends Error {
1241
- action;
1242
- taskId;
1243
- issues;
1244
- constructor(args) {
1245
- const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1246
- `);
1247
- super(`Action "${args.action}" on task "${args.taskId}" rejected: invalid params
1248
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.taskId = args.taskId, this.issues = args.issues;
1758
+ function resolveStaticSource(src, ctx) {
1759
+ switch (src.type) {
1760
+ case "literal":
1761
+ return { handled: !0, value: src.value };
1762
+ case "param":
1763
+ return { handled: !0, value: ctx.params?.[src.param] };
1764
+ case "actor":
1765
+ return { handled: !0, value: ctx.actor };
1766
+ case "now":
1767
+ return { handled: !0, value: ctx.now };
1768
+ default:
1769
+ return { handled: !1 };
1249
1770
  }
1250
1771
  }
1251
- class WorkflowStateDivergedError extends Error {
1252
- instanceId;
1253
- /** The guard-deploy failure that triggered the (attempted) rollback. */
1254
- guardError;
1255
- /** The rollback failure, when rollback was attempted and itself failed. */
1256
- rollbackError;
1257
- constructor(args) {
1258
- super(
1259
- `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1260
- { cause: args.guardError }
1261
- ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1262
- }
1772
+ const STATE_OP_TYPES = [
1773
+ "state.set",
1774
+ "state.unset",
1775
+ "state.append",
1776
+ "state.updateWhere",
1777
+ "state.removeWhere"
1778
+ ];
1779
+ function isStateOp(summary) {
1780
+ return STATE_OP_TYPES.includes(summary.opType);
1263
1781
  }
1264
- const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1265
- class ConcurrentFireActionError extends Error {
1266
- instanceId;
1267
- taskId;
1268
- action;
1269
- attempts;
1270
- constructor(args) {
1271
- super(
1272
- `Action "${args.action}" on task "${args.taskId}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
1273
- ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.taskId = args.taskId, this.action = args.action, this.attempts = args.attempts;
1782
+ function validateActionParams(action, taskName, callerParams) {
1783
+ const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
1784
+ for (const decl of declared) {
1785
+ const value = params[decl.name], present = decl.name in params && value !== void 0 && value !== null;
1786
+ if (decl.required === !0 && !present) {
1787
+ issues.push({ param: decl.name, reason: "required but missing" });
1788
+ continue;
1789
+ }
1790
+ present && (checkParamType(value, decl) || issues.push({
1791
+ param: decl.name,
1792
+ reason: `expected type=${decl.type}, got ${typeof value}`
1793
+ }));
1274
1794
  }
1795
+ if (issues.length > 0)
1796
+ throw new ActionParamsInvalidError({ action: action.name, task: taskName, issues });
1797
+ return params;
1275
1798
  }
1276
- function isRevisionConflict(error) {
1277
- if (typeof error != "object" || error === null) return !1;
1278
- const { statusCode, message } = error;
1279
- return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1799
+ function hasStringField(value, field) {
1800
+ return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
1280
1801
  }
1281
- class CascadeLimitError extends Error {
1282
- instanceId;
1283
- limit;
1284
- constructor(args) {
1285
- super(
1286
- `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
1287
- ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1802
+ function checkParamType(value, decl) {
1803
+ switch (decl.type) {
1804
+ case "string":
1805
+ case "url":
1806
+ case "dateTime":
1807
+ return typeof value == "string";
1808
+ case "number":
1809
+ return typeof value == "number" && Number.isFinite(value);
1810
+ case "boolean":
1811
+ return typeof value == "boolean";
1812
+ case "actor":
1813
+ return hasStringField(value, "kind");
1814
+ case "doc.ref":
1815
+ return hasStringField(value, "id");
1816
+ case "doc.refs":
1817
+ return Array.isArray(value) && value.every((v2) => checkParamType(v2, { type: "doc.ref" }));
1818
+ case "json":
1819
+ return !0;
1820
+ default:
1821
+ return !1;
1288
1822
  }
1289
1823
  }
1290
- async function deployOrRollback(args) {
1291
- try {
1292
- await args.deploy();
1293
- } catch (guardError) {
1294
- if (!args.reversible)
1295
- throw new WorkflowStateDivergedError({
1296
- instanceId: args.instanceId,
1297
- guardError,
1298
- reason: "the move created child instances a parent-only rollback cannot undo"
1299
- });
1300
- try {
1301
- let patch = args.client.patch(args.instanceId).set(args.restore);
1302
- args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1303
- } catch (rollbackError) {
1304
- throw new WorkflowStateDivergedError({
1305
- instanceId: args.instanceId,
1306
- guardError,
1307
- rollbackError,
1308
- reason: "rollback of the committed move failed"
1309
- });
1310
- }
1311
- throw guardError;
1824
+ function runOps(args) {
1825
+ const { ops, mutation, stage, origin, params, actor, self, now } = args;
1826
+ if (ops === void 0 || ops.length === 0) return [];
1827
+ const summaries = [];
1828
+ for (const op of ops) {
1829
+ const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
1830
+ summaries.push(summary), mutation.history.push({
1831
+ _key: randomKey(),
1832
+ _type: "opApplied",
1833
+ at: now,
1834
+ stage,
1835
+ ...origin.task !== void 0 ? { task: origin.task } : {},
1836
+ ...origin.action !== void 0 ? { action: origin.action } : {},
1837
+ ...origin.transition !== void 0 ? { transition: origin.transition } : {},
1838
+ opType: summary.opType,
1839
+ ...summary.target !== void 0 ? { target: summary.target } : {},
1840
+ ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
1841
+ ...actor !== void 0 ? { actor } : {}
1842
+ });
1312
1843
  }
1844
+ return summaries;
1313
1845
  }
1314
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
1315
- function validateTags(tags) {
1316
- if (tags.length === 0)
1317
- throw new Error("tags: must be a non-empty array");
1318
- for (const tag of tags)
1319
- if (!TAG_RE.test(tag))
1320
- throw new Error(
1321
- `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
1322
- );
1323
- }
1324
- function canonicalTag(tags) {
1325
- return tags[0];
1846
+ function applyOp(op, ctx) {
1847
+ switch (op.type) {
1848
+ case "state.set":
1849
+ return applyStateSet(op, ctx);
1850
+ case "state.unset":
1851
+ return applyStateUnset(op, ctx);
1852
+ case "state.append":
1853
+ return applyStateAppend(op, ctx);
1854
+ case "state.updateWhere":
1855
+ return applyStateUpdateWhere(op, ctx);
1856
+ case "state.removeWhere":
1857
+ return applyStateRemoveWhere(op, ctx);
1858
+ case "status.set":
1859
+ return applyStatusSet(op, ctx);
1860
+ }
1326
1861
  }
1327
- function tagScopeFilter(param = "engineTags") {
1328
- return `count(tags[@ in $${param}]) > 0`;
1862
+ function applyStateSet(op, ctx) {
1863
+ const value = resolveOpSource(op.value, ctx), entry = locateEntry(ctx, op.target);
1864
+ return validateStateValue({ entryType: entry._type, entryName: entry.name, value }), setEntryValue(entry, value), { opType: op.type, target: op.target, resolved: { value } };
1865
+ }
1866
+ const EMPTY_BY_KIND = {
1867
+ "doc.refs": [],
1868
+ checklist: [],
1869
+ notes: [],
1870
+ assignees: []
1871
+ };
1872
+ function applyStateUnset(op, ctx) {
1873
+ const entry = locateEntry(ctx, op.target);
1874
+ return setEntryValue(entry, EMPTY_BY_KIND[entry._type] ?? null), { opType: op.type, target: op.target };
1329
1875
  }
1330
- function definitionLookupGroq(explicit) {
1331
- const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && workflowId == $workflowId && ${tagScopeFilter()}`;
1332
- return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
1876
+ function applyStateAppend(op, ctx) {
1877
+ const item = resolveOpSource(op.value, ctx), entry = locateEntry(ctx, op.target);
1878
+ validateStateAppendItem({ entryType: entry._type, entryName: entry.name, item });
1879
+ const current = Array.isArray(entry.value) ? entry.value : [];
1880
+ return setEntryValue(entry, [...current, withRowKey(item)]), { opType: op.type, target: op.target, resolved: { item } };
1333
1881
  }
1334
- async function discoverItems(args) {
1335
- const { client, groq, params, perspective } = args, fetchOptions = perspective !== void 0 ? { perspective } : void 0;
1336
- return client.fetch(groq, paramsForLake(params), fetchOptions);
1882
+ function withRowKey(item) {
1883
+ return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
1337
1884
  }
1338
- function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
1339
- if (!subjectGdrUri || !subjectGdrUri.includes(":")) return defaultClient;
1340
- try {
1341
- return clientForGdr(parseGdr(subjectGdrUri));
1342
- } catch {
1343
- return defaultClient;
1344
- }
1885
+ function applyStateUpdateWhere(op, ctx) {
1886
+ const entry = locateEntry(ctx, op.target), rows = requireArrayValue(entry, op), merge = resolveOpSource(op.value, ctx);
1887
+ if (merge === null || typeof merge != "object" || Array.isArray(merge))
1888
+ throw new Error(
1889
+ `state.updateWhere value must resolve to an object of fields to merge (target "${op.target.state}")`
1890
+ );
1891
+ return setEntryValue(
1892
+ entry,
1893
+ rows.map(
1894
+ (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...merge } : row
1895
+ )
1896
+ ), { opType: op.type, target: op.target, resolved: { merge } };
1345
1897
  }
1346
- const MAX_SPAWN_DEPTH = 6;
1347
- async function spawnChildren(ctx, mutation, task, spawn, actor) {
1348
- if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
1349
- const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
1898
+ function applyStateRemoveWhere(op, ctx) {
1899
+ const entry = locateEntry(ctx, op.target), rows = requireArrayValue(entry, op);
1900
+ return setEntryValue(
1901
+ entry,
1902
+ rows.filter((row) => !evalOpPredicate(op.where, row, ctx))
1903
+ ), { opType: op.type, target: op.target };
1904
+ }
1905
+ function requireArrayValue(entry, op) {
1906
+ if (!Array.isArray(entry.value))
1350
1907
  throw new Error(
1351
- `Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
1908
+ `${op.type} target ${op.target.scope}:"${op.target.state}" holds a non-array ${entry._type} value \u2014 where-ops operate on array entries`
1352
1909
  );
1353
- }
1354
- const now = ctx.now, rows = await resolveSpawnRows(ctx, spawn), refs = [];
1355
- for (const row of rows) {
1356
- const projected = projectSpawnRow(ctx.instance, spawn, row, now), { ref, body } = await prepareChildInstance({
1357
- client: ctx.client,
1358
- parent: ctx.instance,
1359
- task,
1360
- spawn,
1361
- initialState: projected.initialState,
1362
- effectsContext: projected.effectsContext,
1363
- actor,
1364
- now
1365
- });
1366
- refs.push(ref), mutation.pendingCreates.push(body);
1367
- }
1368
- return refs;
1910
+ return entry.value;
1369
1911
  }
1370
- async function resolveSpawnRows(ctx, spawn) {
1371
- const params = buildParams(ctx.instance, ctx.now), groq = spawn.forEach.groq;
1372
- let value;
1373
- if (groq.includes("*")) {
1374
- const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1375
- value = await discoverItems({
1376
- client,
1377
- groq,
1378
- params,
1379
- ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
1380
- });
1381
- } else {
1382
- const tree = groqJs.parse(groq, { params });
1383
- value = await (await groqJs.evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
1384
- }
1385
- return Array.isArray(value) ? value : value == null ? [] : [value];
1912
+ function applyStatusSet(op, ctx) {
1913
+ const entry = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.name === op.task);
1914
+ if (entry === void 0)
1915
+ throw new Error(`status.set targets task "${op.task}" which has no entry in the open stage`);
1916
+ return applyTaskStatusChange({
1917
+ entry,
1918
+ history: ctx.mutation.history,
1919
+ stage: ctx.stage,
1920
+ to: op.status,
1921
+ at: ctx.now,
1922
+ ...ctx.actor !== void 0 ? { actor: ctx.actor } : {}
1923
+ }), { opType: op.type, resolved: { task: op.task, status: op.status } };
1924
+ }
1925
+ function setEntryValue(entry, value) {
1926
+ entry.value = value;
1927
+ }
1928
+ function locateEntry(ctx, target) {
1929
+ const entry = stateHost(ctx, target.scope)?.find((s) => s.name === target.state);
1930
+ if (entry === void 0)
1931
+ throw new Error(
1932
+ `Op target ${target.scope}:"${target.state}" has no resolved state entry on this instance \u2014 the deployed definition and the instance state have diverged`
1933
+ );
1934
+ return entry;
1386
1935
  }
1387
- function firstDocRefGdrUri(slots) {
1388
- if (slots) {
1389
- for (const s of slots)
1390
- if (s._type === "workflow.state.doc.ref" && s.value !== null) return s.value.id;
1391
- }
1936
+ function stateHost(ctx, scope) {
1937
+ if (scope === "workflow") return ctx.mutation.state;
1938
+ const stageEntry = findOpenStageEntry(ctx.mutation);
1939
+ if (scope === "stage") return stageEntry?.state;
1940
+ const task = stageEntry?.tasks.find((t) => t.name === ctx.taskName);
1941
+ return task !== void 0 && task.state === void 0 && (task.state = []), task?.state;
1392
1942
  }
1393
- function resolveSpawnSource(source, parent, row, now) {
1394
- switch (source.source) {
1395
- case "literal":
1396
- return source.value;
1397
- case "row":
1398
- return source.path !== void 0 ? getPath(row, source.path) : row;
1399
- case "parentState": {
1400
- const slot = parent.state?.find((s) => s.id === source.slotId);
1401
- return slot === void 0 ? null : source.path !== void 0 ? getPath(slot, source.path) : slot;
1402
- }
1943
+ function resolveOpSource(src, ctx) {
1944
+ const staticValue = resolveStaticSource(src, ctx);
1945
+ if (staticValue.handled) return staticValue.value;
1946
+ switch (src.type) {
1403
1947
  case "self":
1404
- return selfGdr(parent);
1405
- case "stageId":
1406
- return parent.currentStageId;
1407
- case "now":
1408
- return now;
1948
+ return ctx.self;
1949
+ case "stage":
1950
+ return ctx.stage;
1951
+ case "stateRead": {
1952
+ const value = readEntryFromMutation(ctx, src);
1953
+ return src.path !== void 0 ? getPath(value, src.path) : value;
1954
+ }
1409
1955
  case "object": {
1410
1956
  const out = {};
1411
- for (const [k, v2] of Object.entries(source.fields))
1412
- out[k] = resolveSpawnSource(v2, parent, row, now);
1957
+ for (const [field, fieldSrc] of Object.entries(src.fields))
1958
+ out[field] = resolveOpSource(fieldSrc, ctx);
1413
1959
  return out;
1414
1960
  }
1415
- case "actor":
1416
- case "param":
1417
- case "stateRead":
1418
- case "effectOutput":
1419
- return null;
1420
- }
1421
- }
1422
- function projectSpawnRow(parent, spawn, row, now) {
1423
- const initialState = [];
1424
- for (const entry of spawn.forEach.as.initialState ?? []) {
1425
- const raw = resolveSpawnSource(entry.value, parent, row, now), value = canonicaliseSpawnValue(entry.type, raw, parent.workflowResource);
1426
- initialState.push({
1427
- type: entry.type,
1428
- id: entry.id,
1429
- value
1430
- });
1431
- }
1432
- const effectsContext = {};
1433
- for (const [k, src] of Object.entries(spawn.forEach.as.effectsContext ?? {})) {
1434
- const v2 = resolveSpawnSource(src, parent, row, now);
1435
- v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (effectsContext[k] = v2);
1436
- }
1437
- return { initialState, effectsContext };
1438
- }
1439
- function canonicaliseSpawnValue(slotType, value, workflowResource) {
1440
- return slotType === "workflow.state.doc.ref" ? coerceSpawnGdr(value, workflowResource) : slotType === "workflow.state.doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
1441
- }
1442
- function coerceSpawnGdr(raw, workflowResource) {
1443
- const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1444
- if (raw == null) return null;
1445
- if (typeof raw == "object" && "id" in raw && "type" in raw) {
1446
- const r = raw;
1447
- if (typeof r.id == "string" && typeof r.type == "string")
1448
- return { id: toUri(r.id), type: r.type };
1449
- }
1450
- if (typeof raw == "object" && "_ref" in raw) {
1451
- const r = raw;
1452
- if (typeof r._ref == "string") return { id: toUri(r._ref), type: "document" };
1961
+ default:
1962
+ throw new Error(`Source type "${src.type}" is a state-entry origin, not a value`);
1453
1963
  }
1454
- return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1455
1964
  }
1456
- async function resolveLogicalDefinition(client, ref, tags) {
1457
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
1458
- return wantsExplicit && (params.version = ref.version), await client.fetch(
1459
- definitionLookupGroq(wantsExplicit),
1460
- params
1461
- ) ?? null;
1965
+ function entryValue(entries, name) {
1966
+ return entries?.find((s) => s.name === name)?.value;
1462
1967
  }
1463
- async function prepareChildInstance(args) {
1464
- const { client, parent, task, spawn, initialState, effectsContext, actor, now } = args, definition = await resolveLogicalDefinition(client, spawn.definitionRef, parent.tags ?? []);
1465
- if (definition === null) {
1466
- const versionLabel = spawn.definitionRef.version === void 0 || spawn.definitionRef.version === "latest" ? "latest" : `v${spawn.definitionRef.version}`;
1467
- throw new Error(
1468
- `Spawn target workflowId="${spawn.definitionRef.workflowId}" ${versionLabel} not deployed (parent ${parent._id}, task ${task.id})`
1469
- );
1968
+ function readEntryFromMutation(ctx, src) {
1969
+ const scopes = src.scope !== void 0 ? [src.scope] : ["stage", "workflow"], stageEntry = findOpenStageEntry(ctx.mutation);
1970
+ for (const scope of scopes) {
1971
+ const host = scope === "workflow" ? ctx.mutation.state : stageEntry?.state, value = entryValue(host, src.state);
1972
+ if (value !== void 0) return value;
1470
1973
  }
1471
- const childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
1472
- ...parent.ancestors,
1473
- {
1474
- id: selfGdr(parent),
1475
- type: WORKFLOW_INSTANCE_TYPE
1476
- }
1477
- ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1478
- slotDefs: definition.state,
1479
- initialState,
1480
- ctx: {
1481
- client,
1482
- now,
1483
- selfId: childDocId,
1484
- tags: childTags,
1485
- workflowResource,
1486
- ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1487
- },
1488
- randomKey
1489
- }), childPerspective = derivePerspectiveFromState(childState) ?? inheritedPerspective, effectsContextEntries = Object.entries(effectsContext).map(
1490
- ([key, value]) => {
1491
- if (typeof value == "object" && value !== null && "id" in value && "type" in value) {
1492
- if (!isGdrUri(value.id))
1493
- throw new Error(
1494
- `spawn.forEach.as.effectsContext["${key}"]: GDR id must be a "<scheme>:..." URI, got ${JSON.stringify(value.id)}.`
1495
- );
1496
- return effectsContextEntry(key, { id: value.id, type: value.type });
1497
- }
1498
- return effectsContextEntry(key, value);
1499
- }
1500
- ), base = buildInstanceBase({
1501
- id: childDocId,
1502
- now,
1503
- tags: childTags,
1504
- workflowResource,
1505
- workflowId: definition.workflowId,
1506
- pinnedVersion: definition.version,
1507
- definition,
1508
- state: childState,
1509
- effectsContext: effectsContextEntries,
1510
- ancestors,
1511
- perspective: childPerspective,
1512
- initialStageId: definition.initialStageId,
1513
- actor
1514
- });
1515
- return { ref: childRef, body: base };
1516
1974
  }
1517
- async function checkSpawnCompletion(client, entry, spawn) {
1518
- const refs = entry.spawnedInstances ?? [];
1519
- if (refs.length === 0) {
1520
- const policy2 = spawn.completionPolicy ?? { kind: "all" };
1521
- return policy2.kind === "all" || policy2.kind === "count" && policy2.n === 0;
1522
- }
1523
- const ids = refs.map((r) => r.id.includes(":") ? gdrToBareId(r.id) : r.id), children = await client.fetch("*[_id in $ids]{_id, currentStageId, definitionSnapshot}", { ids });
1524
- let terminalCount = 0;
1525
- for (const child of children)
1526
- JSON.parse(child.definitionSnapshot).stages.find((s) => s.id === child.currentStageId)?.kind === "terminal" && terminalCount++;
1527
- const policy = spawn.completionPolicy ?? { kind: "all" };
1528
- switch (policy.kind) {
1975
+ function evalOpPredicate(p, row, ctx) {
1976
+ switch (p.type) {
1529
1977
  case "all":
1530
- return terminalCount === refs.length;
1978
+ return p.of.every((sub) => evalOpPredicate(sub, row, ctx));
1531
1979
  case "any":
1532
- return terminalCount >= 1;
1533
- case "count":
1534
- return terminalCount >= policy.n;
1980
+ return p.of.some((sub) => evalOpPredicate(sub, row, ctx));
1981
+ case "field":
1982
+ return row[p.field] === resolveOpSource(p.equals, ctx);
1535
1983
  }
1536
1984
  }
1537
1985
  async function invokeTask(args) {
1538
- const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId, {
1539
- ...options?.clock ? { clock: options.clock } : {}
1986
+ const { client, instanceId, task: taskName, options } = args, ctx = await loadContext(client, instanceId, {
1987
+ ...options?.clock ? { clock: options.clock } : {},
1988
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1540
1989
  });
1541
- return commitInvoke(ctx, taskId, options?.actor);
1990
+ return commitInvoke(ctx, taskName, options?.actor);
1542
1991
  }
1543
- async function commitInvoke(ctx, taskId, actor) {
1544
- const { task } = findTaskInCurrentStage(ctx, taskId), entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
1992
+ async function commitInvoke(ctx, taskName, actor) {
1993
+ const { task } = findTaskInCurrentStage(ctx, taskName), entry = findCurrentTasks(ctx.instance).find((t) => t.name === taskName);
1545
1994
  if (entry === void 0)
1546
- throw new Error(`Task "${taskId}" has no status entry on instance ${ctx.instance._id}`);
1995
+ throw new Error(`Task "${taskName}" has no status entry on instance ${ctx.instance._id}`);
1547
1996
  if (entry.status !== "pending")
1548
- return { invoked: !1, taskId };
1549
- const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId);
1550
- return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, taskId };
1551
- }
1552
- async function autoResolveOnActivate(ctx, mutation, task, entry, now) {
1553
- const checks = [
1554
- { filter: task.failWhen, outcome: "failed", systemId: "engine.failWhen" },
1555
- { filter: task.completeWhen, outcome: "done", systemId: "engine.completeWhen" }
1556
- ];
1557
- for (const { filter, outcome, systemId } of checks)
1558
- if (filter !== void 0 && await ctxEvaluateFilter(ctx, filter))
1559
- return entry.status = outcome, entry.completedAt = now, entry.completedBy = systemId, mutation.history.push(
1560
- taskStatusChangedEntry({
1561
- stageId: mutation.currentStageId,
1562
- taskId: task.id,
1563
- from: "active",
1564
- to: outcome,
1565
- at: now,
1566
- actor: { kind: "system", id: systemId }
1567
- })
1568
- ), !0;
1569
- return !1;
1997
+ return { invoked: !1, task: taskName };
1998
+ const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskName);
1999
+ return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, task: taskName };
1570
2000
  }
1571
2001
  async function activateTask(ctx, mutation, task, entry, actor) {
1572
2002
  const now = ctx.now;
1573
2003
  if (entry.status = "active", entry.startedAt = now, task.state !== void 0 && task.state.length > 0) {
1574
- const stage = findStage(ctx.definition, mutation.currentStageId);
1575
- entry.state = await resolveTaskStateSlots({
2004
+ const stage = findStage(ctx.definition, mutation.currentStage);
2005
+ entry.state = await resolveTaskStateEntries({
1576
2006
  client: ctx.client,
1577
2007
  instance: ctx.instance,
1578
2008
  stage,
@@ -1580,55 +2010,73 @@ async function activateTask(ctx, mutation, task, entry, actor) {
1580
2010
  now
1581
2011
  });
1582
2012
  }
1583
- if (!await autoResolveOnActivate(ctx, mutation, task, entry, now) && (mutation.history.push({
2013
+ runOps({
2014
+ ops: task.ops,
2015
+ mutation,
2016
+ stage: mutation.currentStage,
2017
+ origin: { task: task.name },
2018
+ params: {},
2019
+ actor,
2020
+ self: selfGdr(ctx.instance),
2021
+ now
2022
+ }), mutation.history.push({
1584
2023
  _key: randomKey(),
1585
- _type: "workflow.history.taskActivated",
2024
+ _type: "taskActivated",
1586
2025
  at: now,
1587
- stageId: mutation.currentStageId,
1588
- taskId: task.id,
2026
+ stage: mutation.currentStage,
2027
+ task: task.name,
1589
2028
  ...actor !== void 0 ? { actor } : {}
1590
- }), await queueEffects(
1591
- ctx,
1592
- mutation,
1593
- task.effects,
1594
- {
1595
- kind: "taskInvoke",
1596
- id: task.id
1597
- },
1598
- actor
1599
- ), task.spawns !== void 0)) {
1600
- const refs = await spawnChildren(ctx, mutation, task, task.spawns, actor);
1601
- entry.spawnedInstances = refs;
2029
+ }), await queueEffects(ctx, mutation, task.effects, { kind: "task", name: task.name }, actor, {
2030
+ taskName: task.name
2031
+ });
2032
+ let pendingChildren;
2033
+ if (task.subworkflows !== void 0) {
2034
+ const createsBefore = mutation.pendingCreates.length, refs = await spawnSubworkflows(ctx, mutation, task, task.subworkflows, actor);
2035
+ entry.spawnedInstances = refs, pendingChildren = mutation.pendingCreates.slice(createsBefore).map((c) => ({ _id: c._id, stage: c.currentStage, status: "active" }));
1602
2036
  for (const ref of refs)
1603
2037
  mutation.history.push({
1604
2038
  _key: randomKey(),
1605
- _type: "workflow.history.spawned",
2039
+ _type: "spawned",
1606
2040
  at: now,
1607
- taskId: task.id,
2041
+ task: task.name,
1608
2042
  instanceRef: ref
1609
2043
  });
1610
- if (await checkSpawnCompletion(ctx.client, entry, task.spawns)) {
1611
- const previousStatus = entry.status;
1612
- entry.status = "done", entry.completedAt = now, mutation.history.push(
1613
- taskStatusChangedEntry({
1614
- stageId: mutation.currentStageId,
1615
- taskId: task.id,
1616
- from: previousStatus,
1617
- to: "done",
1618
- at: now,
1619
- ...actor !== void 0 ? { actor } : {}
1620
- })
1621
- );
1622
- }
1623
2044
  }
2045
+ await autoResolveOnActivate(ctx, mutation, task, entry, now, pendingChildren) || resolveMachineStep(mutation, task, entry, now);
2046
+ }
2047
+ async function autoResolveOnActivate(ctx, mutation, task, entry, now, pendingChildren) {
2048
+ if (task.failWhen === void 0 && effectiveCompleteWhen(task) === void 0) return !1;
2049
+ const vars = pendingChildren !== void 0 ? { subworkflows: pendingChildren } : await taskConditionVars(ctx, entry), outcome = await evaluateResolutionGate(ctx, task, vars);
2050
+ return outcome === void 0 ? !1 : (applyTaskStatusChange({
2051
+ entry,
2052
+ history: mutation.history,
2053
+ stage: mutation.currentStage,
2054
+ to: outcome,
2055
+ at: now,
2056
+ actor: gateActor(outcome)
2057
+ }), !0);
2058
+ }
2059
+ async function taskConditionVars(ctx, entry) {
2060
+ return entry.spawnedInstances === void 0 || entry.spawnedInstances.length === 0 ? { subworkflows: [] } : { subworkflows: await subworkflowRows(ctx, entry.spawnedInstances) };
2061
+ }
2062
+ function resolveMachineStep(mutation, task, entry, now) {
2063
+ const waitsForActor = task.actions !== void 0 && task.actions.length > 0, waitsForCondition = task.completeWhen !== void 0 || task.subworkflows !== void 0;
2064
+ waitsForActor || waitsForCondition || applyTaskStatusChange({
2065
+ entry,
2066
+ history: mutation.history,
2067
+ stage: mutation.currentStage,
2068
+ to: "done",
2069
+ at: now,
2070
+ actor: { kind: "system", id: "engine.machineStep" }
2071
+ });
1624
2072
  }
1625
2073
  async function buildStageTasks(ctx, stage) {
1626
2074
  const entries = [];
1627
2075
  for (const task of stage.tasks ?? []) {
1628
- const inScope = await ctxEvaluateFilter(ctx, task.filter);
2076
+ const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
1629
2077
  entries.push({
1630
2078
  _key: randomKey(),
1631
- id: task.id,
2079
+ name: task.name,
1632
2080
  status: inScope ? "pending" : "skipped",
1633
2081
  ...task.filter !== void 0 ? {
1634
2082
  filterEvaluation: {
@@ -1640,22 +2088,23 @@ async function buildStageTasks(ctx, stage) {
1640
2088
  }
1641
2089
  return entries;
1642
2090
  }
1643
- function activatesOnStageEnter(task) {
1644
- return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
2091
+ function isTerminalStage(stage) {
2092
+ return (stage.transitions ?? []).length === 0;
1645
2093
  }
1646
2094
  async function setStage(args) {
1647
- const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId, {
1648
- ...options?.clock ? { clock: options.clock } : {}
2095
+ const { client, instanceId, targetStage, reason, options } = args, ctx = await loadContext(client, instanceId, {
2096
+ ...options?.clock ? { clock: options.clock } : {},
2097
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1649
2098
  });
1650
- return commitSetStage(ctx, targetStageId, reason, options?.actor);
2099
+ return commitSetStage(ctx, targetStage, reason, options?.actor);
1651
2100
  }
1652
2101
  async function enterStage(ctx, mutation, nextStage, actor, at) {
1653
- mutation.currentStageId = nextStage.id;
2102
+ mutation.currentStage = nextStage.name;
1654
2103
  const nextStageEntry = {
1655
2104
  _key: randomKey(),
1656
- id: nextStage.id,
2105
+ name: nextStage.name,
1657
2106
  enteredAt: at,
1658
- state: await resolveStageStateSlots({
2107
+ state: await resolveStageStateEntries({
1659
2108
  client: ctx.client,
1660
2109
  instance: ctx.instance,
1661
2110
  stage: nextStage,
@@ -1663,55 +2112,44 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1663
2112
  }),
1664
2113
  tasks: await buildStageTasks(ctx, nextStage)
1665
2114
  };
1666
- mutation.stages.push(nextStageEntry), await queueEffects(
1667
- ctx,
1668
- mutation,
1669
- nextStage.onEnter,
1670
- { kind: "stageEnter", id: nextStage.id },
1671
- actor
1672
- );
2115
+ mutation.stages.push(nextStageEntry);
1673
2116
  for (const task of nextStage.tasks ?? []) {
1674
- if (!activatesOnStageEnter(task)) continue;
1675
- const entry = nextStageEntry.tasks.find((t) => t.id === task.id);
2117
+ if (task.activation !== "auto") continue;
2118
+ const entry = nextStageEntry.tasks.find((t) => t.name === task.name);
1676
2119
  entry && entry.status === "pending" && await activateTask(ctx, mutation, task, entry, actor);
1677
2120
  }
1678
- nextStage.kind === "terminal" && (mutation.completedAt = at);
2121
+ isTerminalStage(nextStage) && (mutation.completedAt = at);
1679
2122
  }
1680
- async function beginStageMove(ctx, currentStage, actor) {
1681
- const mutation = startMutation(ctx.instance), at = ctx.now;
1682
- return await queueEffects(
1683
- ctx,
1684
- mutation,
1685
- currentStage.onExit,
1686
- { kind: "stageExit", id: currentStage.id },
1687
- actor
1688
- ), { mutation, at };
2123
+ function isTerminal(ctx) {
2124
+ return ctx.instance.completedAt !== void 0;
1689
2125
  }
1690
- async function commitSetStage(ctx, targetStageId, reason, actor) {
1691
- const currentStage = findStage(ctx.definition, ctx.instance.currentStageId), nextStage = findStage(ctx.definition, targetStageId);
1692
- if (currentStage.id === nextStage.id)
2126
+ async function commitSetStage(ctx, targetStage, reason, actor) {
2127
+ if (isTerminal(ctx))
2128
+ return { fired: !1 };
2129
+ const currentStage = findStage(ctx.definition, ctx.instance.currentStage), nextStage = findStage(ctx.definition, targetStage);
2130
+ if (currentStage.name === nextStage.name)
1693
2131
  return { fired: !1 };
1694
- const { mutation, at } = await beginStageMove(ctx, currentStage, actor), transitionId = `setStage:${currentStage.id}->${nextStage.id}`;
2132
+ const mutation = startMutation(ctx.instance), at = ctx.now, transitionName = `setStage:${currentStage.name}->${nextStage.name}`;
1695
2133
  mutation.history.push(
1696
2134
  ...stageTransitionHistory({
1697
- fromStageId: currentStage.id,
1698
- toStageId: nextStage.id,
2135
+ fromStage: currentStage.name,
2136
+ toStage: nextStage.name,
1699
2137
  at,
1700
- transitionId,
2138
+ transition: transitionName,
1701
2139
  via: "setStage",
1702
2140
  ...actor !== void 0 ? { actor } : {},
1703
2141
  ...reason !== void 0 ? { reason } : {}
1704
2142
  })
1705
2143
  );
1706
2144
  const priorEntry = findOpenStageEntry(mutation);
1707
- return priorEntry !== void 0 && (priorEntry.exitedAt = at), await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.id, nextStage.id), {
2145
+ return priorEntry !== void 0 && (priorEntry.exitedAt = at), await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.name, nextStage.name), {
1708
2146
  fired: !0,
1709
- fromStageId: currentStage.id,
1710
- toStageId: nextStage.id,
1711
- transitionId
2147
+ fromStage: currentStage.name,
2148
+ toStage: nextStage.name,
2149
+ transition: transitionName
1712
2150
  };
1713
2151
  }
1714
- async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
2152
+ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
1715
2153
  await persistThenDeploy(
1716
2154
  ctx,
1717
2155
  mutation,
@@ -1720,7 +2158,7 @@ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1720
2158
  clientForGdr: ctx.clientForGdr,
1721
2159
  instance: materializeInstance(ctx.instance, mutation),
1722
2160
  definition: ctx.definition,
1723
- stageId: enteredStageId,
2161
+ stageName: enteredStage,
1724
2162
  now: ctx.now
1725
2163
  })
1726
2164
  ), await retractStageGuards({
@@ -1728,7 +2166,7 @@ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1728
2166
  clientForGdr: ctx.clientForGdr,
1729
2167
  instance: ctx.instance,
1730
2168
  definition: ctx.definition,
1731
- stageId: exitedStageId,
2169
+ stageName: exitedStage,
1732
2170
  now: ctx.now
1733
2171
  });
1734
2172
  }
@@ -1744,53 +2182,63 @@ async function persistThenDeploy(ctx, mutation, deploy) {
1744
2182
  deploy
1745
2183
  });
1746
2184
  }
1747
- async function refreshStageGuards(ctx, mutation, stageId) {
2185
+ async function refreshStageGuards(ctx, mutation, stageName) {
1748
2186
  await deployStageGuards({
1749
2187
  client: ctx.client,
1750
2188
  clientForGdr: ctx.clientForGdr,
1751
2189
  instance: materializeInstance(ctx.instance, mutation),
1752
2190
  definition: ctx.definition,
1753
- stageId,
2191
+ stageName,
1754
2192
  now: ctx.now
1755
2193
  });
1756
2194
  }
1757
- async function commitTransition(ctx, action, actor) {
1758
- const currentStage = findStage(ctx.definition, ctx.instance.currentStageId), transition = await pickTransition(ctx, currentStage);
2195
+ async function commitTransition(ctx, actor) {
2196
+ if (isTerminal(ctx))
2197
+ return { fired: !1 };
2198
+ const currentStage = findStage(ctx.definition, ctx.instance.currentStage), transition = await pickTransition(ctx, currentStage);
1759
2199
  if (transition === void 0)
1760
2200
  return { fired: !1 };
1761
- const { mutation, at } = await beginStageMove(ctx, currentStage, actor);
1762
- await queueEffects(
2201
+ const mutation = startMutation(ctx.instance), at = ctx.now;
2202
+ runOps({
2203
+ ops: transition.ops,
2204
+ mutation,
2205
+ stage: currentStage.name,
2206
+ origin: { transition: transition.name },
2207
+ params: {},
2208
+ actor,
2209
+ self: selfGdr(ctx.instance),
2210
+ now: at
2211
+ }), await queueEffects(
1763
2212
  ctx,
1764
2213
  mutation,
1765
2214
  transition.effects,
1766
2215
  {
1767
2216
  kind: "transition",
1768
- id: transition.id ?? `${currentStage.id}->${transition.to}`
2217
+ name: transition.name
1769
2218
  },
1770
2219
  actor
1771
2220
  ), mutation.history.push(
1772
2221
  ...stageTransitionHistory({
1773
- fromStageId: currentStage.id,
1774
- toStageId: transition.to,
2222
+ fromStage: currentStage.name,
2223
+ toStage: transition.to,
1775
2224
  at,
1776
- ...transition.id !== void 0 ? { transitionId: transition.id } : {},
2225
+ transition: transition.name,
1777
2226
  ...actor !== void 0 ? { actor } : {}
1778
2227
  })
1779
2228
  );
1780
2229
  const priorEntry = findOpenStageEntry(mutation);
1781
2230
  priorEntry !== void 0 && (priorEntry.exitedAt = at);
1782
2231
  const nextStage = findStage(ctx.definition, transition.to);
1783
- return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.id, nextStage.id), {
2232
+ return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.name, nextStage.name), {
1784
2233
  fired: !0,
1785
- fromStageId: currentStage.id,
1786
- toStageId: transition.to,
1787
- ...transition.id !== void 0 ? { transitionId: transition.id } : {}
2234
+ fromStage: currentStage.name,
2235
+ toStage: transition.to,
2236
+ transition: transition.name
1788
2237
  };
1789
2238
  }
1790
- async function pickTransition(ctx, stage, action) {
1791
- const candidates = (stage.transitions ?? []).filter((t) => t.on === void 0);
1792
- for (const candidate of candidates)
1793
- if (await ctxEvaluateFilter(ctx, candidate.filter)) return candidate;
2239
+ async function pickTransition(ctx, stage) {
2240
+ for (const candidate of stage.transitions ?? [])
2241
+ if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
1794
2242
  }
1795
2243
  async function evaluateAutoTransitions(args) {
1796
2244
  const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
@@ -1798,58 +2246,27 @@ async function evaluateAutoTransitions(args) {
1798
2246
  ...clock ? { clock } : {},
1799
2247
  ...overlay ? { overlay } : {}
1800
2248
  });
1801
- return commitTransition(ctx, void 0, options?.actor);
2249
+ return commitTransition(ctx, options?.actor);
1802
2250
  }
1803
2251
  async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
1804
2252
  const instance = await client.getDocument(instanceId);
1805
- if (!instance || instance.stages.length > 0 || instance.pendingEffects.length > 0) return;
1806
- const definition = JSON.parse(instance.definitionSnapshot), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2253
+ if (!instance || instance.stages.length > 0) return;
2254
+ const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.name === instance.currentStage);
1807
2255
  if (stage === void 0) return;
1808
- const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
2256
+ const ctx = await buildEngineContext({
1809
2257
  client,
1810
2258
  clientForGdr: clientForGdr ?? (() => client),
1811
- instance
1812
- }), tasks = [];
1813
- for (const task of stage.tasks ?? []) {
1814
- const inScope = await evaluateFilter({
1815
- filter: task.filter,
1816
- definition,
1817
- snapshot,
1818
- params: buildParams(instance, now)
1819
- });
1820
- tasks.push({
1821
- _key: randomKey(),
1822
- id: task.id,
1823
- status: inScope ? "pending" : "skipped"
1824
- });
1825
- }
1826
- const initialStageEntry = {
2259
+ instance,
2260
+ definition,
2261
+ ...clock ? { clock } : {}
2262
+ }), now = ctx.now, initialStageEntry = {
1827
2263
  _key: randomKey(),
1828
- id: stage.id,
2264
+ name: stage.name,
1829
2265
  enteredAt: now,
1830
- state: await resolveStageStateSlots({ client, instance, stage, now }),
1831
- tasks
1832
- }, pendingEffects = [], newHistory = [...instance.history];
1833
- for (const effect of stage.onEnter ?? []) {
1834
- const params = await resolveBindings({
1835
- bindings: effect.bindings,
1836
- staticInput: effect.input,
1837
- instance,
1838
- now,
1839
- ...actor !== void 0 ? { actor } : {}
1840
- }), { pending, history } = buildQueuedEffect(
1841
- effect,
1842
- { kind: "stageEnter", id: stage.id },
1843
- params,
1844
- actor,
1845
- now
1846
- );
1847
- pendingEffects.push(pending), newHistory.push(history);
1848
- }
1849
- const committed = await client.patch(instance._id).set({
2266
+ state: await resolveStageStateEntries({ client, instance, stage, now }),
2267
+ tasks: await buildStageTasks(ctx, stage)
2268
+ }, committed = await client.patch(instance._id).set({
1850
2269
  stages: [initialStageEntry],
1851
- pendingEffects,
1852
- history: newHistory,
1853
2270
  lastChangedAt: now
1854
2271
  }).ifRevisionId(instance._rev).commit();
1855
2272
  await deployOrRollback({
@@ -1858,7 +2275,6 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1858
2275
  committedRev: committed._rev,
1859
2276
  restore: {
1860
2277
  stages: instance.stages,
1861
- pendingEffects: instance.pendingEffects,
1862
2278
  history: instance.history
1863
2279
  },
1864
2280
  reversible: !0,
@@ -1867,7 +2283,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1867
2283
  clientForGdr: clientForGdr ?? (() => client),
1868
2284
  instance,
1869
2285
  definition,
1870
- stageId: stage.id,
2286
+ stageName: stage.name,
1871
2287
  now
1872
2288
  })
1873
2289
  }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
@@ -1875,7 +2291,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1875
2291
  async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
1876
2292
  const resolvedClientForGdr = clientForGdr ?? (() => client);
1877
2293
  for (const task of stage.tasks ?? []) {
1878
- if (!activatesOnStageEnter(task)) continue;
2294
+ if (task.activation !== "auto") continue;
1879
2295
  const updated = await client.getDocument(instanceId);
1880
2296
  if (!updated) continue;
1881
2297
  const updatedCtx = await buildEngineContext({
@@ -1884,21 +2300,19 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
1884
2300
  instance: updated,
1885
2301
  definition,
1886
2302
  ...clock ? { clock } : {}
1887
- }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
2303
+ }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.name === task.name);
1888
2304
  mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
1889
2305
  }
1890
2306
  }
1891
2307
  const CASCADE_LIMIT = 100;
1892
2308
  async function gatherAutoResolveCandidates(ctx, tasks) {
1893
2309
  const currentTasksList = findCurrentTasks(ctx.instance), candidates = [];
1894
- for (const task of tasks)
1895
- if (currentTasksList.find((e) => e.id === task.id)?.status === "active") {
1896
- if (task.failWhen !== void 0 && await ctxEvaluateFilter(ctx, task.failWhen)) {
1897
- candidates.push({ task, outcome: "failed" });
1898
- continue;
1899
- }
1900
- task.completeWhen !== void 0 && await ctxEvaluateFilter(ctx, task.completeWhen) && candidates.push({ task, outcome: "done" });
1901
- }
2310
+ for (const task of tasks) {
2311
+ const entry = currentTasksList.find((e) => e.name === task.name);
2312
+ if (entry?.status !== "active") continue;
2313
+ const outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
2314
+ outcome !== void 0 && candidates.push({ task, outcome });
2315
+ }
1902
2316
  return candidates;
1903
2317
  }
1904
2318
  async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
@@ -1906,31 +2320,24 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, over
1906
2320
  ...clientForGdr ? { clientForGdr } : {},
1907
2321
  ...clock ? { clock } : {},
1908
2322
  ...overlay ? { overlay } : {}
1909
- }), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1910
- (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
2323
+ }), stage = findStage(ctx.definition, ctx.instance.currentStage), gatedTasks = (stage.tasks ?? []).filter(
2324
+ (t) => effectiveCompleteWhen(t) !== void 0 || t.failWhen !== void 0
1911
2325
  );
1912
- if (tasksWithAutoPredicate.length === 0) return 0;
1913
- const candidates = await gatherAutoResolveCandidates(ctx, tasksWithAutoPredicate);
2326
+ if (gatedTasks.length === 0) return 0;
2327
+ const candidates = await gatherAutoResolveCandidates(ctx, gatedTasks);
1914
2328
  if (candidates.length === 0) return 0;
1915
2329
  const mutation = startMutation(ctx.instance);
1916
2330
  let count = 0;
1917
2331
  for (const { task, outcome } of candidates) {
1918
- const mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1919
- if (mutEntry === void 0 || mutEntry.status !== "active") continue;
1920
- const previousStatus = mutEntry.status, actor = {
1921
- kind: "system",
1922
- id: outcome === "failed" ? "engine.failWhen" : "engine.completeWhen"
1923
- }, now = ctx.now;
1924
- mutEntry.status = outcome, mutEntry.completedAt = now, mutEntry.completedBy = actor.id, mutation.history.push(
1925
- taskStatusChangedEntry({
1926
- stageId: stage.id,
1927
- taskId: task.id,
1928
- from: previousStatus,
1929
- to: outcome,
1930
- at: now,
1931
- actor
1932
- })
1933
- ), count++;
2332
+ const mutEntry = currentTasks(mutation).find((t) => t.name === task.name);
2333
+ mutEntry === void 0 || mutEntry.status !== "active" || (applyTaskStatusChange({
2334
+ entry: mutEntry,
2335
+ history: mutation.history,
2336
+ stage: stage.name,
2337
+ to: outcome,
2338
+ at: ctx.now,
2339
+ actor: gateActor(outcome)
2340
+ }), count++);
1934
2341
  }
1935
2342
  return count > 0 && await persist(ctx, mutation), count;
1936
2343
  }
@@ -1949,43 +2356,47 @@ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, c
1949
2356
  throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
1950
2357
  }
1951
2358
  }
1952
- async function findSatisfiedParentSpawn(client, child) {
2359
+ async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
1953
2360
  const parentRef = child.ancestors.at(-1);
1954
2361
  if (parentRef === void 0) return;
1955
2362
  const parent = await client.getDocument(gdrToBareId(parentRef.id));
1956
2363
  if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
1957
- const definition = JSON.parse(parent.definitionSnapshot), stage = definition.stages.find((s) => s.id === parent.currentStageId);
2364
+ const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
1958
2365
  if (stage === void 0) return;
1959
- const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.spawns === void 0 ? !1 : parentCurrentTasks.find((e) => e.id === t.id)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
1960
- if (task?.spawns === void 0) return;
1961
- const entry = parentCurrentTasks.find((e) => e.id === task.id);
1962
- if (!(entry === void 0 || entry.status === "done") && await checkSpawnCompletion(client, entry, task.spawns))
1963
- return { parent, definition, stage, task };
2366
+ const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.subworkflows === void 0 ? !1 : parentCurrentTasks.find((e) => e.name === t.name)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
2367
+ if (task?.subworkflows === void 0) return;
2368
+ const entry = parentCurrentTasks.find((e) => e.name === task.name);
2369
+ if (entry === void 0 || entry.status !== "active") return;
2370
+ const ctx = await buildEngineContext({
2371
+ client,
2372
+ clientForGdr,
2373
+ instance: parent,
2374
+ definition,
2375
+ ...clock ? { clock } : {}
2376
+ }), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
2377
+ if (outcome !== void 0)
2378
+ return { parent, definition, stage, task, outcome };
1964
2379
  }
1965
2380
  async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
1966
2381
  const instance = await client.getDocument(instanceId);
1967
2382
  if (!instance) return;
1968
- const found = await findSatisfiedParentSpawn(client, instance);
2383
+ const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
1969
2384
  if (found === void 0) return;
1970
- const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
2385
+ const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
1971
2386
  client,
1972
- clientForGdr: clientForGdr ?? (() => client),
2387
+ clientForGdr: resolvedClientForGdr,
1973
2388
  instance: parent,
1974
2389
  definition,
1975
2390
  ...clock ? { clock } : {}
1976
- }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.id === task.id);
1977
- if (mutEntry === void 0) return;
1978
- const previousStatus = mutEntry.status, now = ctx.now;
1979
- mutEntry.status = "done", mutEntry.completedAt = now, mutation.history.push(
1980
- taskStatusChangedEntry({
1981
- stageId: stage.id,
1982
- taskId: task.id,
1983
- from: previousStatus,
1984
- to: "done",
1985
- at: now,
1986
- ...actor !== void 0 ? { actor } : {}
1987
- })
1988
- ), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock);
2391
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
2392
+ mutEntry !== void 0 && (applyTaskStatusChange({
2393
+ entry: mutEntry,
2394
+ history: mutation.history,
2395
+ stage: stage.name,
2396
+ to: outcome,
2397
+ at: ctx.now,
2398
+ actor: gateActor(outcome)
2399
+ }), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
1989
2400
  }
1990
2401
  const parsedFilters = /* @__PURE__ */ new Map();
1991
2402
  async function matchesFilter(args) {
@@ -2081,7 +2492,6 @@ async function fetchGrantsCached(requestFn, resourcePath) {
2081
2492
  return;
2082
2493
  }
2083
2494
  }
2084
- const WILDCARD_ROLE = "*";
2085
2495
  async function evaluateInstance(args) {
2086
2496
  const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2087
2497
  validateTags(tags);
@@ -2093,45 +2503,45 @@ async function evaluateInstance(args) {
2093
2503
  throw new Error(`Workflow instance "${instanceId}" not found`);
2094
2504
  if (!tags.some((t) => instance.tags?.includes(t)))
2095
2505
  throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2096
- const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance });
2506
+ const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
2097
2507
  return evaluateFromSnapshot({
2098
2508
  instance,
2099
2509
  definition,
2100
2510
  actor,
2101
2511
  snapshot,
2512
+ guards,
2102
2513
  now,
2103
2514
  ...grants !== void 0 ? { grants } : {}
2104
2515
  });
2105
2516
  }
2106
2517
  async function evaluateFromSnapshot(args) {
2107
- const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.id === instance.currentStageId);
2518
+ const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.name === instance.currentStage);
2108
2519
  if (stage === void 0)
2109
2520
  throw new Error(
2110
- `Instance "${instance._id}" currentStageId "${instance.currentStageId}" not in definition`
2521
+ `Instance "${instance._id}" currentStage "${instance.currentStage}" not in definition`
2111
2522
  );
2112
- const currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], taskEvaluations = [];
2523
+ const scope = await renderScope({ instance, definition, actor, grants, snapshot, now }), currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], guardDenial = await instanceGuardReason(instance, actor, args.guards), taskEvaluations = [];
2113
2524
  for (const task of stage.tasks ?? [])
2114
2525
  taskEvaluations.push(
2115
2526
  await evaluateTask({
2116
2527
  task,
2117
- statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2528
+ statusEntry: currentTaskEntries.find((t) => t.name === task.name),
2118
2529
  instance,
2119
- definition,
2120
2530
  actor,
2121
- grants,
2122
2531
  snapshot,
2123
- now
2532
+ scope,
2533
+ stageHasExits: !isTerminalStage(stage),
2534
+ guardDenial
2124
2535
  })
2125
2536
  );
2126
2537
  const transitionEvaluations = [];
2127
2538
  for (const transition of stage.transitions ?? [])
2128
2539
  transitionEvaluations.push({
2129
2540
  transition,
2130
- filterSatisfied: await evaluateFilter({
2131
- filter: transition.filter,
2132
- definition,
2541
+ filterSatisfied: await evaluateCondition({
2542
+ condition: transition.filter,
2133
2543
  snapshot,
2134
- params: buildParams(instance, now)
2544
+ params: scope
2135
2545
  })
2136
2546
  });
2137
2547
  const currentStage = {
@@ -2148,142 +2558,102 @@ async function evaluateFromSnapshot(args) {
2148
2558
  canInteract
2149
2559
  };
2150
2560
  }
2561
+ async function renderScope(args) {
2562
+ const { instance, definition, actor, grants, snapshot, now } = args, params = {
2563
+ ...buildParams({ instance, now, snapshot }),
2564
+ actor,
2565
+ assigned: !1,
2566
+ can: await advisoryCan(instance, actor, grants)
2567
+ };
2568
+ return { ...await evaluatePredicates({
2569
+ predicates: definition.predicates,
2570
+ snapshot,
2571
+ params
2572
+ }), ...params };
2573
+ }
2574
+ async function advisoryCan(instance, actor, grants) {
2575
+ if (grants === void 0) return;
2576
+ const can = {};
2577
+ for (const permission of schema.DOCUMENT_VALUE_PERMISSIONS)
2578
+ can[permission] = await grantsPermissionOn({
2579
+ document: instance,
2580
+ grants,
2581
+ permission,
2582
+ userId: actor.id
2583
+ });
2584
+ return can;
2585
+ }
2151
2586
  async function evaluateTask(args) {
2152
- const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
2587
+ const { task, statusEntry, instance, actor, snapshot, scope, stageHasExits, guardDenial } = args, status = statusEntry?.status ?? "pending", assigned = assignedFor(instance, task.name, actor), taskScope = {
2588
+ ...scope,
2589
+ state: {
2590
+ ...scope.state,
2591
+ ...scopedStateOverlay(instance, snapshot, task.name)
2592
+ },
2593
+ assigned
2594
+ }, actions = [];
2153
2595
  for (const action of task.actions ?? [])
2154
2596
  actions.push(
2155
2597
  await evaluateAction({
2156
2598
  action,
2157
- task,
2158
- ...statusEntry !== void 0 ? { statusEntry } : {},
2159
2599
  status,
2160
2600
  instance,
2161
- definition,
2162
- actor,
2163
- ...grants !== void 0 ? { grants } : {},
2164
2601
  snapshot,
2165
- now
2602
+ taskScope,
2603
+ stageHasExits,
2604
+ guardDenial
2166
2605
  })
2167
2606
  );
2168
2607
  return {
2169
2608
  task,
2170
- status,
2171
- // "pending on actor" treats both `pending` and `active` as waiting on
2172
- // the assignee — pending tasks just haven't been auto-invoked yet, but
2173
- // they're still inbox items.
2174
- pendingOnActor: (status === "active" || status === "pending") && actorIsAssignee(actor, task.assignees ?? []),
2175
- actions
2176
- };
2177
- }
2178
- async function evaluateAction(args) {
2179
- const { action, task, status, instance, definition, actor, grants, snapshot, now } = args, syncReason = lifecycleReason(instance, definition, status) ?? actorReason(action, task, actor);
2180
- if (syncReason !== void 0) return disabled(action, syncReason);
2181
- const asyncReason = await permissionReason(action, instance, actor, grants) ?? await filterReason(action, instance, definition, snapshot, now);
2182
- return asyncReason !== void 0 ? disabled(action, asyncReason) : { action, allowed: !0 };
2183
- }
2184
- function lifecycleReason(instance, definition, status) {
2185
- if (instance.completedAt !== void 0)
2186
- return { kind: "instance-completed", completedAt: instance.completedAt };
2187
- if (definition.stages.find((s) => s.id === instance.currentStageId)?.kind === "terminal")
2188
- return { kind: "stage-terminal", stageId: instance.currentStageId };
2189
- if (status === "done" || status === "skipped" || status === "failed")
2190
- return { kind: "task-not-active", status };
2191
- }
2192
- function actorReason(action, task, actor) {
2193
- const actorRoles = actor.roles ?? [];
2194
- if (action.roles !== void 0 && action.roles.length > 0 && !(actorRoles.includes(WILDCARD_ROLE) || action.roles.some((r) => actorRoles.includes(r))))
2195
- return { kind: "role-required", required: [...action.roles], actorRoles: [...actorRoles] };
2196
- if (action.requireAssignment === !0 && !actorIsAssignee(actor, task.assignees ?? []))
2197
- return { kind: "not-assigned", assignees: [...task.assignees ?? []], actorId: actor.id };
2198
- }
2199
- async function permissionReason(action, instance, actor, grants) {
2200
- if (grants === void 0) return;
2201
- const permission = action.requiredPermission ?? "update";
2202
- if (!await grantsPermissionOn({
2203
- document: instance,
2204
- grants,
2205
- permission,
2206
- userId: actor.id
2207
- }))
2208
- return { kind: "permission-denied", permission, matchingGrants: grants.length };
2209
- }
2210
- async function filterReason(action, instance, definition, snapshot, now) {
2211
- if (!(action.filter === void 0 || await evaluateFilter({
2212
- filter: action.filter,
2213
- definition,
2214
- snapshot,
2215
- params: buildParams(instance, now)
2216
- })))
2217
- return { kind: "filter-failed", filter: action.filter };
2218
- }
2219
- function disabled(action, reason) {
2220
- return { action, allowed: !1, disabledReason: reason };
2221
- }
2222
- function actorIsAssignee(actor, assignees) {
2223
- if (assignees.length === 0) return !1;
2224
- const actorRoles = actor.roles ?? [], wildcard = actorRoles.includes(WILDCARD_ROLE);
2225
- for (const assignee of assignees)
2226
- if (assignee.kind === "user" && assignee.id === actor.id || assignee.kind === "role" && (wildcard || actorRoles.includes(assignee.role)))
2227
- return !0;
2228
- return !1;
2229
- }
2230
- function parseDefinitionSnapshot(instance) {
2231
- try {
2232
- return JSON.parse(instance.definitionSnapshot);
2233
- } catch (err) {
2234
- throw new Error(
2235
- `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
2236
- { cause: err }
2237
- );
2238
- }
2609
+ status,
2610
+ // "pending on actor" treats both `pending` and `active` as waiting on
2611
+ // the assignee — pending tasks just haven't been activated yet, but
2612
+ // they're still inbox items. The inbox reads the assignees-kind state
2613
+ // entry BY KIND (who-data is state).
2614
+ pendingOnActor: (status === "active" || status === "pending") && assigned,
2615
+ actions
2616
+ };
2239
2617
  }
2240
- const resourceKey = (r) => `${r.type}.${r.id}`;
2241
- function guardsForResource(client) {
2242
- return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2618
+ async function evaluateAction(args) {
2619
+ const { action, status, instance, snapshot, taskScope, stageHasExits, guardDenial } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2620
+ return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : action.filter !== void 0 && !await evaluateCondition({
2621
+ condition: action.filter,
2622
+ snapshot,
2623
+ params: taskScope
2624
+ }) ? disabled(action, { kind: "filter-failed", filter: action.filter }) : { action, allowed: !0 };
2243
2625
  }
2244
- async function guardsForInstance(args) {
2245
- const { client, clientForGdr, instance } = args, clientsByResource = /* @__PURE__ */ new Map([
2246
- [resourceKey(instance.workflowResource), client]
2247
- ]), stateUris = [
2248
- ...collectSlotDocUris(instance.state),
2249
- ...instance.stages.flatMap((stage) => collectSlotDocUris(stage.state))
2250
- ];
2251
- for (const uri of stateUris) {
2252
- const parsed = tryParseGdr(uri);
2253
- if (parsed === void 0) continue;
2254
- const key = resourceKey(resourceFromParsed(parsed));
2255
- clientsByResource.has(key) || clientsByResource.set(key, clientForGdr(parsed));
2256
- }
2257
- const perResource = await Promise.all(
2258
- [...clientsByResource.values()].map(
2259
- (c) => c.fetch("*[_type == $t && sourceInstanceId == $id]", {
2260
- t: GUARD_DOC_TYPE,
2261
- id: instance._id
2262
- })
2263
- )
2264
- );
2265
- return dedupById(perResource.flat());
2626
+ async function instanceGuardReason(instance, actor, guards) {
2627
+ if (guards === void 0 || guards.length === 0) return;
2628
+ const denied = await instanceWriteDenials({ instance, guards, identity: actor.id });
2629
+ if (denied.length !== 0)
2630
+ return {
2631
+ kind: "mutation-guard-denied",
2632
+ guardIds: denied.map((g) => g._id),
2633
+ detail: denied.map((g) => g.name ?? g._id).join(", ")
2634
+ };
2266
2635
  }
2267
- function tryParseGdr(uri) {
2268
- try {
2269
- return parseGdr(uri);
2270
- } catch {
2271
- return;
2272
- }
2636
+ function lifecycleReason(instance, status, stageHasExits) {
2637
+ if (instance.completedAt !== void 0)
2638
+ return { kind: "instance-completed", completedAt: instance.completedAt };
2639
+ if (!stageHasExits)
2640
+ return { kind: "stage-terminal", stage: instance.currentStage };
2641
+ if (schema.isTerminalTaskStatus(status))
2642
+ return { kind: "task-not-active", status };
2273
2643
  }
2274
- function dedupById(guards) {
2275
- return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2644
+ function disabled(action, reason) {
2645
+ return { action, allowed: !1, disabledReason: reason };
2276
2646
  }
2277
- function definitionDocId(_workflowResource, prefix, workflowId, version) {
2278
- return `${prefix}.${workflowId}.v${version}`;
2647
+ function definitionDocId(_workflowResource, prefix, definition, version) {
2648
+ return `${prefix}.${definition}.v${version}`;
2279
2649
  }
2280
2650
  async function sortByDependencies(client, definitions, tags, workflowResource) {
2281
- const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2651
+ const prefix = canonicalTag(tags), byName = /* @__PURE__ */ new Map();
2282
2652
  for (const def of definitions) {
2283
- const list = byWorkflowId.get(def.workflowId) ?? [];
2284
- list.push(def), byWorkflowId.set(def.workflowId, list);
2653
+ const list = byName.get(def.name) ?? [];
2654
+ list.push(def), byName.set(def.name, list);
2285
2655
  }
2286
- const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2656
+ const findInBatch = (ref) => findRefInBatch(byName, ref);
2287
2657
  return await assertCrossBatchRefsDeployed(client, definitions, {
2288
2658
  tags,
2289
2659
  workflowResource,
@@ -2291,8 +2661,8 @@ async function sortByDependencies(client, definitions, tags, workflowResource) {
2291
2661
  findInBatch
2292
2662
  }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2293
2663
  }
2294
- function findRefInBatch(byWorkflowId, ref) {
2295
- const candidates = byWorkflowId.get(ref.workflowId);
2664
+ function findRefInBatch(byName, ref) {
2665
+ const candidates = byName.get(ref.name);
2296
2666
  if (!(candidates === void 0 || candidates.length === 0))
2297
2667
  return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2298
2668
  (best, c) => best === void 0 || c.version > best.version ? c : best,
@@ -2302,7 +2672,7 @@ function findRefInBatch(byWorkflowId, ref) {
2302
2672
  async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2303
2673
  const missing = [];
2304
2674
  for (const def of definitions) {
2305
- const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2675
+ const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
2306
2676
  for (const ref of refsOf(def)) {
2307
2677
  if (ctx.findInBatch(ref) !== void 0) continue;
2308
2678
  const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
@@ -2318,16 +2688,16 @@ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2318
2688
  );
2319
2689
  }
2320
2690
  async function resolveDeployedRefLabel(client, ref, tags) {
2321
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
2691
+ const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
2322
2692
  if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2323
2693
  definitionLookupGroq(wantsExplicit),
2324
2694
  params
2325
2695
  ))
2326
- return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2696
+ return wantsExplicit ? `${ref.name} v${ref.version}` : ref.name;
2327
2697
  }
2328
2698
  function topoSortDefinitions(definitions, ctx) {
2329
2699
  const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2330
- const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2700
+ const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
2331
2701
  if (!visited.has(id)) {
2332
2702
  if (visiting.has(id))
2333
2703
  throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
@@ -2346,9 +2716,9 @@ function refsOf(def) {
2346
2716
  const out = [];
2347
2717
  for (const stage of def.stages)
2348
2718
  for (const task of stage.tasks ?? []) {
2349
- const ref = task.spawns?.definitionRef;
2719
+ const ref = task.subworkflows?.definition;
2350
2720
  ref !== void 0 && out.push({
2351
- workflowId: ref.workflowId,
2721
+ name: ref.name,
2352
2722
  ...ref.version !== void 0 ? { version: ref.version } : {}
2353
2723
  });
2354
2724
  }
@@ -2368,324 +2738,145 @@ function stableStringify(value) {
2368
2738
  ([a], [b]) => a.localeCompare(b)
2369
2739
  ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2370
2740
  }
2371
- async function loadDefinition(client, workflowId, version, tags) {
2741
+ async function loadDefinition(client, definition, version, tags) {
2372
2742
  if (version !== void 0) {
2373
2743
  const doc = await client.fetch(
2374
2744
  definitionLookupGroq(!0),
2375
- { workflowId, version, engineTags: tags }
2745
+ { definition, version, engineTags: tags }
2376
2746
  );
2377
2747
  if (!doc)
2378
- throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2748
+ throw new Error(`Workflow definition ${definition} v${version} not deployed`);
2379
2749
  return doc;
2380
2750
  }
2381
2751
  const latest = await client.fetch(definitionLookupGroq(!1), {
2382
- workflowId,
2752
+ definition,
2383
2753
  engineTags: tags
2384
2754
  });
2385
2755
  if (!latest)
2386
- throw new Error(`No deployed definition for workflow ${workflowId}`);
2756
+ throw new Error(`No deployed definition for workflow ${definition}`);
2387
2757
  return latest;
2388
2758
  }
2389
- const STATE_OP_TYPES = [
2390
- "workflow.op.state.set",
2391
- "workflow.op.state.append",
2392
- "workflow.op.state.updateWhere",
2393
- "workflow.op.state.removeWhere"
2394
- ];
2395
- function isStateOp(summary) {
2396
- return STATE_OP_TYPES.includes(summary.opType);
2397
- }
2398
- function validateActionParams(action, taskId, callerParams) {
2399
- const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
2400
- for (const decl of declared) {
2401
- const value = params[decl.id], present = decl.id in params && value !== void 0 && value !== null;
2402
- if (decl.required === !0 && !present) {
2403
- issues.push({ paramId: decl.id, reason: "required but missing" });
2404
- continue;
2405
- }
2406
- present && (checkParamType(value, decl) || issues.push({
2407
- paramId: decl.id,
2408
- reason: `expected paramType=${decl.paramType}, got ${typeof value}`
2409
- }));
2410
- }
2411
- if (issues.length > 0)
2412
- throw new ActionParamsInvalidError({ action: action.id, taskId, issues });
2413
- return params;
2414
- }
2415
- function hasStringField(value, field) {
2416
- return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
2417
- }
2418
- function checkParamType(value, decl) {
2419
- switch (decl.paramType) {
2420
- case "string":
2421
- case "url":
2422
- case "dateTime":
2423
- return typeof value == "string";
2424
- case "number":
2425
- return typeof value == "number" && Number.isFinite(value);
2426
- case "boolean":
2427
- return typeof value == "boolean";
2428
- case "actor":
2429
- return hasStringField(value, "kind");
2430
- case "doc.ref":
2431
- return hasStringField(value, "id");
2432
- case "doc.refs":
2433
- return Array.isArray(value) && value.every((v2) => checkParamType(v2, { paramType: "doc.ref" }));
2434
- case "json":
2435
- return !0;
2436
- default:
2437
- return !1;
2438
- }
2439
- }
2440
- async function runActionOps(args) {
2441
- const { ops, mutation, stageId, taskId, actionName, params, actor, self, now } = args;
2442
- if (ops === void 0 || ops.length === 0) return [];
2443
- const summaries = [];
2444
- for (const op of ops) {
2445
- const summary = applyOp(op, { mutation, stageId, taskId, params, actor, self, now });
2446
- summaries.push(summary), mutation.history.push({
2447
- _key: randomKey(),
2448
- _type: "workflow.history.opApplied",
2449
- at: now,
2450
- stageId,
2451
- taskId,
2452
- actionId: actionName,
2453
- opType: summary.opType,
2454
- ...summary.target !== void 0 ? { target: summary.target } : {},
2455
- ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
2456
- ...actor !== void 0 ? { actor } : {}
2457
- });
2458
- }
2459
- return summaries;
2460
- }
2461
- function applyOp(op, ctx) {
2462
- switch (op.type) {
2463
- case "workflow.op.state.set":
2464
- return applyStateSet(op, ctx);
2465
- case "workflow.op.state.append":
2466
- return applyStateAppend(op, ctx);
2467
- case "workflow.op.state.updateWhere":
2468
- return applyStateUpdateWhere(op, ctx);
2469
- case "workflow.op.state.removeWhere":
2470
- return applyStateRemoveWhere(op, ctx);
2471
- case "workflow.op.status.set":
2472
- return applyStatusSet(op, ctx);
2473
- }
2474
- }
2475
- function applyStateSet(op, ctx) {
2476
- const value = resolveOpSource(op.value, ctx), slot = locateSlot(
2477
- ctx,
2478
- op.target,
2479
- /*createIfMissing*/
2480
- !0
2481
- );
2482
- return slot !== void 0 && (validateSlotValue({ slotType: slot._type, slotId: slot.id, value }), slot.value = value), { opType: op.type, target: op.target, resolved: { value } };
2483
- }
2484
- function applyStateAppend(op, ctx) {
2485
- const item = resolveOpSource(op.item, ctx), slot = locateSlot(
2486
- ctx,
2487
- op.target,
2488
- /*createIfMissing*/
2489
- !0
2759
+ async function loadDefinitionVersions(client, definition, tags) {
2760
+ return client.fetch(
2761
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && count(tags[@ in $engineTags]) > 0] | order(version desc)`,
2762
+ { definition, engineTags: tags }
2490
2763
  );
2491
- if (slot !== void 0) {
2492
- validateSlotAppendItem({ slotType: slot._type, slotId: slot.id, item });
2493
- const current = Array.isArray(slot.value) ? slot.value : [];
2494
- slot.value = [...current, withRowKey(item)];
2495
- }
2496
- return { opType: op.type, target: op.target, resolved: { item } };
2497
2764
  }
2498
- function withRowKey(item) {
2499
- return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
2500
- }
2501
- function applyStateUpdateWhere(op, ctx) {
2502
- const slot = locateSlot(
2503
- ctx,
2504
- op.target,
2505
- /*createIfMissing*/
2506
- !1
2507
- ), resolvedMerge = {};
2508
- for (const [k, v2] of Object.entries(op.merge))
2509
- resolvedMerge[k] = resolveOpSource(v2, ctx);
2510
- return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.map(
2511
- (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...resolvedMerge } : row
2512
- )), { opType: op.type, target: op.target, resolved: { merge: resolvedMerge } };
2513
- }
2514
- function applyStateRemoveWhere(op, ctx) {
2515
- const slot = locateSlot(
2516
- ctx,
2517
- op.target,
2518
- /*createIfMissing*/
2519
- !1
2520
- );
2521
- return slot !== void 0 && Array.isArray(slot.value) && (slot.value = slot.value.filter(
2522
- (row) => !evalOpPredicate(op.where, row, ctx)
2523
- )), { opType: op.type, target: op.target };
2765
+ async function abortInstance(args) {
2766
+ const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
2767
+ ...options?.clock ? { clock: options.clock } : {},
2768
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
2769
+ });
2770
+ return commitAbort(ctx, reason, options?.actor);
2524
2771
  }
2525
- function applyStatusSet(op, ctx) {
2526
- const entry = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.id === op.taskId);
2527
- return entry !== void 0 && (entry.status = op.status), { opType: op.type, resolved: { taskId: op.taskId, status: op.status } };
2528
- }
2529
- function locateSlot(ctx, target, createIfMissing) {
2530
- let host;
2531
- if (target.scope === "workflow")
2532
- host = ctx.mutation.state;
2533
- else if (target.scope === "stage")
2534
- host = findOpenStageEntry(ctx.mutation)?.state;
2535
- else {
2536
- const task = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.id === ctx.taskId);
2537
- host = task?.state, host === void 0 && task !== void 0 && createIfMissing && (task.state = [], host = task.state);
2538
- }
2539
- if (host === void 0)
2540
- throw new Error(
2541
- `Op target scope "${target.scope}" has no state container on this instance \u2014 declare the slot or check stage/task identity`
2542
- );
2543
- let slot = host.find((s) => s.id === target.slotId);
2544
- return slot === void 0 && createIfMissing && (slot = {
2772
+ async function commitAbort(ctx, reason, actor) {
2773
+ if (ctx.instance.completedAt !== void 0)
2774
+ return { fired: !1 };
2775
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
2776
+ return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
2777
+ ...mutation.pendingEffects.map(
2778
+ (pending) => buildEffectHistoryEntry(pending, {
2779
+ status: "failed",
2780
+ ranAt: at,
2781
+ actor,
2782
+ detail: "instance aborted before dispatch",
2783
+ error: void 0,
2784
+ durationMs: void 0,
2785
+ outputs: void 0
2786
+ })
2787
+ )
2788
+ ), mutation.pendingEffects = [], mutation.history.push({
2545
2789
  _key: randomKey(),
2546
- _type: "workflow.state.value.string",
2547
- id: target.slotId,
2548
- value: null
2549
- }, host.push(slot)), slot;
2550
- }
2551
- function resolveOpSource(src, ctx) {
2552
- const staticValue = resolveStaticSource(src, ctx);
2553
- if (staticValue.handled) return staticValue.value;
2554
- switch (src.source) {
2555
- case "self":
2556
- return ctx.self;
2557
- case "stageId":
2558
- return ctx.stageId;
2559
- case "stateRead": {
2560
- const value = readSlotFromMutation(ctx, src.scope, src.slotId);
2561
- return src.path !== void 0 ? getPath(value, src.path) : value;
2562
- }
2563
- case "effectOutput": {
2564
- const entry = ctx.mutation.effectsContext.find((e) => e.id === src.contextId);
2565
- if (entry === void 0) return null;
2566
- const v2 = src.path !== void 0 ? getPath(entry.value, src.path) : entry.value;
2567
- return v2 === void 0 ? null : v2;
2568
- }
2569
- case "object": {
2570
- const out = {};
2571
- for (const [field, fieldSrc] of Object.entries(src.fields))
2572
- out[field] = resolveOpSource(fieldSrc, ctx);
2573
- return out;
2574
- }
2575
- case "row":
2576
- case "parentState":
2577
- return;
2578
- default:
2579
- return;
2580
- }
2581
- }
2582
- function slotValue(slots, slotId) {
2583
- return slots?.find((s) => s.id === slotId)?.value;
2584
- }
2585
- function readSlotFromMutation(ctx, scope, slotId) {
2586
- if (scope === "workflow") return slotValue(ctx.mutation.state, slotId);
2587
- const stageEntry = findOpenStageEntry(ctx.mutation);
2588
- if (scope === "stage") return slotValue(stageEntry?.state, slotId);
2589
- const task = stageEntry?.tasks.find((t) => t.id === ctx.taskId);
2590
- return slotValue(task?.state, slotId);
2591
- }
2592
- function evalOpPredicate(p, row, ctx) {
2593
- if ("all" in p) return p.all.every((sub) => evalOpPredicate(sub, row, ctx));
2594
- if ("any" in p) return p.any.some((sub) => evalOpPredicate(sub, row, ctx));
2595
- const target = resolveOpSource(p.equals, ctx);
2596
- return row[p.field] === target;
2790
+ _type: "aborted",
2791
+ at,
2792
+ stage: stage.name,
2793
+ ...reason !== void 0 ? { reason } : {},
2794
+ ...actor !== void 0 ? { actor } : {}
2795
+ }), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
2796
+ client: ctx.client,
2797
+ clientForGdr: ctx.clientForGdr,
2798
+ instance: ctx.instance,
2799
+ definition: ctx.definition,
2800
+ stageName: stage.name,
2801
+ now: ctx.now
2802
+ }), { fired: !0, stage: stage.name };
2597
2803
  }
2598
2804
  async function fireAction(args) {
2599
- const { client, instanceId, taskId, action, params, options } = args;
2805
+ const { client, instanceId, task, action, params, options } = args;
2600
2806
  for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2601
2807
  const ctx = await loadContext(client, instanceId, {
2602
- ...options?.clock ? { clock: options.clock } : {}
2808
+ ...options?.clock ? { clock: options.clock } : {},
2809
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
2603
2810
  });
2604
2811
  try {
2605
- return await commitAction(ctx, taskId, action, params, options?.actor);
2812
+ return await commitAction(ctx, task, action, params, options);
2606
2813
  } catch (error) {
2607
2814
  if (!isRevisionConflict(error)) throw error;
2608
2815
  }
2609
2816
  }
2610
2817
  throw new ConcurrentFireActionError({
2611
2818
  instanceId,
2612
- taskId,
2819
+ task,
2613
2820
  action,
2614
2821
  attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2615
2822
  });
2616
2823
  }
2617
- async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
2618
- const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
2824
+ async function resolveActionCommit(ctx, taskName, actionName, callerParams, options) {
2825
+ const actor = options?.actor, { stage, task } = findTaskInCurrentStage(ctx, taskName), action = (task.actions ?? []).find((a) => a.name === actionName);
2619
2826
  if (action === void 0)
2620
- throw new Error(`Action "${actionName}" not declared on task "${taskId}"`);
2621
- if (!await ctxEvaluateFilter(ctx, action.filter))
2622
- throw new Error(`Action "${actionName}" filter rejected on task "${taskId}"`);
2623
- const entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
2827
+ throw new Error(`Action "${actionName}" not declared on task "${taskName}"`);
2828
+ const can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0;
2829
+ if (!await ctxEvaluateCondition(ctx, action.filter, {
2830
+ taskName,
2831
+ ...actor !== void 0 ? { actor } : {},
2832
+ ...can !== void 0 ? { vars: { can } } : {}
2833
+ }))
2834
+ throw new Error(`Action "${actionName}" filter rejected on task "${taskName}"`);
2835
+ const entry = findCurrentTasks(ctx.instance).find((t) => t.name === taskName);
2624
2836
  if (entry === void 0 || entry.status !== "active")
2625
2837
  throw new Error(
2626
- `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2838
+ `Task "${taskName}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2627
2839
  );
2628
- const params = validateActionParams(action, taskId, callerParams);
2840
+ const params = validateActionParams(action, taskName, callerParams);
2629
2841
  return { stage, task, action, params };
2630
2842
  }
2631
- function applyActionSetStatus(action, mutation, mutEntry, stageId, taskId, actor, now) {
2632
- if (action.setStatus === void 0) return;
2633
- const initialStatus = mutEntry.status, newStatus = action.setStatus;
2634
- return mutEntry.status = newStatus, mutEntry.completedAt = now, actor && (mutEntry.completedBy = actor.id), mutation.history.push(
2635
- taskStatusChangedEntry({
2636
- stageId,
2637
- taskId,
2638
- from: initialStatus,
2639
- to: newStatus,
2640
- at: now,
2641
- ...actor !== void 0 ? { actor } : {}
2642
- })
2643
- ), newStatus;
2644
- }
2645
- async function commitAction(ctx, taskId, actionName, callerParams, actor) {
2646
- const { stage, task, action, params } = await resolveActionCommit(
2843
+ async function commitAction(ctx, taskName, actionName, callerParams, options) {
2844
+ const actor = options?.actor, { stage, action, params } = await resolveActionCommit(
2647
2845
  ctx,
2648
- taskId,
2846
+ taskName,
2649
2847
  actionName,
2650
- callerParams
2651
- ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
2848
+ callerParams,
2849
+ options
2850
+ ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskName), now = ctx.now;
2652
2851
  mutation.history.push({
2653
2852
  _key: randomKey(),
2654
- _type: "workflow.history.actionFired",
2853
+ _type: "actionFired",
2655
2854
  at: now,
2656
- stageId: stage.id,
2657
- taskId,
2658
- actionId: actionName,
2855
+ stage: stage.name,
2856
+ task: taskName,
2857
+ action: actionName,
2659
2858
  ...actor !== void 0 ? { actor } : {}
2660
2859
  });
2661
- const ranOps = await runActionOps({
2860
+ const statusBefore = mutEntry.status, ranOps = await runOps({
2662
2861
  ops: action.ops,
2663
2862
  mutation,
2664
- stageId: stage.id,
2665
- taskId,
2666
- actionName,
2863
+ stage: stage.name,
2864
+ origin: { task: taskName, action: actionName },
2667
2865
  params,
2668
2866
  actor,
2669
2867
  self: selfGdr(ctx.instance),
2670
2868
  now
2671
- }), newStatus = applyActionSetStatus(action, mutation, mutEntry, stage.id, taskId, actor, now);
2672
- return await queueEffects(
2673
- ctx,
2674
- mutation,
2675
- action.effects,
2676
- {
2677
- kind: "action",
2678
- id: `${task.id}:${actionName}`
2679
- },
2680
- actor,
2681
- params
2682
- ), await persistThenDeploy(
2869
+ }), newStatus = mutEntry.status !== statusBefore ? mutEntry.status : void 0;
2870
+ return await queueEffects(ctx, mutation, action.effects, { kind: "action", name: actionName }, actor, {
2871
+ callerParams: params,
2872
+ taskName
2873
+ }), await persistThenDeploy(
2683
2874
  ctx,
2684
2875
  mutation,
2685
- () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
2876
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
2686
2877
  ), {
2687
2878
  fired: !0,
2688
- taskId,
2879
+ task: taskName,
2689
2880
  action: actionName,
2690
2881
  ...newStatus !== void 0 ? { newStatus } : {},
2691
2882
  ...ranOps.length > 0 ? { ranOps } : {}
@@ -2693,25 +2884,22 @@ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
2693
2884
  }
2694
2885
  class ActionDisabledError extends Error {
2695
2886
  reason;
2696
- taskId;
2887
+ task;
2697
2888
  action;
2698
2889
  constructor(args) {
2699
- super(formatDisabledReason(args.taskId, args.action, args.reason)), this.name = "ActionDisabledError", this.reason = args.reason, this.taskId = args.taskId, this.action = args.action;
2890
+ super(formatDisabledReason(args.task, args.action, args.reason)), this.name = "ActionDisabledError", this.reason = args.reason, this.task = args.task, this.action = args.action;
2700
2891
  }
2701
2892
  }
2702
2893
  const disabledReasonDetail = {
2703
- "role-required": (r) => `requires one of [${r.required.join(", ")}]; actor has [${r.actorRoles.join(", ") || "(none)"}]`,
2704
- "not-assigned": (r) => `actor "${r.actorId}" is not in the task assignees`,
2705
- "permission-denied": (r) => `actor lacks "${r.permission}" permission on the workflow instance`,
2706
2894
  "filter-failed": (r) => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
2707
2895
  "task-not-active": (r) => `task status is "${r.status}"`,
2708
- "stage-terminal": (r) => `stage "${r.stageId}" is terminal`,
2896
+ "stage-terminal": (r) => `stage "${r.stage}" is terminal`,
2709
2897
  "instance-completed": (r) => `instance completed at ${r.completedAt}`,
2710
2898
  "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
2711
2899
  };
2712
- function formatDisabledReason(taskId, action, reason) {
2900
+ function formatDisabledReason(task, action, reason) {
2713
2901
  const detail = disabledReasonDetail[reason.kind](reason);
2714
- return `Action "${taskId}:${action}" is not allowed: ${detail}`;
2902
+ return `Action "${task}:${action}" is not allowed: ${detail}`;
2715
2903
  }
2716
2904
  function bareIdFromSpawnRef(uri) {
2717
2905
  if (!uri.includes(":")) return [uri];
@@ -2744,6 +2932,15 @@ async function cascade(client, instanceId, actor, clientForGdr, clock, overlay)
2744
2932
  );
2745
2933
  return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
2746
2934
  }
2935
+ async function abortAndPropagate(args) {
2936
+ const { client, instanceId, reason, actor, clientForGdr, clock } = args, result = await abortInstance({
2937
+ client,
2938
+ instanceId,
2939
+ ...reason !== void 0 ? { reason } : {},
2940
+ options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
2941
+ });
2942
+ return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), result.fired;
2943
+ }
2747
2944
  function buildClientForGdr(defaultClient, resolver) {
2748
2945
  return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
2749
2946
  }
@@ -2760,36 +2957,109 @@ function intersectsTags(docTags, engineTags) {
2760
2957
  function toEffectsContextEntries(ctx) {
2761
2958
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2762
2959
  }
2763
- function assertActionAllowed(evaluation, taskId, action) {
2764
- const actionEval = evaluation.currentStage.tasks.find((t) => t.task.id === taskId)?.actions.find((a) => a.action.id === action);
2960
+ function assertActionAllowed(evaluation, task, action) {
2961
+ const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
2765
2962
  if (actionEval?.allowed === !1 && actionEval.disabledReason)
2766
- throw new ActionDisabledError({ taskId, action, reason: actionEval.disabledReason });
2963
+ throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
2767
2964
  }
2768
2965
  async function applyAction(args) {
2769
- const { client, instance, taskId, action, params, actor, clock } = args, options = { actor, ...clock !== void 0 ? { clock } : {} };
2770
- return findOpenStageEntry(instance)?.tasks.find((t) => t.id === taskId)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, taskId, options }), (await fireAction({
2966
+ const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = {
2967
+ actor,
2968
+ ...grants !== void 0 ? { grants } : {},
2969
+ ...clock !== void 0 ? { clock } : {},
2970
+ ...clientForGdr !== void 0 ? { clientForGdr } : {}
2971
+ };
2972
+ return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
2771
2973
  client,
2772
2974
  instanceId: instance._id,
2773
- taskId,
2975
+ task,
2774
2976
  action,
2775
2977
  ...params !== void 0 ? { params } : {},
2776
2978
  options
2777
2979
  })).ranOps;
2778
2980
  }
2981
+ async function deleteDefinition(args) {
2982
+ const { client, tags, definition, version, cascade: cascade2, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, versions = await loadDefinitionVersions(client, definition, tags);
2983
+ if (versions.length === 0)
2984
+ throw new Error(`No deployed definition for workflow ${definition}`);
2985
+ const targets = version === void 0 ? versions : versions.filter((d) => d.version === version);
2986
+ if (targets.length === 0)
2987
+ throw new Error(`Workflow definition ${definition} v${version} not deployed`);
2988
+ const lastVersionGoes = targets.length === versions.length;
2989
+ await assertNoSpawnReferrers(client, tags, definition, targets, lastVersionGoes);
2990
+ const instanceIds = await nonTerminalInstanceIds(client, tags, definition, version);
2991
+ if (instanceIds.length > 0 && cascade2 !== !0)
2992
+ throw new Error(
2993
+ `Cannot delete ${definition}: ${instanceIds.length} non-terminal instance(s) exist (${previewIds(instanceIds)}). Pass cascade to abort them first \u2014 instances are aborted in place, never deleted.`
2994
+ );
2995
+ const abortedInstanceIds = await abortInstances({
2996
+ client,
2997
+ instanceIds,
2998
+ reason,
2999
+ actor,
3000
+ clientForGdr,
3001
+ clock
3002
+ }), tx = client.transaction();
3003
+ for (const target of targets) tx.delete(target._id);
3004
+ await tx.commit();
3005
+ const deletedGuardCount = lastVersionGoes ? await deleteOrphanedDefinitionGuards({
3006
+ client,
3007
+ clientForGdr,
3008
+ workflowResource: args.workflowResource,
3009
+ definition,
3010
+ definitions: versions,
3011
+ tags
3012
+ }) : 0;
3013
+ return {
3014
+ name: definition,
3015
+ deletedVersions: targets.map((d) => d.version),
3016
+ abortedInstanceIds,
3017
+ deletedGuardCount
3018
+ };
3019
+ }
3020
+ async function assertNoSpawnReferrers(client, tags, definition, targets, lastVersionGoes) {
3021
+ const deployed = await client.fetch(
3022
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}]`,
3023
+ { engineTags: tags }
3024
+ ), targetIds = new Set(targets.map((d) => d._id)), targetVersions = new Set(targets.map((d) => d.version)), referrers = deployed.filter((d) => !targetIds.has(d._id)).filter(
3025
+ (d) => refsOf(d).some(
3026
+ (ref) => ref.name === definition && (typeof ref.version == "number" ? targetVersions.has(ref.version) : lastVersionGoes)
3027
+ )
3028
+ );
3029
+ if (referrers.length > 0) {
3030
+ const names = referrers.map((d) => `${d.name} v${d.version}`).join(", ");
3031
+ throw new Error(
3032
+ `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`
3033
+ );
3034
+ }
3035
+ }
3036
+ async function nonTerminalInstanceIds(client, tags, definition, version) {
3037
+ const versionFilter = version === void 0 ? "" : " && pinnedVersion == $version";
3038
+ return (await client.fetch(
3039
+ `*[_type == "${WORKFLOW_INSTANCE_TYPE}" && definition == $definition && ${tagScopeFilter()} && !defined(completedAt)${versionFilter}] | order(_id asc){_id}`,
3040
+ { definition, engineTags: tags, ...version !== void 0 ? { version } : {} }
3041
+ )).map((doc) => doc._id);
3042
+ }
3043
+ async function abortInstances(args) {
3044
+ const { client, instanceIds, reason, actor, clientForGdr, clock } = args, aborted = [];
3045
+ for (const instanceId of instanceIds)
3046
+ await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock }) && aborted.push(instanceId);
3047
+ return aborted;
3048
+ }
3049
+ function previewIds(ids) {
3050
+ const head = ids.slice(0, 3).join(", ");
3051
+ return ids.length > 3 ? `${head}, \u2026` : head;
3052
+ }
2779
3053
  const workflow = {
2780
3054
  /**
2781
- * Persist a workflow definition to the lake. Stored as a
2782
- * `workflow.definition` document with id
2783
- * `workflow.definition.<workflowId>.v<version>`. Subsequent versions
2784
- * are stored alongside earlier ones `startInstance` picks the
2785
- * highest version by default.
2786
- */
2787
- /**
2788
- * Deploy a set of workflow definitions as one call. The SDK figures
2789
- * out the dependency order itself (children before parents that spawn
2790
- * them via `task.spawns.definitionRef`), idempotently writes any
2791
- * definition whose content has changed, and reports a per-definition
2792
- * outcome (`created` / `updated` / `unchanged`).
3055
+ * Deploy a set of workflow definitions as one call. Each lands as a
3056
+ * {@link WORKFLOW_DEFINITION_TYPE} document; subsequent versions are
3057
+ * stored alongside earlier ones — `startInstance` picks the highest
3058
+ * version by default. The SDK figures out the dependency order itself
3059
+ * (children before parents that spawn them via
3060
+ * `subworkflows.definition`), idempotently writes any definition
3061
+ * whose content has changed, and reports a per-definition outcome
3062
+ * (`created` / `updated` / `unchanged`).
2793
3063
  *
2794
3064
  * Refs may point inside the batch OR at already-deployed definitions
2795
3065
  * in the lake — both are valid. A ref pointing at neither errors with
@@ -2805,14 +3075,14 @@ const workflow = {
2805
3075
  const ordered = await sortByDependencies(client, definitions, tags, workflowResource), results = [], prefix = canonicalTag(tags), tx = client.transaction();
2806
3076
  let hasWrites = !1;
2807
3077
  for (const def of ordered) {
2808
- const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
3078
+ const id = definitionDocId(workflowResource, prefix, def.name, def.version), expected = {
2809
3079
  ...def,
2810
3080
  _id: id,
2811
3081
  _type: schema.WORKFLOW_DEFINITION_TYPE,
2812
3082
  tags
2813
3083
  }, existing = await client.getDocument(id);
2814
3084
  if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
2815
- results.push({ workflowId: def.workflowId, version: def.version, status: "unchanged" });
3085
+ results.push({ definition: def.name, version: def.version, status: "unchanged" });
2816
3086
  continue;
2817
3087
  }
2818
3088
  if (existing) {
@@ -2823,13 +3093,21 @@ const workflow = {
2823
3093
  } else
2824
3094
  tx.create(expected);
2825
3095
  hasWrites = !0, results.push({
2826
- workflowId: def.workflowId,
3096
+ definition: def.name,
2827
3097
  version: def.version,
2828
3098
  status: existing ? "updated" : "created"
2829
3099
  });
2830
3100
  }
2831
3101
  return hasWrites && await tx.commit(), { results };
2832
3102
  },
3103
+ /**
3104
+ * Remove a deployed workflow definition (all versions, or one via
3105
+ * `version`). Refuses while non-terminal instances exist unless
3106
+ * `cascade` aborts them first — instances are never deleted, only
3107
+ * aborted in place; see {@link deleteDefinitionInternal} for the
3108
+ * full contract (spawn-referrer check, guard-doc housekeeping).
3109
+ */
3110
+ deleteDefinition: async (args) => deleteDefinition(args),
2833
3111
  /**
2834
3112
  * Spawn a new workflow instance from a deployed definition.
2835
3113
  *
@@ -2843,20 +3121,20 @@ const workflow = {
2843
3121
  client,
2844
3122
  tags,
2845
3123
  workflowResource,
2846
- workflowId,
3124
+ definition: definitionName,
2847
3125
  version,
2848
3126
  initialState,
2849
3127
  ancestors,
2850
3128
  effectsContext,
2851
3129
  instanceId,
2852
3130
  perspective
2853
- } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, workflowId, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2854
- if (definition.stages.find((s) => s.id === definition.initialStageId) === void 0)
3131
+ } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
3132
+ if (definition.stages.find((s) => s.name === definition.initialStage) === void 0)
2855
3133
  throw new Error(
2856
- `Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
3134
+ `Initial stage "${definition.initialStage}" missing in ${definitionName} v${definition.version}`
2857
3135
  );
2858
3136
  const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2859
- slotDefs: definition.state ?? [],
3137
+ entryDefs: definition.state ?? [],
2860
3138
  initialState: initialState ?? [],
2861
3139
  ctx: {
2862
3140
  client,
@@ -2872,14 +3150,14 @@ const workflow = {
2872
3150
  now,
2873
3151
  tags,
2874
3152
  workflowResource,
2875
- workflowId: definition.workflowId,
3153
+ definitionName: definition.name,
2876
3154
  pinnedVersion: definition.version,
2877
3155
  definition,
2878
3156
  state: resolvedState,
2879
3157
  effectsContext: effectsContextEntries,
2880
3158
  ancestors: ancestors ?? [],
2881
3159
  perspective: effectivePerspective,
2882
- initialStageId: definition.initialStageId,
3160
+ initialStage: definition.initialStage,
2883
3161
  actor
2884
3162
  });
2885
3163
  return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
@@ -2899,13 +3177,13 @@ const workflow = {
2899
3177
  client,
2900
3178
  tags,
2901
3179
  instanceId,
2902
- taskId,
3180
+ task,
2903
3181
  action,
2904
3182
  params,
2905
3183
  idempotent,
2906
3184
  resourceClients
2907
3185
  } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags);
2908
- if (findOpenStageEntry(before)?.tasks.find((t) => t.id === taskId) === void 0 && idempotent === !0)
3186
+ if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
2909
3187
  return { instance: before, cascaded: 0, fired: !1 };
2910
3188
  const evaluation = await evaluateInstance({
2911
3189
  client,
@@ -2915,14 +3193,16 @@ const workflow = {
2915
3193
  clock,
2916
3194
  ...resourceClients !== void 0 ? { resourceClients } : {}
2917
3195
  });
2918
- assertActionAllowed(evaluation, taskId, action);
3196
+ assertActionAllowed(evaluation, task, action);
2919
3197
  const ranOps = await applyAction({
2920
3198
  client,
2921
3199
  instance: before,
2922
- taskId,
3200
+ task,
2923
3201
  action,
2924
3202
  actor,
3203
+ ...access.grants !== void 0 ? { grants: access.grants } : {},
2925
3204
  clock,
3205
+ clientForGdr,
2926
3206
  ...params !== void 0 ? { params } : {}
2927
3207
  }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2928
3208
  return {
@@ -2950,7 +3230,7 @@ const workflow = {
2950
3230
  ...detail !== void 0 ? { detail } : {},
2951
3231
  ...error !== void 0 ? { error } : {},
2952
3232
  ...durationMs !== void 0 ? { durationMs } : {},
2953
- options: { ...actor !== void 0 ? { actor } : {}, clock }
3233
+ options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
2954
3234
  });
2955
3235
  const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2956
3236
  return {
@@ -2969,8 +3249,8 @@ const workflow = {
2969
3249
  * affected instances and the engine re-evaluates.
2970
3250
  */
2971
3251
  tick: async (args) => {
2972
- const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2973
- await reload(client, instanceId, tags);
3252
+ const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, current = await reload(client, instanceId, tags), guards = await verdictGuardsForInstance(client, current._id);
3253
+ await assertInstanceWriteAllowed({ instance: current, guards, identity: actor.id });
2974
3254
  const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2975
3255
  return {
2976
3256
  instance: await reload(client, instanceId, tags),
@@ -2979,19 +3259,19 @@ const workflow = {
2979
3259
  };
2980
3260
  },
2981
3261
  /**
2982
- * Admin override — force the instance into `targetStageId` regardless
3262
+ * Admin override — force the instance into `targetStage` regardless
2983
3263
  * of filters or declared transitions. ACL gating should be enforced
2984
3264
  * upstream; this verb performs the mechanical move.
2985
3265
  */
2986
3266
  setStage: async (args) => {
2987
- const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3267
+ const { client, tags, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2988
3268
  await reload(client, instanceId, tags);
2989
3269
  const result = await setStage({
2990
3270
  client,
2991
3271
  instanceId,
2992
- targetStageId,
3272
+ targetStage,
2993
3273
  ...reason !== void 0 ? { reason } : {},
2994
- options: { ...actor !== void 0 ? { actor } : {}, clock }
3274
+ options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
2995
3275
  }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
2996
3276
  return {
2997
3277
  instance: await reload(client, instanceId, tags),
@@ -2999,6 +3279,22 @@ const workflow = {
2999
3279
  fired: result.fired
3000
3280
  };
3001
3281
  },
3282
+ /**
3283
+ * Admin override — hard-stop an in-flight instance where it stands.
3284
+ * No stage move, no transition effects, pending effects cancelled;
3285
+ * see {@link abortAndPropagate} for the abort + ancestor-propagation
3286
+ * contract (propagated, not cascaded — the instance is terminal).
3287
+ */
3288
+ abortInstance: async (args) => {
3289
+ const { client, tags, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3290
+ await reload(client, instanceId, tags);
3291
+ const fired = await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock });
3292
+ return {
3293
+ instance: await reload(client, instanceId, tags),
3294
+ cascaded: 0,
3295
+ fired
3296
+ };
3297
+ },
3002
3298
  /**
3003
3299
  * Fetch a workflow instance by id, scoped to the engine's tags.
3004
3300
  * Throws when the instance doesn't exist or isn't visible to this
@@ -3018,6 +3314,20 @@ const workflow = {
3018
3314
  instance
3019
3315
  });
3020
3316
  },
3317
+ guardsForDefinition: async (args) => {
3318
+ const { client, tags, workflowResource, definition, resourceClients } = args;
3319
+ validateTags(tags);
3320
+ const definitions = await loadDefinitionVersions(client, definition, tags);
3321
+ if (definitions.length === 0)
3322
+ throw new Error(`No deployed definition for workflow ${definition}`);
3323
+ return guardsForDefinition({
3324
+ client,
3325
+ clientForGdr: buildClientForGdr(client, resourceClients),
3326
+ workflowResource,
3327
+ definition,
3328
+ definitions
3329
+ });
3330
+ },
3021
3331
  /**
3022
3332
  * Run a caller-supplied GROQ query with the engine's tags bound as
3023
3333
  * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
@@ -3040,12 +3350,13 @@ const workflow = {
3040
3350
  * Snapshot-aware GROQ — runs against the same in-memory view that
3041
3351
  * filters see for a given instance.
3042
3352
  *
3043
- * Hydrates the instance's snapshot (instance + subject + ancestors
3044
- * + every doc declared by a `doc.ref` / `doc.refs` slot in scope),
3045
- * then evaluates the supplied GROQ in groq-js against that dataset.
3046
- * Reserved params are auto-bound: `$self`, `$subject`, `$parent`,
3047
- * `$ancestors`, `$stage`, `$now` all in GDR URI form to match the
3048
- * snapshot's keying.
3353
+ * Hydrates the instance's snapshot (instance + ancestors + every doc
3354
+ * declared by a `doc.ref` / `doc.refs` entry in scope), then
3355
+ * evaluates the supplied GROQ in groq-js against that dataset. The
3356
+ * rendered scope is auto-bound: `$self`, `$state`, `$parent`,
3357
+ * `$ancestors`, `$stage`, `$now`, `$effects`, `$tasks`,
3358
+ * `$allTasksDone`, `$anyTaskFailed` — ids in GDR URI form to match
3359
+ * the snapshot's keying.
3049
3360
  *
3050
3361
  * Use when an external consumer (a UI, a debug pane, a test) wants
3051
3362
  * to ask "what does the engine see for this workflow right now?"
@@ -3054,7 +3365,7 @@ const workflow = {
3054
3365
  queryInScope: async (args) => {
3055
3366
  const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
3056
3367
  validateTags(tags);
3057
- const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams(instance, clock()), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
3368
+ const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams({ instance, now: clock(), snapshot }), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
3058
3369
  return await (await groqJs.evaluate(tree, {
3059
3370
  dataset: snapshot.docs,
3060
3371
  params: { ...reserved, ...params }
@@ -3070,7 +3381,7 @@ const workflow = {
3070
3381
  * filters on claim presence; `names` restricts to specific effect
3071
3382
  * names. Both filters compose (AND).
3072
3383
  */
3073
- 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))),
3384
+ 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.name))),
3074
3385
  /**
3075
3386
  * Project the instance from a given actor's perspective. Returns a
3076
3387
  * `WorkflowEvaluation` with per-action verdicts (`allowed` + a
@@ -3080,22 +3391,46 @@ const workflow = {
3080
3391
  * `fireAction` to gate writes via the same logic.
3081
3392
  */
3082
3393
  evaluate: async (args) => evaluateInstance(args),
3394
+ /**
3395
+ * Diagnose why an instance is or isn't progressing. Projects the
3396
+ * instance (the same read as `evaluate`) and classifies it — terminal,
3397
+ * `progressing`, `waiting` (an action is available — healthy), or `stuck`
3398
+ * with a structured cause — returning that verdict as a
3399
+ * {@link DiagnoseResult} alongside the evaluation it came from, so a
3400
+ * consumer can render the supporting evidence without a second projection.
3401
+ * Pure read.
3402
+ */
3403
+ diagnose: async (args) => {
3404
+ const evaluation = await evaluateInstance(args);
3405
+ return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
3406
+ },
3407
+ /**
3408
+ * List the actions an actor could fire on an instance's current stage,
3409
+ * each flagged `allowed` (with a structured `disabledReason` when not).
3410
+ * Projects the instance from the actor's perspective and flattens its
3411
+ * tasks' actions. Returns the evaluation alongside the actions so a
3412
+ * consumer can read the instance/stage context. Pure read.
3413
+ */
3414
+ availableActions: async (args) => {
3415
+ const evaluation = await evaluateInstance(args);
3416
+ return { evaluation, actions: availableActions(evaluation.currentStage.tasks) };
3417
+ },
3083
3418
  /**
3084
3419
  * Materialised spawned children of a parent instance.
3085
3420
  *
3086
- * Walks `history` for `workflow.history.spawned` events — the durable
3421
+ * Walks `history` for `spawned` events — the durable
3087
3422
  * record. (The per-task `spawnedInstances` field on the active stage
3088
3423
  * only exists while that stage is current; history survives.) Strips
3089
3424
  * the GDR URI on each `instanceRef` to a bare `_id`, fetches the
3090
3425
  * instances, drops any that aren't visible to this engine's tags, and
3091
3426
  * returns them sorted by `startedAt` ascending.
3092
3427
  *
3093
- * Pass `taskId` to restrict to a single spawning task on the parent.
3428
+ * Pass `task` to restrict to a single spawning task on the parent.
3094
3429
  */
3095
3430
  children: async (args) => {
3096
- const { client, tags, instanceId, taskId } = args;
3431
+ const { client, tags, instanceId, task } = args;
3097
3432
  validateTags(tags);
3098
- const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "workflow.history.spawned", ids = parent.history.filter(isSpawned).filter((h) => taskId === void 0 || h.taskId === taskId).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
3433
+ const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "spawned", ids = parent.history.filter(isSpawned).filter((h) => task === void 0 || h.task === task).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
3099
3434
  return ids.length === 0 ? [] : client.fetch(
3100
3435
  `*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
3101
3436
  { ids, engineTags: tags }
@@ -3116,11 +3451,11 @@ async function verifyDeployedDefinitionsInternal(args) {
3116
3451
  const { client, tags, effectHandlers, missingHandler, logger } = args;
3117
3452
  validateTags(tags);
3118
3453
  const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
3119
- `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(workflowId asc, version asc)`,
3454
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(name asc, version asc)`,
3120
3455
  { engineTags: tags }
3121
3456
  ), seen = [], missingByName = /* @__PURE__ */ new Map();
3122
3457
  for (const def of definitions)
3123
- seen.push({ workflowId: def.workflowId, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
3458
+ seen.push({ name: def.name, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
3124
3459
  if (effectHandlers[name] !== void 0) return;
3125
3460
  const key = `${name}@@${def._id}`;
3126
3461
  let bucket = missingByName.get(key);
@@ -3151,17 +3486,16 @@ async function verifyDeployedDefinitionsInternal(args) {
3151
3486
  }
3152
3487
  function walkEffectNames(def, visit) {
3153
3488
  const visitEffects = (effects, location) => {
3154
- for (const e of effects ?? []) visit(e.id, location);
3489
+ for (const e of effects ?? []) visit(e.name, location);
3155
3490
  };
3156
3491
  for (const stage of def.stages ?? []) {
3157
- visitEffects(stage.onEnter, `stage[${stage.id}].onEnter`), visitEffects(stage.onExit, `stage[${stage.id}].onExit`);
3158
3492
  for (const t of stage.tasks ?? []) {
3159
- visitEffects(t.effects, `stage[${stage.id}].task[${t.id}].effects`);
3493
+ visitEffects(t.effects, `stage[${stage.name}].task[${t.name}].effects`);
3160
3494
  for (const a of t.actions ?? [])
3161
- visitEffects(a.effects, `stage[${stage.id}].task[${t.id}].action[${a.id}].effects`);
3495
+ visitEffects(a.effects, `stage[${stage.name}].task[${t.name}].action[${a.name}].effects`);
3162
3496
  }
3163
3497
  for (const tr of stage.transitions ?? [])
3164
- visitEffects(tr.effects, `stage[${stage.id}].transition[${tr.to}].effects`);
3498
+ visitEffects(tr.effects, `stage[${stage.name}].transition[${tr.name}].effects`);
3165
3499
  }
3166
3500
  }
3167
3501
  async function drainEffectsInternal(args) {
@@ -3185,7 +3519,7 @@ async function drainEffectsInternal(args) {
3185
3519
  (e) => e.claim === void 0 && !skippedKeys.has(e._key)
3186
3520
  );
3187
3521
  if (candidate === void 0) break;
3188
- const handler = effectHandlers[candidate.effectId];
3522
+ const handler = effectHandlers[candidate.name];
3189
3523
  if (handler === void 0) {
3190
3524
  await assertSkippableOrThrow(missingHandler, candidate, instanceId, log), skipped.push(candidate), skippedKeys.add(candidate._key);
3191
3525
  continue;
@@ -3223,16 +3557,16 @@ async function reportEffectOutcome(args) {
3223
3557
  async function assertSkippableOrThrow(missingHandler, candidate, instanceId, log) {
3224
3558
  if (await applyMissingHandler(
3225
3559
  missingHandler,
3226
- { phase: "drain", name: candidate.effectId, effectKey: candidate._key, instanceId },
3560
+ { phase: "drain", name: candidate.name, effectKey: candidate._key, instanceId },
3227
3561
  log
3228
3562
  ) === "fail")
3229
3563
  throw new Error(
3230
- `drainEffects: no handler registered for "${candidate.effectId}" (effectKey=${candidate._key}, instanceId=${instanceId})`
3564
+ `drainEffects: no handler registered for "${candidate.name}" (effectKey=${candidate._key}, instanceId=${instanceId})`
3231
3565
  );
3232
3566
  }
3233
3567
  async function claimPendingEffect(client, instance, effectKey, drainerActor) {
3234
3568
  const claim = {
3235
- _type: "workflow.pendingEffect.claim",
3569
+ _type: "pendingEffect.claim",
3236
3570
  claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
3237
3571
  claimedBy: drainerActor
3238
3572
  };
@@ -3249,7 +3583,7 @@ async function dispatchEffect(handler, candidate, ctx) {
3249
3583
  client: ctx.client,
3250
3584
  instanceId: ctx.instanceId,
3251
3585
  effectKey: candidate._key,
3252
- log: (message, extra) => ctx.logger(`effect.${candidate.effectId}`).info(message, extra)
3586
+ log: (message, extra) => ctx.logger(`effect.${candidate.name}`).info(message, extra)
3253
3587
  });
3254
3588
  return result?.outputs !== void 0 ? { outputs: result.outputs } : {};
3255
3589
  } catch (err) {
@@ -3269,7 +3603,7 @@ async function applyMissingHandler(policy, info, log) {
3269
3603
  }
3270
3604
  function createInstanceSession(args) {
3271
3605
  const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
3272
- let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), committing = !1, buffered;
3606
+ let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), heldGuards = [], committing = !1, buffered;
3273
3607
  const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
3274
3608
  const next = /* @__PURE__ */ new Map();
3275
3609
  for (const ld of docs) {
@@ -3284,13 +3618,14 @@ function createInstanceSession(args) {
3284
3618
  }, access = () => resolveAccess(client, {
3285
3619
  ...args.access !== void 0 ? { override: args.access } : {},
3286
3620
  ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
3287
- }), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held) => {
3621
+ }), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held, guards) => {
3288
3622
  const { actor, grants } = await access();
3289
3623
  return evaluateFromSnapshot({
3290
3624
  instance,
3291
3625
  definition: parseDefinitionSnapshot(instance),
3292
3626
  actor,
3293
3627
  snapshot: snapshotFrom(held),
3628
+ guards,
3294
3629
  ...clock !== void 0 ? { now: clock() } : {},
3295
3630
  ...grants !== void 0 ? { grants } : {}
3296
3631
  });
@@ -3318,24 +3653,30 @@ function createInstanceSession(args) {
3318
3653
  }
3319
3654
  applyUpdate(docs);
3320
3655
  },
3656
+ updateGuards(guards) {
3657
+ heldGuards = guards.map((guard) => structuredClone(guard));
3658
+ },
3321
3659
  evaluate() {
3322
- return evaluateWith(overlay);
3660
+ return evaluateWith(overlay, heldGuards);
3323
3661
  },
3324
3662
  tick() {
3325
3663
  return commit(async (actor, held) => {
3664
+ await assertInstanceWriteAllowed({ instance, guards: heldGuards, identity: actor.id });
3326
3665
  const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3327
3666
  return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
3328
3667
  });
3329
3668
  },
3330
- fireAction({ taskId, action, params }) {
3669
+ fireAction({ task, action, params }) {
3331
3670
  return commit(async (actor, held) => {
3332
- assertActionAllowed(await evaluateWith(held), taskId, action);
3333
- const ranOps = await applyAction({
3671
+ assertActionAllowed(await evaluateWith(held, heldGuards), task, action);
3672
+ const { grants } = await access(), ranOps = await applyAction({
3334
3673
  client,
3335
3674
  instance,
3336
- taskId,
3675
+ task,
3337
3676
  action,
3338
3677
  actor,
3678
+ clientForGdr,
3679
+ ...grants !== void 0 ? { grants } : {},
3339
3680
  ...clock !== void 0 ? { clock } : {},
3340
3681
  ...params !== void 0 ? { params } : {}
3341
3682
  }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
@@ -3346,7 +3687,7 @@ function createInstanceSession(args) {
3346
3687
  }
3347
3688
  function asHeldInstance(doc) {
3348
3689
  const candidate = doc;
3349
- if (candidate._type !== "workflow.instance" || typeof candidate.currentStageId != "string" || !Array.isArray(candidate.stages))
3690
+ if (candidate._type !== WORKFLOW_INSTANCE_TYPE || typeof candidate.currentStage != "string" || !Array.isArray(candidate.stages))
3350
3691
  throw new Error(`update: self-doc ${doc._id} is not a valid workflow.instance`);
3351
3692
  return doc;
3352
3693
  }
@@ -3390,7 +3731,11 @@ function createEngine(args) {
3390
3731
  completeEffect: (rest) => workflow.completeEffect(bind(rest)),
3391
3732
  tick: (rest) => workflow.tick(bind(rest)),
3392
3733
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
3734
+ diagnose: (rest) => workflow.diagnose(bind(rest)),
3735
+ availableActions: (rest) => workflow.availableActions(bind(rest)),
3393
3736
  setStage: (rest) => workflow.setStage(bind(rest)),
3737
+ abortInstance: (rest) => workflow.abortInstance(bind(rest)),
3738
+ deleteDefinition: (rest) => workflow.deleteDefinition(bind(rest)),
3394
3739
  getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
3395
3740
  subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
3396
3741
  instance: (instanceDoc, opts) => createInstanceSession({
@@ -3409,12 +3754,19 @@ function createEngine(args) {
3409
3754
  instanceId,
3410
3755
  ...resourceClients !== void 0 ? { resourceClients } : {}
3411
3756
  }),
3412
- children: ({ instanceId, taskId }) => workflow.children({
3757
+ guardsForDefinition: ({ definition }) => workflow.guardsForDefinition({
3758
+ client,
3759
+ tags,
3760
+ workflowResource,
3761
+ definition,
3762
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3763
+ }),
3764
+ children: ({ instanceId, task }) => workflow.children({
3413
3765
  client,
3414
3766
  tags,
3415
3767
  workflowResource,
3416
3768
  instanceId,
3417
- ...taskId !== void 0 ? { taskId } : {}
3769
+ ...task !== void 0 ? { task } : {}
3418
3770
  }),
3419
3771
  query: ({ groq, params }) => workflow.query({
3420
3772
  client,
@@ -3461,112 +3813,158 @@ function createEngine(args) {
3461
3813
  })
3462
3814
  };
3463
3815
  }
3816
+ function docIdFor(def, target) {
3817
+ return definitionDocId(target.workflowResource, canonicalTag(target.tags), def.name, def.version);
3818
+ }
3819
+ function diffStatus(existing, expected) {
3820
+ return existing === void 0 ? "create" : isDefinitionUnchanged(existing, expected) ? "unchanged" : "update";
3821
+ }
3822
+ function diffEntry(def, existingRaw, target) {
3823
+ const docId = docIdFor(def, target), expected = {
3824
+ ...def,
3825
+ _id: docId,
3826
+ _type: schema.WORKFLOW_DEFINITION_TYPE,
3827
+ tags: target.tags
3828
+ }, status = diffStatus(existingRaw, expected), existing = existingRaw ? stripSystemFields(existingRaw) : void 0;
3829
+ return {
3830
+ name: def.name,
3831
+ version: def.version,
3832
+ status,
3833
+ docId,
3834
+ expected,
3835
+ ...existing !== void 0 ? { existing } : {}
3836
+ };
3837
+ }
3838
+ async function computeDiffEntries(client, defs, target) {
3839
+ const entries = [];
3840
+ for (const def of defs) {
3841
+ const existingRaw = await client.getDocument(docIdFor(def, target)) ?? void 0;
3842
+ entries.push(diffEntry(def, existingRaw, target));
3843
+ }
3844
+ return entries;
3845
+ }
3464
3846
  const HISTORY_DISPLAY = {
3465
- "workflow.history.stageEntered": {
3847
+ stageEntered: {
3466
3848
  title: "Stage entered",
3467
3849
  description: "The instance crossed into a new stage."
3468
3850
  },
3469
- "workflow.history.stageExited": {
3851
+ stageExited: {
3470
3852
  title: "Stage exited",
3471
3853
  description: "The instance left a stage on the way to the next one."
3472
3854
  },
3473
- "workflow.history.taskActivated": {
3855
+ taskActivated: {
3474
3856
  title: "Task activated",
3475
3857
  description: "A task flipped from `pending` to `active` and its onEnter effects queued."
3476
3858
  },
3477
- "workflow.history.taskStatusChanged": {
3859
+ taskStatusChanged: {
3478
3860
  title: "Task status changed",
3479
3861
  description: "An action or op flipped a task's status (active \u2192 done / skipped / failed)."
3480
3862
  },
3481
- "workflow.history.actionFired": {
3863
+ actionFired: {
3482
3864
  title: "Action fired",
3483
3865
  description: "An action was invoked against a task \u2014 ops ran, status may have flipped, effects queued."
3484
3866
  },
3485
- "workflow.history.transitionFired": {
3867
+ transitionFired: {
3486
3868
  title: "Transition fired",
3487
3869
  description: "A transition committed and the instance moved to a new stage."
3488
3870
  },
3489
- "workflow.history.effectQueued": {
3871
+ effectQueued: {
3490
3872
  title: "Effect queued",
3491
3873
  description: "A pending effect was added to the queue, waiting for a drainer to dispatch it."
3492
3874
  },
3493
- "workflow.history.effectCompleted": {
3875
+ effectCompleted: {
3494
3876
  title: "Effect completed",
3495
3877
  description: "A drainer ran an effect handler to completion (done or failed) and merged its outputs into effectsContext."
3496
3878
  },
3497
- "workflow.history.spawned": {
3879
+ spawned: {
3498
3880
  title: "Spawned child",
3499
- description: "A `task.spawns` declaration spawned a child workflow instance."
3881
+ description: "A task's `subworkflows` declaration spawned a child workflow instance."
3500
3882
  },
3501
- "workflow.history.opApplied": {
3883
+ aborted: {
3884
+ title: "Instance aborted",
3885
+ description: "An admin hard-stopped the instance \u2014 pending effects cancelled, stage guards lifted, abortedAt + completedAt stamped."
3886
+ },
3887
+ opApplied: {
3502
3888
  title: "Op applied",
3503
3889
  description: "An inline state-mutation op (state.set / state.append / status.set / etc.) ran during an action commit."
3504
3890
  }
3505
3891
  }, STATE_SLOT_DISPLAY = {
3506
- "workflow.state.doc.ref": {
3892
+ "doc.ref": {
3507
3893
  title: "Document reference",
3508
3894
  description: "Single GDR pointer at a document in this or another resource."
3509
3895
  },
3510
- "workflow.state.doc.refs": {
3896
+ "doc.refs": {
3511
3897
  title: "Document references",
3512
3898
  description: "Ordered list of GDR pointers \u2014 multi-doc selection."
3513
3899
  },
3514
- "workflow.state.query": {
3900
+ "release.ref": {
3901
+ title: "Content Release reference",
3902
+ description: "GDR pointer at a Content Release system doc, carrying the release name the engine derives the instance's read perspective from."
3903
+ },
3904
+ query: {
3515
3905
  title: "Query result",
3516
- description: "Snapshot of a GROQ projection captured at slot-resolve time; resolvedAt is stamped on the value."
3906
+ description: "Snapshot of a GROQ projection captured at entry-resolve time; resolvedAt is stamped on the value."
3517
3907
  },
3518
- "workflow.state.value.string": {
3908
+ "value.string": {
3519
3909
  title: "String value",
3520
- description: "Free-form string slot."
3910
+ description: "Free-form string entry."
3521
3911
  },
3522
- "workflow.state.value.url": {
3912
+ "value.url": {
3523
3913
  title: "URL value",
3524
- description: "Validated URL slot."
3914
+ description: "Validated URL entry."
3525
3915
  },
3526
- "workflow.state.value.number": {
3916
+ "value.number": {
3527
3917
  title: "Number value",
3528
- description: "Numeric slot."
3918
+ description: "Numeric entry."
3529
3919
  },
3530
- "workflow.state.value.boolean": {
3920
+ "value.boolean": {
3531
3921
  title: "Boolean value",
3532
- description: "True/false slot."
3922
+ description: "True/false entry."
3533
3923
  },
3534
- "workflow.state.value.dateTime": {
3924
+ "value.dateTime": {
3535
3925
  title: "Date / time value",
3536
- description: "ISO-timestamp slot."
3926
+ description: "ISO-timestamp entry."
3537
3927
  },
3538
- "workflow.state.checklist": {
3928
+ "value.actor": {
3929
+ title: "Actor value",
3930
+ description: "A typed (user|ai|system) actor identity."
3931
+ },
3932
+ checklist: {
3539
3933
  title: "Checklist",
3540
3934
  description: "Append-only list of checkable items; ops add/update/remove entries."
3541
3935
  },
3542
- "workflow.state.notes": {
3936
+ notes: {
3543
3937
  title: "Notes log",
3544
3938
  description: "Append-only structured-note log (signoffs, comments, audit rows)."
3545
3939
  },
3546
- "workflow.state.assignees": {
3940
+ assignees: {
3547
3941
  title: "Assignees",
3548
3942
  description: "Ordered list of typed (user|role) assignees."
3549
3943
  }
3550
3944
  }, OP_DISPLAY = {
3551
- "workflow.op.state.set": {
3552
- title: "Set slot",
3553
- description: "Overwrite a state slot's value with a resolved Source."
3945
+ "state.set": {
3946
+ title: "Set state entry",
3947
+ description: "Overwrite a state entry's value with a resolved Source."
3948
+ },
3949
+ "state.unset": {
3950
+ title: "Unset state entry",
3951
+ description: "Reset a state entry to its default (null for scalars, [] for array kinds)."
3554
3952
  },
3555
- "workflow.op.state.append": {
3556
- title: "Append to slot",
3557
- description: "Push a resolved item onto an array-typed slot (notes, checklist, assignees, refs)."
3953
+ "state.append": {
3954
+ title: "Append to state entry",
3955
+ description: "Push a resolved item onto an array-kind entry (notes, checklist, assignees, refs)."
3558
3956
  },
3559
- "workflow.op.state.updateWhere": {
3957
+ "state.updateWhere": {
3560
3958
  title: "Update matching rows",
3561
- description: "Merge fields into rows of an array-typed slot that match the predicate."
3959
+ description: "Merge fields into rows of an array-kind entry that match the predicate."
3562
3960
  },
3563
- "workflow.op.state.removeWhere": {
3961
+ "state.removeWhere": {
3564
3962
  title: "Remove matching rows",
3565
- description: "Drop rows of an array-typed slot that match the predicate."
3963
+ description: "Drop rows of an array-kind entry that match the predicate."
3566
3964
  },
3567
- "workflow.op.status.set": {
3965
+ "status.set": {
3568
3966
  title: "Set task status",
3569
- description: "Flip a task's status explicitly (action.setStatus is the more common path)."
3967
+ description: "Flip a task's status explicitly (the action-level `status:` sugar desugars to this)."
3570
3968
  }
3571
3969
  }, EFFECTS_CONTEXT_DISPLAY = {
3572
3970
  "effectsContext.string": {
@@ -3590,53 +3988,21 @@ const HISTORY_DISPLAY = {
3590
3988
  description: "Stable JSON payload for effect handlers."
3591
3989
  }
3592
3990
  }, AUTHORING_DISPLAY = {
3593
- "workflow.stage": {
3594
- title: "Stage",
3595
- description: "A bucket of tasks the workflow is currently in."
3596
- },
3597
- "workflow.task": {
3598
- title: "Task",
3599
- description: "A unit of work inside a stage; carries actions and may spawn children."
3600
- },
3601
- "workflow.action": {
3602
- title: "Action",
3603
- description: "An invocable transition on a task \u2014 runs ops, queues effects, may flip status."
3604
- },
3605
- "workflow.action.param": {
3606
- title: "Action param",
3607
- description: "Caller-supplied parameter on an action; validated before any commit."
3608
- },
3609
- "workflow.transition": {
3610
- title: "Transition",
3611
- description: "An edge between stages, gated by a filter."
3612
- },
3613
- "workflow.predicate": {
3614
- title: "Predicate",
3615
- description: "Named GROQ filter, reusable across transitions / tasks / actions."
3616
- },
3617
- "workflow.effect": {
3618
- title: "Effect",
3619
- description: "Externally-dispatched side effect; queued on commit, drained by a runtime, completed asynchronously."
3620
- },
3621
- "workflow.spawn": {
3622
- title: "Spawn",
3623
- description: "Declares a task that fans out one or more child workflow instances."
3624
- },
3625
- "workflow.pendingEffect": {
3991
+ pendingEffect: {
3626
3992
  title: "Pending effect",
3627
3993
  description: "An effect that has been queued but not yet dispatched."
3628
3994
  },
3629
- "workflow.pendingEffect.claim": {
3995
+ "pendingEffect.claim": {
3630
3996
  title: "Claim",
3631
3997
  description: "A drainer's lock on a pending effect while it dispatches."
3632
3998
  },
3633
- "workflow.definition": {
3999
+ [schema.WORKFLOW_DEFINITION_TYPE]: {
3634
4000
  title: "Workflow definition",
3635
- description: "An immutable workflow blueprint pinned by `workflowId` + `version`."
4001
+ description: "An immutable workflow blueprint, addressed by `name` + `version`."
3636
4002
  },
3637
- "workflow.instance": {
4003
+ [WORKFLOW_INSTANCE_TYPE]: {
3638
4004
  title: "Workflow instance",
3639
- description: "A running (or finished) workflow against a subject."
4005
+ description: "A running (or finished) workflow against its declared state."
3640
4006
  }
3641
4007
  }, DISPLAY = {
3642
4008
  ...HISTORY_DISPLAY,
@@ -3669,16 +4035,26 @@ exports.OP_DISPLAY = OP_DISPLAY;
3669
4035
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
3670
4036
  exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
3671
4037
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
4038
+ exports.abortReason = abortReason;
4039
+ exports.actionVerdict = actionVerdict;
4040
+ exports.assigneesOf = assigneesOf;
4041
+ exports.availableActions = availableActions;
3672
4042
  exports.buildSnapshot = buildSnapshot;
3673
4043
  exports.canonicalTag = canonicalTag;
3674
4044
  exports.compileGuard = compileGuard;
4045
+ exports.computeDiffEntries = computeDiffEntries;
3675
4046
  exports.contentReleaseName = contentReleaseName;
3676
4047
  exports.createEngine = createEngine;
4048
+ exports.datasetResourceParts = datasetResourceParts;
3677
4049
  exports.defaultLoggerFactory = defaultLoggerFactory;
3678
4050
  exports.denyingGuards = denyingGuards;
3679
4051
  exports.deployStageGuards = deployStageGuards;
4052
+ exports.diagnoseInputFromEvaluation = diagnoseInputFromEvaluation;
4053
+ exports.diagnoseInstance = diagnoseInstance;
4054
+ exports.diffEntry = diffEntry;
3680
4055
  exports.displayDescription = displayDescription;
3681
4056
  exports.displayTitle = displayTitle;
4057
+ exports.effectsContextMap = effectsContextMap;
3682
4058
  exports.evaluateFromSnapshot = evaluateFromSnapshot;
3683
4059
  exports.evaluateMutationGuard = evaluateMutationGuard;
3684
4060
  exports.extractDocumentId = extractDocumentId;
@@ -3686,11 +4062,15 @@ exports.gdrFromResource = gdrFromResource;
3686
4062
  exports.gdrRef = gdrRef;
3687
4063
  exports.gdrUri = gdrUri;
3688
4064
  exports.guardMatches = guardMatches;
4065
+ exports.guardsForDefinition = guardsForDefinition;
3689
4066
  exports.guardsForInstance = guardsForInstance;
3690
4067
  exports.guardsForResource = guardsForResource;
4068
+ exports.instanceGuardQuery = instanceGuardQuery;
3691
4069
  exports.isDefinitionUnchanged = isDefinitionUnchanged;
3692
4070
  exports.isGdr = isGdr;
4071
+ exports.isTerminalStage = isTerminalStage;
3693
4072
  exports.lakeGuardId = lakeGuardId;
4073
+ exports.openStage = openStage;
3694
4074
  exports.parseGdr = parseGdr;
3695
4075
  exports.readsRaw = readsRaw;
3696
4076
  exports.refCanvas = refCanvas;
@@ -3702,10 +4082,12 @@ exports.resourceFromParsed = resourceFromParsed;
3702
4082
  exports.retractStageGuards = retractStageGuards;
3703
4083
  exports.silentLogger = silentLogger;
3704
4084
  exports.stripSystemFields = stripSystemFields;
4085
+ exports.subscriptionDocument = subscriptionDocument;
3705
4086
  exports.subscriptionDocumentsForInstance = subscriptionDocumentsForInstance;
3706
4087
  exports.tagScopeFilter = tagScopeFilter;
3707
4088
  exports.validateDefinition = validateDefinition;
3708
4089
  exports.validateTags = validateTags;
4090
+ exports.verdictGuardsForInstance = verdictGuardsForInstance;
3709
4091
  exports.wallClock = wallClock;
3710
4092
  exports.workflow = workflow;
3711
4093
  //# sourceMappingURL=index.cjs.map