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