@sanity/workflow-engine 0.3.0 → 0.4.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,157 @@ 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
+ );
320
248
  }
321
- function describeValue(value) {
322
- return Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
323
- }
324
- const GUARD_DOC_TYPE = "temp.system.guard";
249
+ const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), GUARD_DOC_TYPE = "temp.system.guard";
325
250
  class MutationGuardDeniedError extends Error {
326
251
  denied;
327
252
  documentId;
@@ -330,9 +255,24 @@ class MutationGuardDeniedError extends Error {
330
255
  const ids = args.denied.map((d) => d.guardId).join(", ");
331
256
  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
257
  }
258
+ /**
259
+ * Build the error straight from the denying guard docs — the one mapping
260
+ * from {@link MutationGuardDoc} to the structured `denied` payload, shared
261
+ * by every enforcement site (engine pre-flight, bench write seam).
262
+ */
263
+ static fromGuards(args) {
264
+ return new MutationGuardDeniedError({
265
+ documentId: args.documentId,
266
+ action: args.action,
267
+ denied: args.guards.map((g) => ({
268
+ guardId: g._id,
269
+ ...g.name !== void 0 ? { name: g.name } : {}
270
+ }))
271
+ });
272
+ }
333
273
  }
334
274
  function lakeGuardId(args) {
335
- return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.stageId}.${args.index}`;
275
+ return `${GUARD_DOC_TYPE}.${args.instanceDocId}.${args.guardName}`;
336
276
  }
337
277
  function compileGuard(args) {
338
278
  return {
@@ -342,8 +282,8 @@ function compileGuard(args) {
342
282
  resourceId: args.resourceId,
343
283
  owner: args.owner,
344
284
  sourceInstanceId: args.sourceInstanceId,
345
- sourceDefinitionId: args.sourceDefinitionId,
346
- sourceStageId: args.sourceStageId,
285
+ sourceDefinition: args.sourceDefinition,
286
+ sourceStage: args.sourceStage,
347
287
  ...args.name !== void 0 ? { name: args.name } : {},
348
288
  ...args.description !== void 0 ? { description: args.description } : {},
349
289
  match: args.match,
@@ -391,68 +331,129 @@ async function denyingGuards(args) {
391
331
  guardMatches(guard, doc, context.action) && (await evaluateMutationGuard({ guard, context }) || denied.push(guard));
392
332
  return denied;
393
333
  }
394
- function resourceOf(p) {
395
- return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
334
+ async function instanceWriteDenials(args) {
335
+ const { instance, guards, identity } = args, enforceable = guards.filter(
336
+ (g) => g.resourceType === instance.workflowResource.type && g.resourceId === instance.workflowResource.id
337
+ );
338
+ if (enforceable.length === 0) return [];
339
+ const doc = instance;
340
+ return denyingGuards({
341
+ guards: enforceable,
342
+ doc: { id: instance._id, type: instance._type },
343
+ context: {
344
+ action: "update",
345
+ before: doc,
346
+ after: doc,
347
+ ...identity !== void 0 ? { identity } : {}
348
+ }
349
+ });
396
350
  }
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;
351
+ async function assertInstanceWriteAllowed(args) {
352
+ const denied = await instanceWriteDenials(args);
353
+ if (denied.length !== 0)
354
+ throw MutationGuardDeniedError.fromGuards({
355
+ documentId: args.instance._id,
356
+ action: "update",
357
+ guards: denied
358
+ });
407
359
  }
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;
360
+ function findOpenStageEntry(host) {
361
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
416
362
  }
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;
363
+ function buildParams(args) {
364
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
365
+ return {
366
+ self: selfGdr(instance),
367
+ state: renderedState(instance.state ?? [], snapshot),
368
+ parent: instance.ancestors.at(-1)?.id ?? null,
369
+ ancestors: instance.ancestors.map((a) => a.id),
370
+ stage: instance.currentStage,
371
+ now,
372
+ /** `$effects` — parent context handoff by entry name, completed-effect
373
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
374
+ effects: effectsContextMap(instance),
375
+ /** `$tasks` — the current stage's task rows, statuses included. */
376
+ tasks: currentTasks2,
377
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
378
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
379
+ ...extra
380
+ };
427
381
  }
428
- function bareIdRefs(targets) {
429
- return targets.flatMap((g) => [g.parsed.documentId, `drafts.${g.parsed.documentId}`]);
382
+ function renderedState(entries, snapshot) {
383
+ const out = {};
384
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
385
+ return out;
430
386
  }
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;
387
+ function renderedValue(entry, snapshot) {
388
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
435
389
  }
436
- function resolveMetadata(metadata, ctx) {
390
+ function scopedStateOverlay(instance, snapshot, taskName) {
391
+ const stageEntry = findOpenStageEntry(instance);
392
+ if (stageEntry === void 0) return {};
393
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
394
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
395
+ }
396
+ function assignedFor(instance, taskName, actor) {
397
+ if (actor === void 0) return !1;
398
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
399
+ return entry === void 0 ? !1 : entry.value.some(
400
+ (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
401
+ );
402
+ }
403
+ function effectsContextMap(instance) {
437
404
  const out = {};
438
- for (const [k, src] of Object.entries(metadata ?? {}))
439
- out[k] = resolveSource(src, ctx);
405
+ for (const entry of instance.effectsContext)
406
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
440
407
  return out;
441
408
  }
409
+ function parseJsonContextEntry(entry) {
410
+ try {
411
+ return JSON.parse(entry.value);
412
+ } catch (err) {
413
+ throw new Error(
414
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
415
+ { cause: err }
416
+ );
417
+ }
418
+ }
419
+ function paramsForLake(params) {
420
+ return {
421
+ ...params,
422
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
423
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
424
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
425
+ state: stripStateForLake(params.state)
426
+ };
427
+ }
428
+ function stripStateForLake(value) {
429
+ if (value == null) return value;
430
+ if (typeof value == "string") return bareId(value);
431
+ if (Array.isArray(value)) return value.map(stripStateForLake);
432
+ if (typeof value == "object") {
433
+ const out = {};
434
+ for (const [k, v2] of Object.entries(value))
435
+ out[k] = stripStateForLake(v2);
436
+ return out;
437
+ }
438
+ return value;
439
+ }
440
+ function bareId(id) {
441
+ return isGdrUri(id) ? extractDocumentId(id) : id;
442
+ }
442
443
  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);
444
+ function resolveGuard(guard, instance, stageName, now) {
445
+ const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
445
446
  if (targets === null) return null;
446
447
  const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
447
448
  return { doc: compileGuard({
448
- id: lakeGuardId({ instanceDocId: instance._id, stageId, index }),
449
+ id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
449
450
  resourceType: resource.type,
450
451
  resourceId: resource.id,
451
452
  owner: GUARD_OWNER,
452
453
  sourceInstanceId: instance._id,
453
- sourceDefinitionId: instance.workflowId,
454
- sourceStageId: stageId,
455
- ...guard.name !== void 0 ? { name: guard.name } : {},
454
+ sourceDefinition: instance.definition,
455
+ sourceStage: stageName,
456
+ name: guard.name,
456
457
  ...guard.description !== void 0 ? { description: guard.description } : {},
457
458
  match: {
458
459
  ...types !== void 0 ? { types } : {},
@@ -473,24 +474,25 @@ async function upsertGuard(client, doc) {
473
474
  await client.patch(doc._id).set(body).commit();
474
475
  }
475
476
  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);
477
+ const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
478
+ for (const guard of stage?.guards ?? []) {
479
+ const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
479
480
  resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
480
481
  }
481
482
  return out;
482
483
  }
483
- async function committedStageId(args) {
484
- return (await args.client.getDocument(args.instance._id))?.currentStageId;
484
+ async function committedInstance(args) {
485
+ return await args.client.getDocument(args.instance._id) ?? void 0;
485
486
  }
486
487
  async function deployStageGuards(args) {
487
- if (await committedStageId(args) === args.stageId)
488
+ const live = await committedInstance(args);
489
+ if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
488
490
  for (const { client, doc } of resolvedStageGuards(args))
489
491
  await upsertGuard(client, doc);
490
492
  }
491
493
  async function retractStageGuards(args) {
492
- const live = await committedStageId(args);
493
- if (!(live === void 0 || live === args.stageId))
494
+ const live = await committedInstance(args);
495
+ if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
494
496
  for (const { client, doc } of resolvedStageGuards(args))
495
497
  await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
496
498
  }
@@ -498,6 +500,20 @@ function randomKey(length = 12) {
498
500
  const bytes = new Uint8Array(length);
499
501
  return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
500
502
  }
503
+ async function evaluateCondition(args) {
504
+ const { condition, snapshot, params } = args;
505
+ return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
506
+ }
507
+ async function evaluatePredicates(args) {
508
+ const out = {};
509
+ for (const [name, groq] of Object.entries(args.predicates ?? {}))
510
+ out[name] = !!await runGroq(groq, args.params, args.snapshot);
511
+ return out;
512
+ }
513
+ async function runGroq(groq, params, snapshot) {
514
+ const tree = parse(groq, { params });
515
+ return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
516
+ }
501
517
  function buildSnapshot(args) {
502
518
  const docs = [], knownIds = /* @__PURE__ */ new Set();
503
519
  for (const { doc, resource } of args.docs) {
@@ -518,48 +534,59 @@ function rewriteRefsRecursive(value, resource) {
518
534
  k === "_ref" && typeof v2 == "string" ? out[k] = v2.includes(":") ? v2 : gdrFromResource(resource, v2) : out[k] = rewriteRefsRecursive(v2, resource);
519
535
  return out;
520
536
  }
521
- function findOpenStageEntry(host) {
522
- return host.stages.find((s) => s.id === host.currentStageId && s.exitedAt === void 0);
537
+ const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
538
+ function parseDefinitionSnapshot(instance) {
539
+ try {
540
+ return JSON.parse(instance.definitionSnapshot);
541
+ } catch (err) {
542
+ throw new Error(
543
+ `Failed to parse definitionSnapshot on instance "${instance._id}": ${err instanceof Error ? err.message : String(err)}`,
544
+ { cause: err }
545
+ );
546
+ }
523
547
  }
524
548
  function collectWatchRefs(instance) {
525
549
  const stage = findOpenStageEntry(instance);
526
550
  return [
527
551
  gdrRef(instance.workflowResource, instance._id, instance._type),
528
552
  ...instance.ancestors,
529
- ...slotDocRefs(instance.state),
530
- ...slotDocRefs(stage?.state),
531
- ...slotReleaseRefs(instance.state),
532
- ...slotReleaseRefs(stage?.state)
553
+ ...entryDocRefs(instance.state),
554
+ ...entryDocRefs(stage?.state),
555
+ ...entryReleaseRefs(instance.state),
556
+ ...entryReleaseRefs(stage?.state)
533
557
  ];
534
558
  }
535
559
  function readsRaw(ref) {
536
- return ref.type === "workflow.instance" || ref.type === "system.release";
560
+ return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
537
561
  }
538
562
  function contentReleaseName(args) {
539
563
  const { ref, perspective } = args;
540
564
  if (!readsRaw(ref) && Array.isArray(perspective))
541
565
  return perspective.find((entry) => entry !== "drafts" && entry !== "published" && entry !== "raw");
542
566
  }
567
+ function subscriptionDocument(ref) {
568
+ return { ...parseGdr(ref.id), globalDocumentId: ref.id, type: ref.type };
569
+ }
543
570
  function subscriptionDocumentsForInstance(instance) {
544
571
  const seen = /* @__PURE__ */ new Set(), documents = [];
545
572
  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 }));
573
+ !isGdrUri(ref.id) || seen.has(ref.id) || (seen.add(ref.id), documents.push(subscriptionDocument(ref)));
547
574
  return {
548
575
  documents,
549
576
  ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
550
577
  };
551
578
  }
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) : [] : []);
579
+ function entryDocRefs(state) {
580
+ 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
581
  }
555
- function slotReleaseRefs(slots) {
556
- return slotEntries(slots).flatMap(
557
- (slot) => slot._type === "workflow.state.release.ref" && isGdr(slot.value) ? [slot.value] : []
582
+ function entryReleaseRefs(state) {
583
+ return stateEntries(state).flatMap(
584
+ (entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
558
585
  );
559
586
  }
560
- function slotEntries(slots) {
561
- return Array.isArray(slots) ? slots.filter(
562
- (slot) => !!slot && typeof slot == "object"
587
+ function stateEntries(state) {
588
+ return Array.isArray(state) ? state.filter(
589
+ (entry) => !!entry && typeof entry == "object"
563
590
  ) : [];
564
591
  }
565
592
  async function hydrateSnapshot(args) {
@@ -601,17 +628,18 @@ async function readDoc(client, id, perspective) {
601
628
  function resourceFromParsed(parsed) {
602
629
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
603
630
  }
604
- function collectSlotDocUris(resolvedStateSlots) {
605
- return slotDocRefs(resolvedStateSlots).map((ref) => ref.id);
631
+ function collectEntryDocUris(resolvedStateEntries) {
632
+ return entryDocRefs(resolvedStateEntries).map((ref) => ref.id);
606
633
  }
607
- const WORKFLOW_INSTANCE_TYPE = "workflow.instance";
608
- class SlotValueShapeError extends Error {
609
- slotType;
610
- slotId;
634
+ class StateValueShapeError extends Error {
635
+ entryType;
636
+ entryName;
611
637
  issues;
612
638
  constructor(args) {
613
639
  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;
640
+ super(
641
+ `State entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`
642
+ ), this.name = "StateValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName, this.issues = args.issues;
615
643
  }
616
644
  }
617
645
  const GdrShape = v.looseObject({
@@ -633,8 +661,8 @@ const GdrShape = v.looseObject({
633
661
  roles: v.optional(v.array(v.string())),
634
662
  onBehalfOf: v.optional(v.string())
635
663
  }), 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)) })
664
+ v.looseObject({ type: v.literal("user"), id: v.pipe(v.string(), v.minLength(1)) }),
665
+ v.looseObject({ type: v.literal("role"), role: v.pipe(v.string(), v.minLength(1)) })
638
666
  ]), ChecklistItemShape = v.looseObject({
639
667
  label: v.pipe(v.string(), v.minLength(1)),
640
668
  done: v.boolean(),
@@ -656,59 +684,59 @@ const GdrShape = v.looseObject({
656
684
  v.check((s) => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")
657
685
  )
658
686
  ]), 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)
687
+ "doc.ref": v.union([v.null(), GdrShape]),
688
+ "doc.refs": v.array(GdrShape),
689
+ "release.ref": v.union([v.null(), ReleaseRefShape]),
690
+ query: v.any(),
691
+ "value.string": NullableString,
692
+ "value.url": NullableUrl,
693
+ "value.number": NullableNumber,
694
+ "value.boolean": NullableBoolean,
695
+ "value.dateTime": NullableDateTime,
696
+ "value.actor": v.union([v.null(), ActorShape]),
697
+ checklist: v.array(ChecklistItemShape),
698
+ notes: v.array(NoteItemShape),
699
+ assignees: v.array(AssigneeShape)
672
700
  }, itemSchemas = {
673
- "workflow.state.doc.refs": GdrShape,
674
- "workflow.state.checklist": ChecklistItemShape,
675
- "workflow.state.notes": NoteItemShape,
676
- "workflow.state.assignees": AssigneeShape
701
+ "doc.refs": GdrShape,
702
+ checklist: ChecklistItemShape,
703
+ notes: NoteItemShape,
704
+ assignees: AssigneeShape
677
705
  };
678
- function isAppendable(slotType) {
679
- return slotType in itemSchemas;
706
+ function isAppendable(entryType) {
707
+ return entryType in itemSchemas;
680
708
  }
681
- function validateSlotValue(args) {
682
- const schema = valueSchemas[args.slotType];
709
+ function validateStateValue(args) {
710
+ const schema = valueSchemas[args.entryType];
683
711
  if (schema === void 0)
684
- throw new SlotValueShapeError({
685
- slotType: args.slotType,
686
- slotId: args.slotId,
687
- issues: [`unknown slot type ${args.slotType}`],
712
+ throw new StateValueShapeError({
713
+ entryType: args.entryType,
714
+ entryName: args.entryName,
715
+ issues: [`unknown state entry type ${args.entryType}`],
688
716
  mode: "value"
689
717
  });
690
718
  const result = v.safeParse(schema, args.value);
691
719
  if (!result.success)
692
- throw new SlotValueShapeError({
693
- slotType: args.slotType,
694
- slotId: args.slotId,
720
+ throw new StateValueShapeError({
721
+ entryType: args.entryType,
722
+ entryName: args.entryName,
695
723
  issues: formatIssues(result.issues),
696
724
  mode: "value"
697
725
  });
698
726
  }
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`],
727
+ function validateStateAppendItem(args) {
728
+ if (!isAppendable(args.entryType))
729
+ throw new StateValueShapeError({
730
+ entryType: args.entryType,
731
+ entryName: args.entryName,
732
+ issues: [`state entry type ${args.entryType} does not support append`],
705
733
  mode: "item"
706
734
  });
707
- const schema = itemSchemas[args.slotType], result = v.safeParse(schema, args.item);
735
+ const schema = itemSchemas[args.entryType], result = v.safeParse(schema, args.item);
708
736
  if (!result.success)
709
- throw new SlotValueShapeError({
710
- slotType: args.slotType,
711
- slotId: args.slotId,
737
+ throw new StateValueShapeError({
738
+ entryType: args.entryType,
739
+ entryName: args.entryName,
712
740
  issues: formatIssues(result.issues),
713
741
  mode: "item"
714
742
  });
@@ -719,115 +747,115 @@ function formatIssues(issues) {
719
747
  return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
720
748
  });
721
749
  }
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;
750
+ function derivePerspectiveFromState(entries) {
751
+ if (entries !== void 0) {
752
+ for (const entry of entries)
753
+ if (entry._type === "release.ref" && entry.value !== null) {
754
+ const releaseName = entry.value.releaseName;
727
755
  if (typeof releaseName == "string" && releaseName.length > 0)
728
756
  return [releaseName];
729
757
  }
730
758
  }
731
759
  }
732
760
  async function resolveDeclaredState(args) {
733
- const { slotDefs, initialState, ctx, randomKey: randomKey2 } = args;
734
- if (slotDefs === void 0 || slotDefs.length === 0) return [];
761
+ const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
762
+ if (entryDefs === void 0 || entryDefs.length === 0) return [];
735
763
  const out = [];
736
- for (const slot of slotDefs) {
764
+ for (const entry of entryDefs) {
737
765
  const scopedCtx = { ...ctx, resolvedState: out };
738
- out.push(await resolveOneSlot(slot, initialState, scopedCtx, randomKey2));
766
+ out.push(await resolveOneEntry(entry, initialState, scopedCtx, randomKey2));
739
767
  }
740
768
  return out;
741
769
  }
742
770
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
743
- "workflow.state.doc.refs",
744
- "workflow.state.checklist",
745
- "workflow.state.notes",
746
- "workflow.state.assignees"
771
+ "doc.refs",
772
+ "checklist",
773
+ "notes",
774
+ "assignees"
747
775
  ]);
748
- function defaultSlotValue(slotType) {
749
- return ARRAY_SLOT_TYPES.has(slotType) ? [] : null;
776
+ function defaultEntryValue(entryType) {
777
+ return ARRAY_SLOT_TYPES.has(entryType) ? [] : null;
750
778
  }
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);
779
+ function resolveInitValue(entry, initialState, defaultValue) {
780
+ const initMatch = initialState.find((s) => s.name === entry.name && s.type === entry.type);
781
+ return initMatch === void 0 ? defaultValue : (assertInitValueShape(entry, initMatch.value), initMatch.value);
754
782
  }
755
783
  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;
784
+ const target = (source.scope === "workflow" ? ctx.workflowState ?? [] : ctx.resolvedState ?? []).find((s) => s.name === source.state), targetValue = target === void 0 ? void 0 : target.value;
757
785
  return (source.path !== void 0 ? getPath(targetValue, source.path) : targetValue) ?? defaultValue;
758
786
  }
759
- async function resolveQueryValue(slot, source, ctx, client, defaultValue) {
787
+ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
760
788
  const params = paramsForLake({
761
789
  self: ctx.selfId ?? null,
762
790
  state: stateMapFromResolved(ctx.resolvedState ?? []),
763
- stage: ctx.stageId ?? null,
764
- task: ctx.taskId ?? null,
791
+ stage: ctx.stageName ?? null,
792
+ task: ctx.taskName ?? null,
765
793
  now: ctx.now,
766
794
  engineTags: ctx.tags ?? []
767
795
  });
768
796
  try {
769
797
  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;
798
+ return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
771
799
  } catch (err) {
772
800
  throw new Error(
773
- `Failed to resolve query slot "${slot.id}" (${slot.type}): ${err instanceof Error ? err.message : String(err)}`,
801
+ `Failed to resolve query entry "${entry.name}" (${entry.type}): ${err instanceof Error ? err.message : String(err)}`,
774
802
  { cause: err }
775
803
  );
776
804
  }
777
805
  }
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;
806
+ async function resolveEntryValue(entry, initialState, ctx, defaultValue) {
807
+ const source = entry.source;
808
+ 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
809
  }
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" ? {
810
+ function buildResolvedEntry(entry, value, _key, now) {
811
+ const titleProp = entry.title !== void 0 ? { title: entry.title } : {}, descriptionProp = entry.description !== void 0 ? { description: entry.description } : {};
812
+ return entry.type === "query" ? {
785
813
  _key,
786
- _type: "workflow.state.query",
787
- id: slot.id,
814
+ _type: "query",
815
+ name: entry.name,
788
816
  ...titleProp,
789
817
  ...descriptionProp,
790
818
  value,
791
819
  resolvedAt: now
792
820
  } : {
793
821
  _key,
794
- _type: slot.type,
795
- id: slot.id,
822
+ _type: entry.type,
823
+ name: entry.name,
796
824
  ...titleProp,
797
825
  ...descriptionProp,
798
826
  value
799
827
  };
800
828
  }
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);
829
+ async function resolveOneEntry(entry, initialState, ctx, randomKey2) {
830
+ const defaultValue = defaultEntryValue(entry.type), value = await resolveEntryValue(entry, initialState, ctx, defaultValue);
831
+ return validateStateValue({ entryType: entry.type, entryName: entry.name, value }), buildResolvedEntry(entry, value, randomKey2(), ctx.now);
804
832
  }
805
- function stateMapFromResolved(slots) {
833
+ function stateMapFromResolved(entries) {
806
834
  const out = {};
807
- for (const s of slots) out[s.id] = s;
835
+ for (const s of entries) out[s.name] = s.value;
808
836
  return out;
809
837
  }
810
- function assertInitValueShape(slot, value) {
811
- if (slot.type === "workflow.state.doc.ref") {
812
- assertGdrShape(value, `state slot "${slot.id}" (workflow.state.doc.ref)`);
838
+ function assertInitValueShape(entry, value) {
839
+ if (entry.type === "doc.ref") {
840
+ assertGdrShape(value, `state entry "${entry.name}" (doc.ref)`);
813
841
  return;
814
842
  }
815
- if (slot.type === "workflow.state.release.ref") {
816
- assertGdrShape(value, `state slot "${slot.id}" (workflow.state.release.ref)`);
843
+ if (entry.type === "release.ref") {
844
+ assertGdrShape(value, `state entry "${entry.name}" (release.ref)`);
817
845
  const v2 = value;
818
846
  if (typeof v2.releaseName != "string" || v2.releaseName.length === 0)
819
847
  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)}.`
848
+ `Invalid init value for state entry "${entry.name}" (release.ref): \`releaseName\` must be a non-empty string. Got ${JSON.stringify(v2.releaseName)}.`
821
849
  );
822
850
  return;
823
851
  }
824
- if (slot.type === "workflow.state.doc.refs") {
852
+ if (entry.type === "doc.refs") {
825
853
  if (!Array.isArray(value))
826
854
  throw new Error(
827
- `Invalid init value for state slot "${slot.id}" (workflow.state.doc.refs): expected an array of GDRs, got ${typeof value}.`
855
+ `Invalid init value for state entry "${entry.name}" (doc.refs): expected an array of GDRs, got ${typeof value}.`
828
856
  );
829
857
  for (const [i, item] of value.entries())
830
- assertGdrShape(item, `state slot "${slot.id}" (workflow.state.doc.refs) item [${i}]`);
858
+ assertGdrShape(item, `state entry "${entry.name}" (doc.refs) item [${i}]`);
831
859
  }
832
860
  }
833
861
  function assertGdrShape(value, context) {
@@ -845,8 +873,8 @@ function assertGdrShape(value, context) {
845
873
  `Invalid GDR for ${context}: \`type\` (schema name) must be a non-empty string. Got ${JSON.stringify(v2.type)}.`
846
874
  );
847
875
  }
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;
876
+ function normalizeQueryResult(entryType, raw, workflowResource) {
877
+ 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
878
  }
851
879
  function toGdrUri(docId, workflowResource) {
852
880
  return isGdrUri(docId) ? docId : gdrFromResource(workflowResource, docId);
@@ -871,7 +899,7 @@ async function loadContext(client, instanceId, options) {
871
899
  const instance = await client.getDocument(instanceId);
872
900
  if (!instance)
873
901
  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({
902
+ const definition = parseDefinitionSnapshot(instance), clientForGdr = options?.clientForGdr ?? (() => client), clock = options?.clock ?? wallClock, snapshot = await hydrateSnapshot({
875
903
  client,
876
904
  clientForGdr,
877
905
  instance,
@@ -879,12 +907,28 @@ async function loadContext(client, instanceId, options) {
879
907
  });
880
908
  return { client, clientForGdr, clock, now: clock(), instance, definition, snapshot };
881
909
  }
882
- function ctxEvaluateFilter(ctx, filter) {
883
- return evaluateFilter({
884
- filter,
885
- definition: ctx.definition,
910
+ async function ctxConditionParams(ctx, opts) {
911
+ const base = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), state = {
912
+ ...base.state,
913
+ ...scopedStateOverlay(ctx.instance, ctx.snapshot, opts?.taskName)
914
+ }, params = {
915
+ ...base,
916
+ state,
917
+ actor: opts?.actor,
918
+ assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
919
+ ...opts?.vars
920
+ };
921
+ return { ...await evaluatePredicates({
922
+ predicates: ctx.definition.predicates,
886
923
  snapshot: ctx.snapshot,
887
- params: buildParams(ctx.instance, ctx.now)
924
+ params
925
+ }), ...params };
926
+ }
927
+ async function ctxEvaluateCondition(ctx, condition, opts) {
928
+ return condition === void 0 ? !0 : evaluateCondition({
929
+ condition,
930
+ snapshot: ctx.snapshot,
931
+ params: await ctxConditionParams(ctx, opts)
888
932
  });
889
933
  }
890
934
  async function buildEngineContext(args) {
@@ -899,55 +943,63 @@ async function buildEngineContext(args) {
899
943
  snapshot: await hydrateSnapshot({ client, clientForGdr, instance })
900
944
  };
901
945
  }
902
- function findStage(definition, stageId) {
903
- const stage = definition.stages.find((s) => s.id === stageId);
946
+ function findStage(definition, stageName) {
947
+ const stage = definition.stages.find((s) => s.name === stageName);
904
948
  if (stage === void 0)
905
- throw new Error(`Stage "${stageId}" not found in definition ${definition.workflowId}`);
949
+ throw new Error(`Stage "${stageName}" not found in definition ${definition.name}`);
906
950
  return stage;
907
951
  }
908
- async function resolveStageStateSlots(args) {
952
+ async function resolveStageStateEntries(args) {
909
953
  const { client, instance, stage, now, initialState } = args;
910
954
  return resolveDeclaredState({
911
- slotDefs: stage.state,
955
+ entryDefs: stage.state,
912
956
  initialState: initialState ?? [],
913
957
  ctx: {
914
958
  client,
915
959
  now,
916
960
  selfId: instance._id,
917
961
  tags: instance.tags ?? [],
918
- stageId: stage.id,
962
+ stageName: stage.name,
919
963
  workflowResource: instance.workflowResource,
920
- // Allow stage-scope slots' `source: { kind: "stateRead", scope:
921
- // "workflow", ... }` to see the already-resolved workflow-scope
922
- // state.
964
+ // Allow stage-scope entries' `source: { type: "stateRead", scope:
965
+ // "workflow", ... }` to see the already-resolved workflow-scope state.
923
966
  workflowState: instance.state ?? [],
924
967
  ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
925
968
  },
926
969
  randomKey
927
970
  });
928
971
  }
929
- async function resolveTaskStateSlots(args) {
972
+ async function resolveTaskStateEntries(args) {
930
973
  const { client, instance, stage, task, now } = args;
931
974
  return resolveDeclaredState({
932
- slotDefs: task.state,
975
+ entryDefs: task.state,
933
976
  initialState: [],
934
977
  ctx: {
935
978
  client,
936
979
  now,
937
980
  selfId: instance._id,
938
981
  tags: instance.tags ?? [],
939
- stageId: stage.id,
982
+ stageName: stage.name,
940
983
  workflowResource: instance.workflowResource,
941
- taskId: task.id,
984
+ taskName: task.name,
942
985
  workflowState: instance.state ?? [],
943
986
  ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
944
987
  },
945
988
  randomKey
946
989
  });
947
990
  }
948
- function effectsContextEntry(id, value) {
991
+ async function resolveBindings(args) {
992
+ const resolved = {};
993
+ for (const [key, groq] of Object.entries(args.bindings ?? {}))
994
+ resolved[key] = await runGroq(groq, args.params, args.snapshot);
995
+ return { ...resolved, ...args.staticInput };
996
+ }
997
+ function effectsContextEntry(name, value) {
949
998
  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 };
999
+ 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 };
1000
+ }
1001
+ function effectsContextJsonEntry(name, value) {
1002
+ return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
951
1003
  }
952
1004
  function buildInstanceBase(args) {
953
1005
  const { id, now, actor, perspective } = args;
@@ -959,23 +1011,23 @@ function buildInstanceBase(args) {
959
1011
  _updatedAt: now,
960
1012
  tags: args.tags,
961
1013
  workflowResource: args.workflowResource,
962
- workflowId: args.workflowId,
1014
+ definition: args.definitionName,
963
1015
  pinnedVersion: args.pinnedVersion,
964
1016
  definitionSnapshot: JSON.stringify(args.definition),
965
1017
  state: args.state,
966
1018
  effectsContext: args.effectsContext,
967
1019
  ancestors: args.ancestors,
968
1020
  ...perspective !== void 0 ? { perspective } : {},
969
- currentStageId: args.initialStageId,
1021
+ currentStage: args.initialStage,
970
1022
  stages: [],
971
1023
  pendingEffects: [],
972
1024
  effectHistory: [],
973
1025
  history: [
974
1026
  {
975
1027
  _key: randomKey(),
976
- _type: "workflow.history.stageEntered",
1028
+ _type: "stageEntered",
977
1029
  at: now,
978
- stageId: args.initialStageId,
1030
+ stage: args.initialStage,
979
1031
  ...actor !== void 0 ? { actor } : {}
980
1032
  }
981
1033
  ],
@@ -983,253 +1035,15 @@ function buildInstanceBase(args) {
983
1035
  lastChangedAt: now
984
1036
  };
985
1037
  }
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
- };
1008
- }
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
- };
1020
- }
1021
- function stageTransitionHistory(args) {
1022
- const { fromStageId, toStageId, at, transitionId, actor, via, reason } = args, shared = {
1023
- at,
1024
- ...transitionId !== void 0 ? { transitionId } : {},
1025
- ...actor !== void 0 ? { actor } : {},
1026
- ...via !== void 0 ? { via } : {},
1027
- ...reason !== void 0 ? { reason } : {}
1028
- };
1029
- return [
1030
- {
1031
- _key: randomKey(),
1032
- _type: "workflow.history.stageExited",
1033
- stageId: fromStageId,
1034
- toStageId,
1035
- ...shared
1036
- },
1037
- {
1038
- _key: randomKey(),
1039
- _type: "workflow.history.transitionFired",
1040
- fromStageId,
1041
- toStageId,
1042
- ...shared
1043
- },
1044
- {
1045
- _key: randomKey(),
1046
- _type: "workflow.history.stageEntered",
1047
- stageId: toStageId,
1048
- fromStageId,
1049
- ...shared
1050
- }
1051
- ];
1052
- }
1053
- function instanceStateFields(src) {
1054
- const fields = {
1055
- currentStageId: src.currentStageId,
1056
- state: src.state,
1057
- stages: src.stages,
1058
- pendingEffects: src.pendingEffects,
1059
- effectHistory: src.effectHistory,
1060
- effectsContext: src.effectsContext,
1061
- history: src.history
1062
- };
1063
- return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), fields;
1064
- }
1065
- function materializeInstance(base, mutation) {
1066
- return {
1067
- ...base,
1068
- currentStageId: mutation.currentStageId,
1069
- state: mutation.state,
1070
- stages: mutation.stages
1071
- };
1072
- }
1073
- async function persist(ctx, mutation) {
1074
- const set = {
1075
- ...instanceStateFields(mutation),
1076
- lastChangedAt: ctx.now
1077
- }, pendingCreates = mutation.pendingCreates;
1078
- if (pendingCreates.length === 0)
1079
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1080
- const tx = ctx.client.transaction();
1081
- for (const body of pendingCreates) tx.create(body);
1082
- tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1083
- const actorForPriming = void 0;
1084
- for (const body of pendingCreates)
1085
- await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1086
- const reloaded = await ctx.client.getDocument(ctx.instance._id);
1087
- if (!reloaded)
1088
- throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1089
- return reloaded;
1090
- }
1091
- function currentStageEntry(mutation) {
1092
- const entry = findOpenStageEntry(mutation);
1093
- if (entry === void 0)
1094
- throw new Error(
1095
- `Mutation invariant broken: no current (un-exited) StageEntry for currentStageId "${mutation.currentStageId}"`
1096
- );
1097
- return entry;
1098
- }
1099
- function currentTasks(mutation) {
1100
- return currentStageEntry(mutation).tasks;
1101
- }
1102
- function findTaskInCurrentStage(ctx, taskId) {
1103
- const stage = findStage(ctx.definition, ctx.instance.currentStageId), task = (stage.tasks ?? []).find((t) => t.id === taskId);
1104
- if (task === void 0)
1105
- throw new Error(
1106
- `Task "${taskId}" not found in current stage "${stage.id}" of ${ctx.definition.workflowId}`
1107
- );
1108
- return { stage, task };
1109
- }
1110
- function requireMutationTaskEntry(mutation, taskId) {
1111
- const mutEntry = currentTasks(mutation).find((t) => t.id === taskId);
1112
- if (mutEntry === void 0)
1113
- throw new Error(`Task "${taskId}" disappeared from mutation copy \u2014 invariant broken`);
1114
- return mutEntry;
1115
- }
1116
- function findCurrentStageEntry(instance) {
1117
- return findOpenStageEntry(instance);
1118
- }
1119
- function findCurrentTasks(instance) {
1120
- return findCurrentStageEntry(instance)?.tasks ?? [];
1121
- }
1122
- async function completeEffect(args) {
1123
- const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1124
- ...options?.clock ? { clock: options.clock } : {}
1125
- });
1126
- return commitCompleteEffect(
1127
- ctx,
1128
- effectKey,
1129
- status,
1130
- outputs,
1131
- detail,
1132
- error,
1133
- durationMs,
1134
- options?.actor
1135
- );
1136
- }
1137
- function buildEffectHistoryEntry(pending, outcome) {
1138
- const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
1139
- return {
1140
- _key: pending._key,
1141
- effectId: pending.effectId,
1142
- ...pending.title !== void 0 ? { title: pending.title } : {},
1143
- ...pending.description !== void 0 ? { description: pending.description } : {},
1144
- params: pending.params,
1145
- origin: pending.origin,
1146
- ...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
1147
- ranAt,
1148
- ...durationMs !== void 0 ? { durationMs } : {},
1149
- status,
1150
- ...detail !== void 0 ? { detail } : {},
1151
- ...error !== void 0 ? { error } : {},
1152
- ...outputs !== void 0 ? { outputs } : {}
1153
- };
1154
- }
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
- }
1160
- }
1161
- async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1162
- const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1163
- if (pending === void 0)
1164
- throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1165
- const mutation = startMutation(ctx.instance);
1166
- mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1167
- const ranAt = ctx.now;
1168
- return mutation.effectHistory.push(
1169
- buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1170
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, outputs), mutation.history.push({
1171
- _key: randomKey(),
1172
- _type: "workflow.history.effectCompleted",
1173
- at: ranAt,
1174
- effectKey,
1175
- effectId: pending.effectId,
1176
- status,
1177
- ...outputs !== void 0 ? { outputs } : {},
1178
- ...detail !== void 0 ? { detail } : {},
1179
- ...actor !== void 0 ? { actor } : {}
1180
- }), await persist(ctx, mutation), { effectKey, status };
1181
- }
1182
- function buildQueuedEffect(effect, origin, params, actor, now) {
1183
- const key = randomKey(), pending = {
1184
- _key: key,
1185
- _type: "workflow.pendingEffect",
1186
- effectId: effect.id,
1187
- ...effect.title !== void 0 ? { title: effect.title } : {},
1188
- ...effect.description !== void 0 ? { description: effect.description } : {},
1189
- ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1190
- params,
1191
- origin,
1192
- ...actor !== void 0 ? { actor } : {},
1193
- queuedAt: now
1194
- }, history = {
1195
- _key: randomKey(),
1196
- _type: "workflow.history.effectQueued",
1197
- at: now,
1198
- effectKey: key,
1199
- effectId: effect.id,
1200
- origin
1201
- };
1202
- return { pending, history };
1203
- }
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
- };
1212
- for (const effect of effects) {
1213
- const params = await resolveBindings({
1214
- bindings: effect.bindings,
1215
- 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);
1221
- mutation.pendingEffects.push(pending), mutation.history.push(history);
1222
- }
1223
- }
1224
1038
  class ActionParamsInvalidError extends Error {
1225
1039
  action;
1226
- taskId;
1040
+ task;
1227
1041
  issues;
1228
1042
  constructor(args) {
1229
- const lines = args.issues.map((i) => ` - ${i.paramId}: ${i.reason}`).join(`
1043
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1230
1044
  `);
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;
1045
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1046
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1233
1047
  }
1234
1048
  }
1235
1049
  class WorkflowStateDivergedError extends Error {
@@ -1248,13 +1062,13 @@ class WorkflowStateDivergedError extends Error {
1248
1062
  const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1249
1063
  class ConcurrentFireActionError extends Error {
1250
1064
  instanceId;
1251
- taskId;
1065
+ task;
1252
1066
  action;
1253
1067
  attempts;
1254
1068
  constructor(args) {
1255
1069
  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;
1070
+ `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.`
1071
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1258
1072
  }
1259
1073
  }
1260
1074
  function isRevisionConflict(error) {
@@ -1295,6 +1109,51 @@ async function deployOrRollback(args) {
1295
1109
  throw guardError;
1296
1110
  }
1297
1111
  }
1112
+ function applyTaskStatusChange(args) {
1113
+ const { entry, history, stage, to, at, actor } = args, from = entry.status;
1114
+ entry.status = to, isTerminalTaskStatus(to) && (entry.completedAt = at, actor !== void 0 && (entry.completedBy = actor.id)), history.push({
1115
+ _key: randomKey(),
1116
+ _type: "taskStatusChanged",
1117
+ at,
1118
+ stage,
1119
+ task: entry.name,
1120
+ from,
1121
+ to,
1122
+ ...actor !== void 0 ? { actor } : {}
1123
+ });
1124
+ }
1125
+ function stageTransitionHistory(args) {
1126
+ const { fromStage, toStage, at, transition, actor, via, reason } = args, shared = {
1127
+ at,
1128
+ ...transition !== void 0 ? { transition } : {},
1129
+ ...actor !== void 0 ? { actor } : {},
1130
+ ...via !== void 0 ? { via } : {},
1131
+ ...reason !== void 0 ? { reason } : {}
1132
+ };
1133
+ return [
1134
+ {
1135
+ _key: randomKey(),
1136
+ _type: "stageExited",
1137
+ stage: fromStage,
1138
+ toStage,
1139
+ ...shared
1140
+ },
1141
+ {
1142
+ _key: randomKey(),
1143
+ _type: "transitionFired",
1144
+ fromStage,
1145
+ toStage,
1146
+ ...shared
1147
+ },
1148
+ {
1149
+ _key: randomKey(),
1150
+ _type: "stageEntered",
1151
+ stage: toStage,
1152
+ fromStage,
1153
+ ...shared
1154
+ }
1155
+ ];
1156
+ }
1298
1157
  const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
1299
1158
  function validateTags(tags) {
1300
1159
  if (tags.length === 0)
@@ -1312,7 +1171,7 @@ function tagScopeFilter(param = "engineTags") {
1312
1171
  return `count(tags[@ in $${param}]) > 0`;
1313
1172
  }
1314
1173
  function definitionLookupGroq(explicit) {
1315
- const scoped = `_type == "${WORKFLOW_DEFINITION_TYPE}" && workflowId == $workflowId && ${tagScopeFilter()}`;
1174
+ const scoped = `_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
1316
1175
  return explicit ? `*[${scoped} && version == $version][0]` : `*[${scoped}] | order(version desc)[0]`;
1317
1176
  }
1318
1177
  async function discoverItems(args) {
@@ -1327,23 +1186,49 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
1327
1186
  return defaultClient;
1328
1187
  }
1329
1188
  }
1330
- const MAX_SPAWN_DEPTH = 6;
1331
- async function spawnChildren(ctx, mutation, task, spawn, actor) {
1189
+ 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";
1190
+ function gateActor(outcome) {
1191
+ return {
1192
+ kind: "system",
1193
+ id: outcome === "failed" ? FAIL_WHEN_SYSTEM_ID : COMPLETE_WHEN_SYSTEM_ID
1194
+ };
1195
+ }
1196
+ function effectiveCompleteWhen(task) {
1197
+ return task.completeWhen ?? (task.subworkflows !== void 0 ? SUBWORKFLOWS_ALL_DONE : void 0);
1198
+ }
1199
+ async function evaluateResolutionGate(ctx, task, vars) {
1200
+ const scope = { taskName: task.name, vars };
1201
+ if (task.failWhen !== void 0 && await ctxEvaluateCondition(ctx, task.failWhen, scope))
1202
+ return "failed";
1203
+ const completeWhen = effectiveCompleteWhen(task);
1204
+ if (completeWhen !== void 0 && await ctxEvaluateCondition(ctx, completeWhen, scope))
1205
+ return "done";
1206
+ }
1207
+ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1332
1208
  if (ctx.instance.ancestors.length + 1 > MAX_SPAWN_DEPTH) {
1333
1209
  const chain = [...ctx.instance.ancestors.map((a) => a.id), selfGdr(ctx.instance)].join(" \u2192 ");
1334
1210
  throw new Error(
1335
- `Spawn depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.id}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
1211
+ `Subworkflow depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.name}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
1212
+ );
1213
+ }
1214
+ const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tags ?? []);
1215
+ if (definition === null) {
1216
+ const versionLabel = sub.definition.version === void 0 || sub.definition.version === "latest" ? "latest" : `v${sub.definition.version}`;
1217
+ throw new Error(
1218
+ `Subworkflow definition "${sub.definition.name}" ${versionLabel} not deployed (parent ${ctx.instance._id}, task ${task.name})`
1336
1219
  );
1337
1220
  }
1338
- const now = ctx.now, rows = await resolveSpawnRows(ctx, spawn), refs = [];
1221
+ const parentScope = await ctxConditionParams(ctx, {
1222
+ taskName: task.name,
1223
+ ...actor ? { actor } : {}
1224
+ }), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1339
1225
  for (const row of rows) {
1340
- const projected = projectSpawnRow(ctx.instance, spawn, row, now), { ref, body } = await prepareChildInstance({
1226
+ const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
1341
1227
  client: ctx.client,
1342
1228
  parent: ctx.instance,
1343
- task,
1344
- spawn,
1345
- initialState: projected.initialState,
1346
- effectsContext: projected.effectsContext,
1229
+ definition,
1230
+ initialState,
1231
+ effectsContext,
1347
1232
  actor,
1348
1233
  now
1349
1234
  });
@@ -1351,8 +1236,8 @@ async function spawnChildren(ctx, mutation, task, spawn, actor) {
1351
1236
  }
1352
1237
  return refs;
1353
1238
  }
1354
- async function resolveSpawnRows(ctx, spawn) {
1355
- const params = buildParams(ctx.instance, ctx.now), groq = spawn.forEach.groq;
1239
+ async function resolveSpawnRows(ctx, sub) {
1240
+ const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
1356
1241
  let value;
1357
1242
  if (groq.includes("*")) {
1358
1243
  const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
@@ -1368,60 +1253,39 @@ async function resolveSpawnRows(ctx, spawn) {
1368
1253
  }
1369
1254
  return Array.isArray(value) ? value : value == null ? [] : [value];
1370
1255
  }
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;
1256
+ function firstDocRefGdrUri(entries) {
1257
+ if (entries) {
1258
+ for (const s of entries)
1259
+ if (s._type === "doc.ref" && s.value !== null) return s.value.id;
1375
1260
  }
1376
1261
  }
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
- }
1387
- case "self":
1388
- return selfGdr(parent);
1389
- case "stageId":
1390
- return parent.currentStageId;
1391
- case "now":
1392
- return now;
1393
- case "object": {
1394
- const out = {};
1395
- for (const [k, v2] of Object.entries(source.fields))
1396
- out[k] = resolveSpawnSource(v2, parent, row, now);
1397
- return out;
1398
- }
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,
1262
+ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1263
+ const out = [];
1264
+ for (const [name, groq] of Object.entries(sub.with ?? {})) {
1265
+ const declared = (childDefinition.state ?? []).find((e) => e.name === name);
1266
+ if (declared === void 0)
1267
+ throw new Error(
1268
+ `subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
1269
+ );
1270
+ const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
1271
+ value != null && out.push({
1272
+ type: declared.type,
1273
+ name,
1413
1274
  value
1414
1275
  });
1415
1276
  }
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);
1277
+ return out;
1278
+ }
1279
+ async function resolveContextHandoff(ctx, sub, parentScope) {
1280
+ const out = {};
1281
+ for (const [name, groq] of Object.entries(sub.context ?? {})) {
1282
+ const v2 = await runGroq(groq, parentScope, ctx.snapshot);
1283
+ v2 != null && (typeof v2 == "string" || typeof v2 == "number" || typeof v2 == "boolean" || typeof v2 == "object" && "id" in v2 && "type" in v2) && (out[name] = v2);
1420
1284
  }
1421
- return { initialState, effectsContext };
1285
+ return out;
1422
1286
  }
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;
1287
+ function canonicaliseSpawnValue(kind, value, workflowResource) {
1288
+ return kind === "doc.ref" ? coerceSpawnGdr(value, workflowResource) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
1425
1289
  }
1426
1290
  function coerceSpawnGdr(raw, workflowResource) {
1427
1291
  const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
@@ -1437,29 +1301,22 @@ function coerceSpawnGdr(raw, workflowResource) {
1437
1301
  }
1438
1302
  return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1439
1303
  }
1440
- async function resolveLogicalDefinition(client, ref, tags) {
1441
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
1304
+ async function resolveDefinitionRef(client, ref, tags) {
1305
+ const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
1442
1306
  return wantsExplicit && (params.version = ref.version), await client.fetch(
1443
1307
  definitionLookupGroq(wantsExplicit),
1444
1308
  params
1445
1309
  ) ?? null;
1446
1310
  }
1447
1311
  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
- );
1454
- }
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 = [
1312
+ 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 = [
1456
1313
  ...parent.ancestors,
1457
1314
  {
1458
1315
  id: selfGdr(parent),
1459
1316
  type: WORKFLOW_INSTANCE_TYPE
1460
1317
  }
1461
1318
  ], inheritedPerspective = parent.perspective, childState = await resolveDeclaredState({
1462
- slotDefs: definition.state,
1319
+ entryDefs: definition.state,
1463
1320
  initialState,
1464
1321
  ctx: {
1465
1322
  client,
@@ -1475,7 +1332,7 @@ async function prepareChildInstance(args) {
1475
1332
  if (typeof value == "object" && value !== null && "id" in value && "type" in value) {
1476
1333
  if (!isGdrUri(value.id))
1477
1334
  throw new Error(
1478
- `spawn.forEach.as.effectsContext["${key}"]: GDR id must be a "<scheme>:..." URI, got ${JSON.stringify(value.id)}.`
1335
+ `subworkflows.context["${key}"]: GDR id must be a "<scheme>:..." URI, got ${JSON.stringify(value.id)}.`
1479
1336
  );
1480
1337
  return effectsContextEntry(key, { id: value.id, type: value.type });
1481
1338
  }
@@ -1486,77 +1343,274 @@ async function prepareChildInstance(args) {
1486
1343
  now,
1487
1344
  tags: childTags,
1488
1345
  workflowResource,
1489
- workflowId: definition.workflowId,
1346
+ definitionName: definition.name,
1490
1347
  pinnedVersion: definition.version,
1491
1348
  definition,
1492
1349
  state: childState,
1493
1350
  effectsContext: effectsContextEntries,
1494
1351
  ancestors,
1495
1352
  perspective: childPerspective,
1496
- initialStageId: definition.initialStageId,
1353
+ initialStage: definition.initialStage,
1497
1354
  actor
1498
1355
  });
1499
1356
  return { ref: childRef, body: base };
1500
1357
  }
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) {
1358
+ async function subworkflowRows(ctx, refs) {
1359
+ if (refs.length === 0) return [];
1360
+ const ids = refs.map((r) => gdrToBareId(r.id));
1361
+ return (await ctx.client.fetch("*[_id in $ids]{_id, currentStage, completedAt}", { ids })).map((c) => ({
1362
+ _id: c._id,
1363
+ stage: c.currentStage,
1364
+ status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
1365
+ }));
1366
+ }
1367
+ function resolveStaticSource(src, ctx) {
1368
+ switch (src.type) {
1369
+ case "literal":
1370
+ return { handled: !0, value: src.value };
1371
+ case "param":
1372
+ return { handled: !0, value: ctx.params?.[src.param] };
1373
+ case "actor":
1374
+ return { handled: !0, value: ctx.actor };
1375
+ case "now":
1376
+ return { handled: !0, value: ctx.now };
1377
+ default:
1378
+ return { handled: !1 };
1379
+ }
1380
+ }
1381
+ const STATE_OP_TYPES = [
1382
+ "state.set",
1383
+ "state.unset",
1384
+ "state.append",
1385
+ "state.updateWhere",
1386
+ "state.removeWhere"
1387
+ ];
1388
+ function isStateOp(summary) {
1389
+ return STATE_OP_TYPES.includes(summary.opType);
1390
+ }
1391
+ function validateActionParams(action, taskName, callerParams) {
1392
+ const declared = action.params ?? [], params = callerParams ?? {}, issues = [];
1393
+ for (const decl of declared) {
1394
+ const value = params[decl.name], present = decl.name in params && value !== void 0 && value !== null;
1395
+ if (decl.required === !0 && !present) {
1396
+ issues.push({ param: decl.name, reason: "required but missing" });
1397
+ continue;
1398
+ }
1399
+ present && (checkParamType(value, decl) || issues.push({
1400
+ param: decl.name,
1401
+ reason: `expected type=${decl.type}, got ${typeof value}`
1402
+ }));
1403
+ }
1404
+ if (issues.length > 0)
1405
+ throw new ActionParamsInvalidError({ action: action.name, task: taskName, issues });
1406
+ return params;
1407
+ }
1408
+ function hasStringField(value, field) {
1409
+ return typeof value == "object" && value !== null && field in value && typeof value[field] == "string";
1410
+ }
1411
+ function checkParamType(value, decl) {
1412
+ switch (decl.type) {
1413
+ case "string":
1414
+ case "url":
1415
+ case "dateTime":
1416
+ return typeof value == "string";
1417
+ case "number":
1418
+ return typeof value == "number" && Number.isFinite(value);
1419
+ case "boolean":
1420
+ return typeof value == "boolean";
1421
+ case "actor":
1422
+ return hasStringField(value, "kind");
1423
+ case "doc.ref":
1424
+ return hasStringField(value, "id");
1425
+ case "doc.refs":
1426
+ return Array.isArray(value) && value.every((v2) => checkParamType(v2, { type: "doc.ref" }));
1427
+ case "json":
1428
+ return !0;
1429
+ default:
1430
+ return !1;
1431
+ }
1432
+ }
1433
+ function runOps(args) {
1434
+ const { ops, mutation, stage, origin, params, actor, self, now } = args;
1435
+ if (ops === void 0 || ops.length === 0) return [];
1436
+ const summaries = [];
1437
+ for (const op of ops) {
1438
+ const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
1439
+ summaries.push(summary), mutation.history.push({
1440
+ _key: randomKey(),
1441
+ _type: "opApplied",
1442
+ at: now,
1443
+ stage,
1444
+ ...origin.task !== void 0 ? { task: origin.task } : {},
1445
+ ...origin.action !== void 0 ? { action: origin.action } : {},
1446
+ ...origin.transition !== void 0 ? { transition: origin.transition } : {},
1447
+ opType: summary.opType,
1448
+ ...summary.target !== void 0 ? { target: summary.target } : {},
1449
+ ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
1450
+ ...actor !== void 0 ? { actor } : {}
1451
+ });
1452
+ }
1453
+ return summaries;
1454
+ }
1455
+ function applyOp(op, ctx) {
1456
+ switch (op.type) {
1457
+ case "state.set":
1458
+ return applyStateSet(op, ctx);
1459
+ case "state.unset":
1460
+ return applyStateUnset(op, ctx);
1461
+ case "state.append":
1462
+ return applyStateAppend(op, ctx);
1463
+ case "state.updateWhere":
1464
+ return applyStateUpdateWhere(op, ctx);
1465
+ case "state.removeWhere":
1466
+ return applyStateRemoveWhere(op, ctx);
1467
+ case "status.set":
1468
+ return applyStatusSet(op, ctx);
1469
+ }
1470
+ }
1471
+ function applyStateSet(op, ctx) {
1472
+ const value = resolveOpSource(op.value, ctx), entry = locateEntry(ctx, op.target);
1473
+ return validateStateValue({ entryType: entry._type, entryName: entry.name, value }), setEntryValue(entry, value), { opType: op.type, target: op.target, resolved: { value } };
1474
+ }
1475
+ const EMPTY_BY_KIND = {
1476
+ "doc.refs": [],
1477
+ checklist: [],
1478
+ notes: [],
1479
+ assignees: []
1480
+ };
1481
+ function applyStateUnset(op, ctx) {
1482
+ const entry = locateEntry(ctx, op.target);
1483
+ return setEntryValue(entry, EMPTY_BY_KIND[entry._type] ?? null), { opType: op.type, target: op.target };
1484
+ }
1485
+ function applyStateAppend(op, ctx) {
1486
+ const item = resolveOpSource(op.value, ctx), entry = locateEntry(ctx, op.target);
1487
+ validateStateAppendItem({ entryType: entry._type, entryName: entry.name, item });
1488
+ const current = Array.isArray(entry.value) ? entry.value : [];
1489
+ return setEntryValue(entry, [...current, withRowKey(item)]), { opType: op.type, target: op.target, resolved: { item } };
1490
+ }
1491
+ function withRowKey(item) {
1492
+ return typeof item == "object" && item !== null && !Array.isArray(item) && !("_key" in item) ? { _key: randomKey(), ...item } : item;
1493
+ }
1494
+ function applyStateUpdateWhere(op, ctx) {
1495
+ const entry = locateEntry(ctx, op.target), rows = requireArrayValue(entry, op), merge = resolveOpSource(op.value, ctx);
1496
+ if (merge === null || typeof merge != "object" || Array.isArray(merge))
1497
+ throw new Error(
1498
+ `state.updateWhere value must resolve to an object of fields to merge (target "${op.target.state}")`
1499
+ );
1500
+ return setEntryValue(
1501
+ entry,
1502
+ rows.map(
1503
+ (row) => evalOpPredicate(op.where, row, ctx) ? { ...row, ...merge } : row
1504
+ )
1505
+ ), { opType: op.type, target: op.target, resolved: { merge } };
1506
+ }
1507
+ function applyStateRemoveWhere(op, ctx) {
1508
+ const entry = locateEntry(ctx, op.target), rows = requireArrayValue(entry, op);
1509
+ return setEntryValue(
1510
+ entry,
1511
+ rows.filter((row) => !evalOpPredicate(op.where, row, ctx))
1512
+ ), { opType: op.type, target: op.target };
1513
+ }
1514
+ function requireArrayValue(entry, op) {
1515
+ if (!Array.isArray(entry.value))
1516
+ throw new Error(
1517
+ `${op.type} target ${op.target.scope}:"${op.target.state}" holds a non-array ${entry._type} value \u2014 where-ops operate on array entries`
1518
+ );
1519
+ return entry.value;
1520
+ }
1521
+ function applyStatusSet(op, ctx) {
1522
+ const entry = findOpenStageEntry(ctx.mutation)?.tasks.find((t) => t.name === op.task);
1523
+ if (entry === void 0)
1524
+ throw new Error(`status.set targets task "${op.task}" which has no entry in the open stage`);
1525
+ return applyTaskStatusChange({
1526
+ entry,
1527
+ history: ctx.mutation.history,
1528
+ stage: ctx.stage,
1529
+ to: op.status,
1530
+ at: ctx.now,
1531
+ ...ctx.actor !== void 0 ? { actor: ctx.actor } : {}
1532
+ }), { opType: op.type, resolved: { task: op.task, status: op.status } };
1533
+ }
1534
+ function setEntryValue(entry, value) {
1535
+ entry.value = value;
1536
+ }
1537
+ function locateEntry(ctx, target) {
1538
+ const entry = stateHost(ctx, target.scope)?.find((s) => s.name === target.state);
1539
+ if (entry === void 0)
1540
+ throw new Error(
1541
+ `Op target ${target.scope}:"${target.state}" has no resolved state entry on this instance \u2014 the deployed definition and the instance state have diverged`
1542
+ );
1543
+ return entry;
1544
+ }
1545
+ function stateHost(ctx, scope) {
1546
+ if (scope === "workflow") return ctx.mutation.state;
1547
+ const stageEntry = findOpenStageEntry(ctx.mutation);
1548
+ if (scope === "stage") return stageEntry?.state;
1549
+ const task = stageEntry?.tasks.find((t) => t.name === ctx.taskName);
1550
+ return task !== void 0 && task.state === void 0 && (task.state = []), task?.state;
1551
+ }
1552
+ function resolveOpSource(src, ctx) {
1553
+ const staticValue = resolveStaticSource(src, ctx);
1554
+ if (staticValue.handled) return staticValue.value;
1555
+ switch (src.type) {
1556
+ case "self":
1557
+ return ctx.self;
1558
+ case "stage":
1559
+ return ctx.stage;
1560
+ case "stateRead": {
1561
+ const value = readEntryFromMutation(ctx, src);
1562
+ return src.path !== void 0 ? getPath(value, src.path) : value;
1563
+ }
1564
+ case "object": {
1565
+ const out = {};
1566
+ for (const [field, fieldSrc] of Object.entries(src.fields))
1567
+ out[field] = resolveOpSource(fieldSrc, ctx);
1568
+ return out;
1569
+ }
1570
+ default:
1571
+ throw new Error(`Source type "${src.type}" is a state-entry origin, not a value`);
1572
+ }
1573
+ }
1574
+ function entryValue(entries, name) {
1575
+ return entries?.find((s) => s.name === name)?.value;
1576
+ }
1577
+ function readEntryFromMutation(ctx, src) {
1578
+ const scopes = src.scope !== void 0 ? [src.scope] : ["stage", "workflow"], stageEntry = findOpenStageEntry(ctx.mutation);
1579
+ for (const scope of scopes) {
1580
+ const host = scope === "workflow" ? ctx.mutation.state : stageEntry?.state, value = entryValue(host, src.state);
1581
+ if (value !== void 0) return value;
1582
+ }
1583
+ }
1584
+ function evalOpPredicate(p, row, ctx) {
1585
+ switch (p.type) {
1513
1586
  case "all":
1514
- return terminalCount === refs.length;
1587
+ return p.of.every((sub) => evalOpPredicate(sub, row, ctx));
1515
1588
  case "any":
1516
- return terminalCount >= 1;
1517
- case "count":
1518
- return terminalCount >= policy.n;
1589
+ return p.of.some((sub) => evalOpPredicate(sub, row, ctx));
1590
+ case "field":
1591
+ return row[p.field] === resolveOpSource(p.equals, ctx);
1519
1592
  }
1520
1593
  }
1521
1594
  async function invokeTask(args) {
1522
- const { client, instanceId, taskId, options } = args, ctx = await loadContext(client, instanceId, {
1595
+ const { client, instanceId, task: taskName, options } = args, ctx = await loadContext(client, instanceId, {
1523
1596
  ...options?.clock ? { clock: options.clock } : {}
1524
1597
  });
1525
- return commitInvoke(ctx, taskId, options?.actor);
1598
+ return commitInvoke(ctx, taskName, options?.actor);
1526
1599
  }
1527
- async function commitInvoke(ctx, taskId, actor) {
1528
- const { task } = findTaskInCurrentStage(ctx, taskId), entry = findCurrentTasks(ctx.instance).find((t) => t.id === taskId);
1600
+ async function commitInvoke(ctx, taskName, actor) {
1601
+ const { task } = findTaskInCurrentStage(ctx, taskName), entry = findCurrentTasks(ctx.instance).find((t) => t.name === taskName);
1529
1602
  if (entry === void 0)
1530
- throw new Error(`Task "${taskId}" has no status entry on instance ${ctx.instance._id}`);
1603
+ throw new Error(`Task "${taskName}" has no status entry on instance ${ctx.instance._id}`);
1531
1604
  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;
1605
+ return { invoked: !1, task: taskName };
1606
+ const mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskName);
1607
+ return await activateTask(ctx, mutation, task, mutEntry, actor), await persist(ctx, mutation), { invoked: !0, task: taskName };
1554
1608
  }
1555
1609
  async function activateTask(ctx, mutation, task, entry, actor) {
1556
1610
  const now = ctx.now;
1557
1611
  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({
1612
+ const stage = findStage(ctx.definition, mutation.currentStage);
1613
+ entry.state = await resolveTaskStateEntries({
1560
1614
  client: ctx.client,
1561
1615
  instance: ctx.instance,
1562
1616
  stage,
@@ -1564,55 +1618,73 @@ async function activateTask(ctx, mutation, task, entry, actor) {
1564
1618
  now
1565
1619
  });
1566
1620
  }
1567
- if (!await autoResolveOnActivate(ctx, mutation, task, entry, now) && (mutation.history.push({
1621
+ runOps({
1622
+ ops: task.ops,
1623
+ mutation,
1624
+ stage: mutation.currentStage,
1625
+ origin: { task: task.name },
1626
+ params: {},
1627
+ actor,
1628
+ self: selfGdr(ctx.instance),
1629
+ now
1630
+ }), mutation.history.push({
1568
1631
  _key: randomKey(),
1569
- _type: "workflow.history.taskActivated",
1632
+ _type: "taskActivated",
1570
1633
  at: now,
1571
- stageId: mutation.currentStageId,
1572
- taskId: task.id,
1573
- ...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;
1634
+ stage: mutation.currentStage,
1635
+ task: task.name,
1636
+ ...actor !== void 0 ? { actor } : {}
1637
+ }), await queueEffects(ctx, mutation, task.effects, { kind: "task", name: task.name }, actor, {
1638
+ taskName: task.name
1639
+ });
1640
+ let pendingChildren;
1641
+ if (task.subworkflows !== void 0) {
1642
+ const createsBefore = mutation.pendingCreates.length, refs = await spawnSubworkflows(ctx, mutation, task, task.subworkflows, actor);
1643
+ entry.spawnedInstances = refs, pendingChildren = mutation.pendingCreates.slice(createsBefore).map((c) => ({ _id: c._id, stage: c.currentStage, status: "active" }));
1586
1644
  for (const ref of refs)
1587
1645
  mutation.history.push({
1588
1646
  _key: randomKey(),
1589
- _type: "workflow.history.spawned",
1647
+ _type: "spawned",
1590
1648
  at: now,
1591
- taskId: task.id,
1649
+ task: task.name,
1592
1650
  instanceRef: ref
1593
1651
  });
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
1652
  }
1653
+ await autoResolveOnActivate(ctx, mutation, task, entry, now, pendingChildren) || resolveMachineStep(mutation, task, entry, now);
1654
+ }
1655
+ async function autoResolveOnActivate(ctx, mutation, task, entry, now, pendingChildren) {
1656
+ if (task.failWhen === void 0 && effectiveCompleteWhen(task) === void 0) return !1;
1657
+ const vars = pendingChildren !== void 0 ? { subworkflows: pendingChildren } : await taskConditionVars(ctx, entry), outcome = await evaluateResolutionGate(ctx, task, vars);
1658
+ return outcome === void 0 ? !1 : (applyTaskStatusChange({
1659
+ entry,
1660
+ history: mutation.history,
1661
+ stage: mutation.currentStage,
1662
+ to: outcome,
1663
+ at: now,
1664
+ actor: gateActor(outcome)
1665
+ }), !0);
1666
+ }
1667
+ async function taskConditionVars(ctx, entry) {
1668
+ return entry.spawnedInstances === void 0 || entry.spawnedInstances.length === 0 ? { subworkflows: [] } : { subworkflows: await subworkflowRows(ctx, entry.spawnedInstances) };
1669
+ }
1670
+ function resolveMachineStep(mutation, task, entry, now) {
1671
+ const waitsForActor = task.actions !== void 0 && task.actions.length > 0, waitsForCondition = task.completeWhen !== void 0 || task.subworkflows !== void 0;
1672
+ waitsForActor || waitsForCondition || applyTaskStatusChange({
1673
+ entry,
1674
+ history: mutation.history,
1675
+ stage: mutation.currentStage,
1676
+ to: "done",
1677
+ at: now,
1678
+ actor: { kind: "system", id: "engine.machineStep" }
1679
+ });
1608
1680
  }
1609
1681
  async function buildStageTasks(ctx, stage) {
1610
1682
  const entries = [];
1611
1683
  for (const task of stage.tasks ?? []) {
1612
- const inScope = await ctxEvaluateFilter(ctx, task.filter);
1684
+ const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
1613
1685
  entries.push({
1614
1686
  _key: randomKey(),
1615
- id: task.id,
1687
+ name: task.name,
1616
1688
  status: inScope ? "pending" : "skipped",
1617
1689
  ...task.filter !== void 0 ? {
1618
1690
  filterEvaluation: {
@@ -1624,22 +1696,22 @@ async function buildStageTasks(ctx, stage) {
1624
1696
  }
1625
1697
  return entries;
1626
1698
  }
1627
- function activatesOnStageEnter(task) {
1628
- return task.invokeOn === "stageEnter" || task.spawns !== void 0 || task.completeWhen !== void 0 || task.failWhen !== void 0;
1699
+ function isTerminalStage(stage) {
1700
+ return (stage.transitions ?? []).length === 0;
1629
1701
  }
1630
1702
  async function setStage(args) {
1631
- const { client, instanceId, targetStageId, reason, options } = args, ctx = await loadContext(client, instanceId, {
1703
+ const { client, instanceId, targetStage, reason, options } = args, ctx = await loadContext(client, instanceId, {
1632
1704
  ...options?.clock ? { clock: options.clock } : {}
1633
1705
  });
1634
- return commitSetStage(ctx, targetStageId, reason, options?.actor);
1706
+ return commitSetStage(ctx, targetStage, reason, options?.actor);
1635
1707
  }
1636
1708
  async function enterStage(ctx, mutation, nextStage, actor, at) {
1637
- mutation.currentStageId = nextStage.id;
1709
+ mutation.currentStage = nextStage.name;
1638
1710
  const nextStageEntry = {
1639
1711
  _key: randomKey(),
1640
- id: nextStage.id,
1712
+ name: nextStage.name,
1641
1713
  enteredAt: at,
1642
- state: await resolveStageStateSlots({
1714
+ state: await resolveStageStateEntries({
1643
1715
  client: ctx.client,
1644
1716
  instance: ctx.instance,
1645
1717
  stage: nextStage,
@@ -1647,55 +1719,44 @@ async function enterStage(ctx, mutation, nextStage, actor, at) {
1647
1719
  }),
1648
1720
  tasks: await buildStageTasks(ctx, nextStage)
1649
1721
  };
1650
- mutation.stages.push(nextStageEntry), await queueEffects(
1651
- ctx,
1652
- mutation,
1653
- nextStage.onEnter,
1654
- { kind: "stageEnter", id: nextStage.id },
1655
- actor
1656
- );
1722
+ mutation.stages.push(nextStageEntry);
1657
1723
  for (const task of nextStage.tasks ?? []) {
1658
- if (!activatesOnStageEnter(task)) continue;
1659
- const entry = nextStageEntry.tasks.find((t) => t.id === task.id);
1724
+ if (task.activation !== "auto") continue;
1725
+ const entry = nextStageEntry.tasks.find((t) => t.name === task.name);
1660
1726
  entry && entry.status === "pending" && await activateTask(ctx, mutation, task, entry, actor);
1661
1727
  }
1662
- nextStage.kind === "terminal" && (mutation.completedAt = at);
1728
+ isTerminalStage(nextStage) && (mutation.completedAt = at);
1663
1729
  }
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 };
1730
+ function isTerminal(ctx) {
1731
+ return ctx.instance.completedAt !== void 0;
1673
1732
  }
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)
1733
+ async function commitSetStage(ctx, targetStage, reason, actor) {
1734
+ if (isTerminal(ctx))
1677
1735
  return { fired: !1 };
1678
- const { mutation, at } = await beginStageMove(ctx, currentStage, actor), transitionId = `setStage:${currentStage.id}->${nextStage.id}`;
1736
+ const currentStage = findStage(ctx.definition, ctx.instance.currentStage), nextStage = findStage(ctx.definition, targetStage);
1737
+ if (currentStage.name === nextStage.name)
1738
+ return { fired: !1 };
1739
+ const mutation = startMutation(ctx.instance), at = ctx.now, transitionName = `setStage:${currentStage.name}->${nextStage.name}`;
1679
1740
  mutation.history.push(
1680
1741
  ...stageTransitionHistory({
1681
- fromStageId: currentStage.id,
1682
- toStageId: nextStage.id,
1742
+ fromStage: currentStage.name,
1743
+ toStage: nextStage.name,
1683
1744
  at,
1684
- transitionId,
1745
+ transition: transitionName,
1685
1746
  via: "setStage",
1686
1747
  ...actor !== void 0 ? { actor } : {},
1687
1748
  ...reason !== void 0 ? { reason } : {}
1688
1749
  })
1689
1750
  );
1690
1751
  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), {
1752
+ return priorEntry !== void 0 && (priorEntry.exitedAt = at), await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.name, nextStage.name), {
1692
1753
  fired: !0,
1693
- fromStageId: currentStage.id,
1694
- toStageId: nextStage.id,
1695
- transitionId
1754
+ fromStage: currentStage.name,
1755
+ toStage: nextStage.name,
1756
+ transition: transitionName
1696
1757
  };
1697
1758
  }
1698
- async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1759
+ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
1699
1760
  await persistThenDeploy(
1700
1761
  ctx,
1701
1762
  mutation,
@@ -1704,7 +1765,7 @@ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1704
1765
  clientForGdr: ctx.clientForGdr,
1705
1766
  instance: materializeInstance(ctx.instance, mutation),
1706
1767
  definition: ctx.definition,
1707
- stageId: enteredStageId,
1768
+ stageName: enteredStage,
1708
1769
  now: ctx.now
1709
1770
  })
1710
1771
  ), await retractStageGuards({
@@ -1712,7 +1773,7 @@ async function persistStageMove(ctx, mutation, exitedStageId, enteredStageId) {
1712
1773
  clientForGdr: ctx.clientForGdr,
1713
1774
  instance: ctx.instance,
1714
1775
  definition: ctx.definition,
1715
- stageId: exitedStageId,
1776
+ stageName: exitedStage,
1716
1777
  now: ctx.now
1717
1778
  });
1718
1779
  }
@@ -1728,53 +1789,63 @@ async function persistThenDeploy(ctx, mutation, deploy) {
1728
1789
  deploy
1729
1790
  });
1730
1791
  }
1731
- async function refreshStageGuards(ctx, mutation, stageId) {
1792
+ async function refreshStageGuards(ctx, mutation, stageName) {
1732
1793
  await deployStageGuards({
1733
1794
  client: ctx.client,
1734
1795
  clientForGdr: ctx.clientForGdr,
1735
1796
  instance: materializeInstance(ctx.instance, mutation),
1736
1797
  definition: ctx.definition,
1737
- stageId,
1798
+ stageName,
1738
1799
  now: ctx.now
1739
1800
  });
1740
1801
  }
1741
- async function commitTransition(ctx, action, actor) {
1742
- const currentStage = findStage(ctx.definition, ctx.instance.currentStageId), transition = await pickTransition(ctx, currentStage);
1802
+ async function commitTransition(ctx, actor) {
1803
+ if (isTerminal(ctx))
1804
+ return { fired: !1 };
1805
+ const currentStage = findStage(ctx.definition, ctx.instance.currentStage), transition = await pickTransition(ctx, currentStage);
1743
1806
  if (transition === void 0)
1744
1807
  return { fired: !1 };
1745
- const { mutation, at } = await beginStageMove(ctx, currentStage, actor);
1746
- await queueEffects(
1808
+ const mutation = startMutation(ctx.instance), at = ctx.now;
1809
+ runOps({
1810
+ ops: transition.ops,
1811
+ mutation,
1812
+ stage: currentStage.name,
1813
+ origin: { transition: transition.name },
1814
+ params: {},
1815
+ actor,
1816
+ self: selfGdr(ctx.instance),
1817
+ now: at
1818
+ }), await queueEffects(
1747
1819
  ctx,
1748
1820
  mutation,
1749
1821
  transition.effects,
1750
1822
  {
1751
1823
  kind: "transition",
1752
- id: transition.id ?? `${currentStage.id}->${transition.to}`
1824
+ name: transition.name
1753
1825
  },
1754
1826
  actor
1755
1827
  ), mutation.history.push(
1756
1828
  ...stageTransitionHistory({
1757
- fromStageId: currentStage.id,
1758
- toStageId: transition.to,
1829
+ fromStage: currentStage.name,
1830
+ toStage: transition.to,
1759
1831
  at,
1760
- ...transition.id !== void 0 ? { transitionId: transition.id } : {},
1832
+ transition: transition.name,
1761
1833
  ...actor !== void 0 ? { actor } : {}
1762
1834
  })
1763
1835
  );
1764
1836
  const priorEntry = findOpenStageEntry(mutation);
1765
1837
  priorEntry !== void 0 && (priorEntry.exitedAt = at);
1766
1838
  const nextStage = findStage(ctx.definition, transition.to);
1767
- return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.id, nextStage.id), {
1839
+ return await enterStage(ctx, mutation, nextStage, actor, at), await persistStageMove(ctx, mutation, currentStage.name, nextStage.name), {
1768
1840
  fired: !0,
1769
- fromStageId: currentStage.id,
1770
- toStageId: transition.to,
1771
- ...transition.id !== void 0 ? { transitionId: transition.id } : {}
1841
+ fromStage: currentStage.name,
1842
+ toStage: transition.to,
1843
+ transition: transition.name
1772
1844
  };
1773
1845
  }
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;
1846
+ async function pickTransition(ctx, stage) {
1847
+ for (const candidate of stage.transitions ?? [])
1848
+ if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
1778
1849
  }
1779
1850
  async function evaluateAutoTransitions(args) {
1780
1851
  const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
@@ -1782,58 +1853,27 @@ async function evaluateAutoTransitions(args) {
1782
1853
  ...clock ? { clock } : {},
1783
1854
  ...overlay ? { overlay } : {}
1784
1855
  });
1785
- return commitTransition(ctx, void 0, options?.actor);
1856
+ return commitTransition(ctx, options?.actor);
1786
1857
  }
1787
1858
  async function primeInitialStage(client, instanceId, actor, clientForGdr, clock) {
1788
1859
  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);
1860
+ if (!instance || instance.stages.length > 0) return;
1861
+ const definition = parseDefinitionSnapshot(instance), stage = definition.stages.find((s) => s.name === instance.currentStage);
1791
1862
  if (stage === void 0) return;
1792
- const now = (clock ?? wallClock)(), snapshot = await hydrateSnapshot({
1863
+ const ctx = await buildEngineContext({
1793
1864
  client,
1794
1865
  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 = {
1866
+ instance,
1867
+ definition,
1868
+ ...clock ? { clock } : {}
1869
+ }), now = ctx.now, initialStageEntry = {
1811
1870
  _key: randomKey(),
1812
- id: stage.id,
1871
+ name: stage.name,
1813
1872
  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({
1873
+ state: await resolveStageStateEntries({ client, instance, stage, now }),
1874
+ tasks: await buildStageTasks(ctx, stage)
1875
+ }, committed = await client.patch(instance._id).set({
1834
1876
  stages: [initialStageEntry],
1835
- pendingEffects,
1836
- history: newHistory,
1837
1877
  lastChangedAt: now
1838
1878
  }).ifRevisionId(instance._rev).commit();
1839
1879
  await deployOrRollback({
@@ -1842,7 +1882,6 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1842
1882
  committedRev: committed._rev,
1843
1883
  restore: {
1844
1884
  stages: instance.stages,
1845
- pendingEffects: instance.pendingEffects,
1846
1885
  history: instance.history
1847
1886
  },
1848
1887
  reversible: !0,
@@ -1851,7 +1890,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1851
1890
  clientForGdr: clientForGdr ?? (() => client),
1852
1891
  instance,
1853
1892
  definition,
1854
- stageId: stage.id,
1893
+ stageName: stage.name,
1855
1894
  now
1856
1895
  })
1857
1896
  }), await autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock);
@@ -1859,7 +1898,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
1859
1898
  async function autoActivatePrimedTasks(client, instanceId, definition, stage, actor, clientForGdr, clock) {
1860
1899
  const resolvedClientForGdr = clientForGdr ?? (() => client);
1861
1900
  for (const task of stage.tasks ?? []) {
1862
- if (!activatesOnStageEnter(task)) continue;
1901
+ if (task.activation !== "auto") continue;
1863
1902
  const updated = await client.getDocument(instanceId);
1864
1903
  if (!updated) continue;
1865
1904
  const updatedCtx = await buildEngineContext({
@@ -1868,21 +1907,19 @@ async function autoActivatePrimedTasks(client, instanceId, definition, stage, ac
1868
1907
  instance: updated,
1869
1908
  definition,
1870
1909
  ...clock ? { clock } : {}
1871
- }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.id === task.id);
1910
+ }), mutation = startMutation(updated), mutEntry = currentTasks(mutation).find((t) => t.name === task.name);
1872
1911
  mutEntry && mutEntry.status === "pending" && (await activateTask(updatedCtx, mutation, task, mutEntry, actor), await persist(updatedCtx, mutation));
1873
1912
  }
1874
1913
  }
1875
1914
  const CASCADE_LIMIT = 100;
1876
1915
  async function gatherAutoResolveCandidates(ctx, tasks) {
1877
1916
  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
- }
1917
+ for (const task of tasks) {
1918
+ const entry = currentTasksList.find((e) => e.name === task.name);
1919
+ if (entry?.status !== "active") continue;
1920
+ const outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
1921
+ outcome !== void 0 && candidates.push({ task, outcome });
1922
+ }
1886
1923
  return candidates;
1887
1924
  }
1888
1925
  async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, overlay) {
@@ -1890,31 +1927,24 @@ async function resolveCompleteWhen(client, instanceId, clientForGdr, clock, over
1890
1927
  ...clientForGdr ? { clientForGdr } : {},
1891
1928
  ...clock ? { clock } : {},
1892
1929
  ...overlay ? { overlay } : {}
1893
- }), stage = findStage(ctx.definition, ctx.instance.currentStageId), tasksWithAutoPredicate = (stage.tasks ?? []).filter(
1894
- (t) => t.completeWhen !== void 0 || t.failWhen !== void 0
1930
+ }), stage = findStage(ctx.definition, ctx.instance.currentStage), gatedTasks = (stage.tasks ?? []).filter(
1931
+ (t) => effectiveCompleteWhen(t) !== void 0 || t.failWhen !== void 0
1895
1932
  );
1896
- if (tasksWithAutoPredicate.length === 0) return 0;
1897
- const candidates = await gatherAutoResolveCandidates(ctx, tasksWithAutoPredicate);
1933
+ if (gatedTasks.length === 0) return 0;
1934
+ const candidates = await gatherAutoResolveCandidates(ctx, gatedTasks);
1898
1935
  if (candidates.length === 0) return 0;
1899
1936
  const mutation = startMutation(ctx.instance);
1900
1937
  let count = 0;
1901
1938
  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++;
1939
+ const mutEntry = currentTasks(mutation).find((t) => t.name === task.name);
1940
+ mutEntry === void 0 || mutEntry.status !== "active" || (applyTaskStatusChange({
1941
+ entry: mutEntry,
1942
+ history: mutation.history,
1943
+ stage: stage.name,
1944
+ to: outcome,
1945
+ at: ctx.now,
1946
+ actor: gateActor(outcome)
1947
+ }), count++);
1918
1948
  }
1919
1949
  return count > 0 && await persist(ctx, mutation), count;
1920
1950
  }
@@ -1933,43 +1963,276 @@ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, c
1933
1963
  throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
1934
1964
  }
1935
1965
  }
1936
- async function findSatisfiedParentSpawn(client, child) {
1966
+ async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
1937
1967
  const parentRef = child.ancestors.at(-1);
1938
1968
  if (parentRef === void 0) return;
1939
1969
  const parent = await client.getDocument(gdrToBareId(parentRef.id));
1940
1970
  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);
1971
+ const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
1942
1972
  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 };
1973
+ 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);
1974
+ if (task?.subworkflows === void 0) return;
1975
+ const entry = parentCurrentTasks.find((e) => e.name === task.name);
1976
+ if (entry === void 0 || entry.status !== "active") return;
1977
+ const ctx = await buildEngineContext({
1978
+ client,
1979
+ clientForGdr,
1980
+ instance: parent,
1981
+ definition,
1982
+ ...clock ? { clock } : {}
1983
+ }), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
1984
+ if (outcome !== void 0)
1985
+ return { parent, definition, stage, task, outcome };
1948
1986
  }
1949
1987
  async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
1950
1988
  const instance = await client.getDocument(instanceId);
1951
1989
  if (!instance) return;
1952
- const found = await findSatisfiedParentSpawn(client, instance);
1990
+ const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
1953
1991
  if (found === void 0) return;
1954
- const { parent, definition, stage, task } = found, ctx = await buildEngineContext({
1992
+ const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
1955
1993
  client,
1956
- clientForGdr: clientForGdr ?? (() => client),
1994
+ clientForGdr: resolvedClientForGdr,
1957
1995
  instance: parent,
1958
1996
  definition,
1959
1997
  ...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);
1998
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
1999
+ mutEntry !== void 0 && (applyTaskStatusChange({
2000
+ entry: mutEntry,
2001
+ history: mutation.history,
2002
+ stage: stage.name,
2003
+ to: outcome,
2004
+ at: ctx.now,
2005
+ actor: gateActor(outcome)
2006
+ }), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
2007
+ }
2008
+ function startMutation(instance) {
2009
+ return {
2010
+ currentStage: instance.currentStage,
2011
+ // Deep-ish copy. Per-scope state entry arrays need their own copies
2012
+ // so ops can mutate without leaking into the source.
2013
+ state: (instance.state ?? []).map((s) => ({ ...s })),
2014
+ stages: instance.stages.map((s) => ({
2015
+ ...s,
2016
+ state: (s.state ?? []).map((entry) => ({ ...entry })),
2017
+ tasks: s.tasks.map((t) => ({
2018
+ ...t,
2019
+ ...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
2020
+ }))
2021
+ })),
2022
+ pendingEffects: [...instance.pendingEffects],
2023
+ effectHistory: [...instance.effectHistory],
2024
+ effectsContext: [...instance.effectsContext],
2025
+ history: [...instance.history],
2026
+ lastChangedAt: instance.lastChangedAt,
2027
+ ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
2028
+ ...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
2029
+ pendingCreates: []
2030
+ };
2031
+ }
2032
+ function instanceStateFields(src) {
2033
+ const fields = {
2034
+ currentStage: src.currentStage,
2035
+ state: src.state,
2036
+ stages: src.stages,
2037
+ pendingEffects: src.pendingEffects,
2038
+ effectHistory: src.effectHistory,
2039
+ effectsContext: src.effectsContext,
2040
+ history: src.history
2041
+ };
2042
+ return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
2043
+ }
2044
+ function materializeInstance(base, mutation) {
2045
+ return {
2046
+ ...base,
2047
+ currentStage: mutation.currentStage,
2048
+ state: mutation.state,
2049
+ stages: mutation.stages
2050
+ };
2051
+ }
2052
+ async function persist(ctx, mutation) {
2053
+ const set = {
2054
+ ...instanceStateFields(mutation),
2055
+ lastChangedAt: ctx.now
2056
+ }, pendingCreates = mutation.pendingCreates;
2057
+ if (pendingCreates.length === 0)
2058
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
2059
+ const tx = ctx.client.transaction();
2060
+ for (const body of pendingCreates) tx.create(body);
2061
+ tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
2062
+ const actorForPriming = void 0;
2063
+ for (const body of pendingCreates)
2064
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
2065
+ const reloaded = await ctx.client.getDocument(ctx.instance._id);
2066
+ if (!reloaded)
2067
+ throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
2068
+ return reloaded;
2069
+ }
2070
+ function currentStageEntry(mutation) {
2071
+ const entry = findOpenStageEntry(mutation);
2072
+ if (entry === void 0)
2073
+ throw new Error(
2074
+ `Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
2075
+ );
2076
+ return entry;
2077
+ }
2078
+ function currentTasks(mutation) {
2079
+ return currentStageEntry(mutation).tasks;
2080
+ }
2081
+ function findTaskInCurrentStage(ctx, taskName) {
2082
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
2083
+ if (task === void 0)
2084
+ throw new Error(
2085
+ `Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
2086
+ );
2087
+ return { stage, task };
2088
+ }
2089
+ function requireMutationTaskEntry(mutation, task) {
2090
+ const mutEntry = currentTasks(mutation).find((t) => t.name === task);
2091
+ if (mutEntry === void 0)
2092
+ throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
2093
+ return mutEntry;
2094
+ }
2095
+ function findCurrentStageEntry(instance) {
2096
+ return findOpenStageEntry(instance);
2097
+ }
2098
+ function findCurrentTasks(instance) {
2099
+ return findCurrentStageEntry(instance)?.tasks ?? [];
2100
+ }
2101
+ async function completeEffect(args) {
2102
+ const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
2103
+ ...options?.clock ? { clock: options.clock } : {}
2104
+ });
2105
+ return commitCompleteEffect(
2106
+ ctx,
2107
+ effectKey,
2108
+ status,
2109
+ outputs,
2110
+ detail,
2111
+ error,
2112
+ durationMs,
2113
+ options?.actor
2114
+ );
2115
+ }
2116
+ function buildEffectHistoryEntry(pending, outcome) {
2117
+ const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
2118
+ return {
2119
+ _key: pending._key,
2120
+ name: pending.name,
2121
+ ...pending.title !== void 0 ? { title: pending.title } : {},
2122
+ ...pending.description !== void 0 ? { description: pending.description } : {},
2123
+ params: pending.params,
2124
+ origin: pending.origin,
2125
+ ...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
2126
+ ranAt,
2127
+ ...durationMs !== void 0 ? { durationMs } : {},
2128
+ status,
2129
+ ...detail !== void 0 ? { detail } : {},
2130
+ ...error !== void 0 ? { error } : {},
2131
+ ...outputs !== void 0 ? { outputs } : {}
2132
+ };
2133
+ }
2134
+ function upsertEffectsContext(mutation, effectName, outputs) {
2135
+ const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
2136
+ existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
2137
+ }
2138
+ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
2139
+ const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
2140
+ if (pending === void 0)
2141
+ throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
2142
+ const mutation = startMutation(ctx.instance);
2143
+ mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
2144
+ const ranAt = ctx.now;
2145
+ return mutation.effectHistory.push(
2146
+ buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
2147
+ ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
2148
+ _key: randomKey(),
2149
+ _type: "effectCompleted",
2150
+ at: ranAt,
2151
+ effectKey,
2152
+ effect: pending.name,
2153
+ status,
2154
+ ...outputs !== void 0 ? { outputs } : {},
2155
+ ...detail !== void 0 ? { detail } : {},
2156
+ ...actor !== void 0 ? { actor } : {}
2157
+ }), await persist(ctx, mutation), { effectKey, status };
2158
+ }
2159
+ function buildQueuedEffect(effect, origin, params, actor, now) {
2160
+ const key = randomKey(), pending = {
2161
+ _key: key,
2162
+ _type: "pendingEffect",
2163
+ name: effect.name,
2164
+ ...effect.title !== void 0 ? { title: effect.title } : {},
2165
+ ...effect.description !== void 0 ? { description: effect.description } : {},
2166
+ ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
2167
+ params,
2168
+ origin,
2169
+ ...actor !== void 0 ? { actor } : {},
2170
+ queuedAt: now
2171
+ }, history = {
2172
+ _key: randomKey(),
2173
+ _type: "effectQueued",
2174
+ at: now,
2175
+ effectKey: key,
2176
+ effect: effect.name,
2177
+ origin
2178
+ };
2179
+ return { pending, history };
2180
+ }
2181
+ async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
2182
+ if (!effects || effects.length === 0) return;
2183
+ const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
2184
+ ...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
2185
+ ...actor !== void 0 ? { actor } : {},
2186
+ // Caller-supplied action params, readable in bindings as `$params.<name>`.
2187
+ vars: { params: opts?.callerParams ?? {} }
2188
+ });
2189
+ for (const effect of effects) {
2190
+ const resolved = await resolveBindings({
2191
+ bindings: effect.bindings,
2192
+ staticInput: effect.input,
2193
+ snapshot: ctx.snapshot,
2194
+ params
2195
+ }), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
2196
+ mutation.pendingEffects.push(pending), mutation.history.push(history);
2197
+ }
2198
+ }
2199
+ async function abortInstance(args) {
2200
+ const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
2201
+ ...options?.clock ? { clock: options.clock } : {}
2202
+ });
2203
+ return commitAbort(ctx, reason, options?.actor);
2204
+ }
2205
+ async function commitAbort(ctx, reason, actor) {
2206
+ if (ctx.instance.completedAt !== void 0)
2207
+ return { fired: !1 };
2208
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
2209
+ return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
2210
+ ...mutation.pendingEffects.map(
2211
+ (pending) => buildEffectHistoryEntry(pending, {
2212
+ status: "failed",
2213
+ ranAt: at,
2214
+ actor,
2215
+ detail: "instance aborted before dispatch",
2216
+ error: void 0,
2217
+ durationMs: void 0,
2218
+ outputs: void 0
2219
+ })
2220
+ )
2221
+ ), mutation.pendingEffects = [], mutation.history.push({
2222
+ _key: randomKey(),
2223
+ _type: "aborted",
2224
+ at,
2225
+ stage: stage.name,
2226
+ ...reason !== void 0 ? { reason } : {},
2227
+ ...actor !== void 0 ? { actor } : {}
2228
+ }), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
2229
+ client: ctx.client,
2230
+ clientForGdr: ctx.clientForGdr,
2231
+ instance: ctx.instance,
2232
+ definition: ctx.definition,
2233
+ stageName: stage.name,
2234
+ now: ctx.now
2235
+ }), { fired: !0, stage: stage.name };
1973
2236
  }
1974
2237
  const parsedFilters = /* @__PURE__ */ new Map();
1975
2238
  async function matchesFilter(args) {
@@ -2055,17 +2318,103 @@ async function fetchActor(requestFn) {
2055
2318
  ...roleNames.length > 0 ? { roles: roleNames } : {}
2056
2319
  };
2057
2320
  }
2058
- async function fetchGrantsCached(requestFn, resourcePath) {
2321
+ async function fetchGrantsCached(requestFn, resourcePath) {
2322
+ try {
2323
+ return await fetchGrants({ client: { request: requestFn }, resourcePath });
2324
+ } catch (err) {
2325
+ console.warn(
2326
+ `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2327
+ );
2328
+ return;
2329
+ }
2330
+ }
2331
+ const resourceKey = (r) => `${r.type}.${r.id}`;
2332
+ function guardsForResource(client) {
2333
+ return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2334
+ }
2335
+ function instanceGuardQuery(instanceId) {
2336
+ return {
2337
+ query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
2338
+ params: { guardType: GUARD_DOC_TYPE, instanceId }
2339
+ };
2340
+ }
2341
+ function verdictGuardsForInstance(client, instanceId) {
2342
+ const { query, params } = instanceGuardQuery(instanceId);
2343
+ return client.fetch(query, params);
2344
+ }
2345
+ async function guardsForInstance(args) {
2346
+ const { client, clientForGdr, instance } = args, stateUris = [
2347
+ ...collectEntryDocUris(instance.state),
2348
+ ...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
2349
+ ], clients = resourceClientMap(
2350
+ instance.workflowResource,
2351
+ client,
2352
+ clientForGdr,
2353
+ stateUris.map((uri) => tryParseGdr(uri))
2354
+ ), { query, params } = instanceGuardQuery(instance._id);
2355
+ return queryGuardsAcross(clients.values(), query, params);
2356
+ }
2357
+ async function guardsForDefinition(args) {
2358
+ const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
2359
+ (def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
2360
+ ), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs);
2361
+ return queryGuardsAcross(clients.values(), "*[_type == $t && sourceDefinition == $definition]", {
2362
+ t: GUARD_DOC_TYPE,
2363
+ definition
2364
+ });
2365
+ }
2366
+ function guardIdRefs(definition) {
2367
+ return definition.stages.flatMap(
2368
+ (stage) => (stage.guards ?? []).flatMap(
2369
+ (guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
2370
+ )
2371
+ );
2372
+ }
2373
+ function staticIdRefGdr(idRef, stage, definition) {
2374
+ const read = /^\$state\.([\w-]+)/.exec(idRef);
2375
+ if (read === null) return;
2376
+ const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
2377
+ return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
2378
+ }
2379
+ function entriesInScope(stage, definition) {
2380
+ return [
2381
+ ...definition.state ?? [],
2382
+ ...stage.state ?? [],
2383
+ ...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
2384
+ ];
2385
+ }
2386
+ function gdrFromValue(value) {
2387
+ if (typeof value == "string") return tryParseGdr(value);
2388
+ if (typeof value == "object" && value !== null && "id" in value) {
2389
+ const id = value.id;
2390
+ return typeof id == "string" ? tryParseGdr(id) : void 0;
2391
+ }
2392
+ }
2393
+ function tryParseGdr(uri) {
2059
2394
  try {
2060
- return await fetchGrants({ client: { request: requestFn }, resourcePath });
2061
- } catch (err) {
2062
- console.warn(
2063
- `workflow: failed to fetch grants from "${resourcePath}"; skipping permission gate. Original error: ${err instanceof Error ? err.message : String(err)}`
2064
- );
2395
+ return parseGdr(uri);
2396
+ } catch {
2065
2397
  return;
2066
2398
  }
2067
2399
  }
2068
- const WILDCARD_ROLE = "*";
2400
+ function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
2401
+ const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
2402
+ for (const parsed of gdrs) {
2403
+ if (parsed === void 0) continue;
2404
+ const key = resourceKey(resourceFromParsed(parsed));
2405
+ map.has(key) || map.set(key, clientForGdr(parsed));
2406
+ }
2407
+ return map;
2408
+ }
2409
+ async function queryGuardsAcross(clients, query, params) {
2410
+ const perResource = await Promise.all(
2411
+ [...clients].map((c) => c.fetch(query, params))
2412
+ );
2413
+ return dedupById(perResource.flat());
2414
+ }
2415
+ function dedupById(guards) {
2416
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2417
+ }
2069
2418
  async function evaluateInstance(args) {
2070
2419
  const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2071
2420
  validateTags(tags);
@@ -2077,45 +2426,45 @@ async function evaluateInstance(args) {
2077
2426
  throw new Error(`Workflow instance "${instanceId}" not found`);
2078
2427
  if (!tags.some((t) => instance.tags?.includes(t)))
2079
2428
  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 });
2429
+ 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
2430
  return evaluateFromSnapshot({
2082
2431
  instance,
2083
2432
  definition,
2084
2433
  actor,
2085
2434
  snapshot,
2435
+ guards,
2086
2436
  now,
2087
2437
  ...grants !== void 0 ? { grants } : {}
2088
2438
  });
2089
2439
  }
2090
2440
  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);
2441
+ const { instance, definition, actor, grants, snapshot } = args, now = args.now ?? wallClock(), stage = definition.stages.find((s) => s.name === instance.currentStage);
2092
2442
  if (stage === void 0)
2093
2443
  throw new Error(
2094
- `Instance "${instance._id}" currentStageId "${instance.currentStageId}" not in definition`
2444
+ `Instance "${instance._id}" currentStage "${instance.currentStage}" not in definition`
2095
2445
  );
2096
- const currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], taskEvaluations = [];
2446
+ const scope = await renderScope({ instance, definition, actor, grants, snapshot, now }), currentTaskEntries = findOpenStageEntry(instance)?.tasks ?? [], guardDenial = await instanceGuardReason(instance, actor, args.guards), taskEvaluations = [];
2097
2447
  for (const task of stage.tasks ?? [])
2098
2448
  taskEvaluations.push(
2099
2449
  await evaluateTask({
2100
2450
  task,
2101
- statusEntry: currentTaskEntries.find((t) => t.id === task.id),
2451
+ statusEntry: currentTaskEntries.find((t) => t.name === task.name),
2102
2452
  instance,
2103
- definition,
2104
2453
  actor,
2105
- grants,
2106
2454
  snapshot,
2107
- now
2455
+ scope,
2456
+ stageHasExits: !isTerminalStage(stage),
2457
+ guardDenial
2108
2458
  })
2109
2459
  );
2110
2460
  const transitionEvaluations = [];
2111
2461
  for (const transition of stage.transitions ?? [])
2112
2462
  transitionEvaluations.push({
2113
2463
  transition,
2114
- filterSatisfied: await evaluateFilter({
2115
- filter: transition.filter,
2116
- definition,
2464
+ filterSatisfied: await evaluateCondition({
2465
+ condition: transition.filter,
2117
2466
  snapshot,
2118
- params: buildParams(instance, now)
2467
+ params: scope
2119
2468
  })
2120
2469
  });
2121
2470
  const currentStage = {
@@ -2132,142 +2481,102 @@ async function evaluateFromSnapshot(args) {
2132
2481
  canInteract
2133
2482
  };
2134
2483
  }
2484
+ async function renderScope(args) {
2485
+ const { instance, definition, actor, grants, snapshot, now } = args, params = {
2486
+ ...buildParams({ instance, now, snapshot }),
2487
+ actor,
2488
+ assigned: !1,
2489
+ can: await advisoryCan(instance, actor, grants)
2490
+ };
2491
+ return { ...await evaluatePredicates({
2492
+ predicates: definition.predicates,
2493
+ snapshot,
2494
+ params
2495
+ }), ...params };
2496
+ }
2497
+ async function advisoryCan(instance, actor, grants) {
2498
+ if (grants === void 0) return;
2499
+ const can = {};
2500
+ for (const permission of DOCUMENT_VALUE_PERMISSIONS)
2501
+ can[permission] = await grantsPermissionOn({
2502
+ document: instance,
2503
+ grants,
2504
+ permission,
2505
+ userId: actor.id
2506
+ });
2507
+ return can;
2508
+ }
2135
2509
  async function evaluateTask(args) {
2136
- const { task, statusEntry, instance, definition, actor, grants, snapshot, now } = args, status = statusEntry?.status ?? "pending", actions = [];
2510
+ const { task, statusEntry, instance, actor, snapshot, scope, stageHasExits, guardDenial } = args, status = statusEntry?.status ?? "pending", assigned = assignedFor(instance, task.name, actor), taskScope = {
2511
+ ...scope,
2512
+ state: {
2513
+ ...scope.state,
2514
+ ...scopedStateOverlay(instance, snapshot, task.name)
2515
+ },
2516
+ assigned
2517
+ }, actions = [];
2137
2518
  for (const action of task.actions ?? [])
2138
2519
  actions.push(
2139
2520
  await evaluateAction({
2140
2521
  action,
2141
- task,
2142
- ...statusEntry !== void 0 ? { statusEntry } : {},
2143
2522
  status,
2144
2523
  instance,
2145
- definition,
2146
- actor,
2147
- ...grants !== void 0 ? { grants } : {},
2148
2524
  snapshot,
2149
- now
2525
+ taskScope,
2526
+ stageHasExits,
2527
+ guardDenial
2150
2528
  })
2151
2529
  );
2152
2530
  return {
2153
2531
  task,
2154
2532
  status,
2155
2533
  // "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 ?? []),
2534
+ // the assignee — pending tasks just haven't been activated yet, but
2535
+ // they're still inbox items. The inbox reads the assignees-kind state
2536
+ // entry BY KIND (who-data is state).
2537
+ pendingOnActor: (status === "active" || status === "pending") && assigned,
2159
2538
  actions
2160
2539
  };
2161
2540
  }
2162
2541
  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 };
2542
+ const { action, status, instance, snapshot, taskScope, stageHasExits, guardDenial } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2543
+ return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : action.filter !== void 0 && !await evaluateCondition({
2544
+ condition: action.filter,
2545
+ snapshot,
2546
+ params: taskScope
2547
+ }) ? disabled(action, { kind: "filter-failed", filter: action.filter }) : { action, allowed: !0 };
2167
2548
  }
2168
- function lifecycleReason(instance, definition, status) {
2549
+ async function instanceGuardReason(instance, actor, guards) {
2550
+ if (guards === void 0 || guards.length === 0) return;
2551
+ const denied = await instanceWriteDenials({ instance, guards, identity: actor.id });
2552
+ if (denied.length !== 0)
2553
+ return {
2554
+ kind: "mutation-guard-denied",
2555
+ guardIds: denied.map((g) => g._id),
2556
+ detail: denied.map((g) => g.name ?? g._id).join(", ")
2557
+ };
2558
+ }
2559
+ function lifecycleReason(instance, status, stageHasExits) {
2169
2560
  if (instance.completedAt !== void 0)
2170
2561
  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")
2562
+ if (!stageHasExits)
2563
+ return { kind: "stage-terminal", stage: instance.currentStage };
2564
+ if (isTerminalTaskStatus(status))
2174
2565
  return { kind: "task-not-active", status };
2175
2566
  }
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
2567
  function disabled(action, reason) {
2204
2568
  return { action, allowed: !1, disabledReason: reason };
2205
2569
  }
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
- }
2223
- }
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 });
2227
- }
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());
2250
- }
2251
- function tryParseGdr(uri) {
2252
- try {
2253
- return parseGdr(uri);
2254
- } catch {
2255
- return;
2256
- }
2257
- }
2258
- function dedupById(guards) {
2259
- return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2260
- }
2261
- function definitionDocId(_workflowResource, prefix, workflowId, version) {
2262
- return `${prefix}.${workflowId}.v${version}`;
2570
+ function definitionDocId(_workflowResource, prefix, definition, version) {
2571
+ return `${prefix}.${definition}.v${version}`;
2263
2572
  }
2264
2573
  async function sortByDependencies(client, definitions, tags, workflowResource) {
2265
- const prefix = canonicalTag(tags), byWorkflowId = /* @__PURE__ */ new Map();
2574
+ const prefix = canonicalTag(tags), byName = /* @__PURE__ */ new Map();
2266
2575
  for (const def of definitions) {
2267
- const list = byWorkflowId.get(def.workflowId) ?? [];
2268
- list.push(def), byWorkflowId.set(def.workflowId, list);
2576
+ const list = byName.get(def.name) ?? [];
2577
+ list.push(def), byName.set(def.name, list);
2269
2578
  }
2270
- const findInBatch = (ref) => findRefInBatch(byWorkflowId, ref);
2579
+ const findInBatch = (ref) => findRefInBatch(byName, ref);
2271
2580
  return await assertCrossBatchRefsDeployed(client, definitions, {
2272
2581
  tags,
2273
2582
  workflowResource,
@@ -2275,8 +2584,8 @@ async function sortByDependencies(client, definitions, tags, workflowResource) {
2275
2584
  findInBatch
2276
2585
  }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2277
2586
  }
2278
- function findRefInBatch(byWorkflowId, ref) {
2279
- const candidates = byWorkflowId.get(ref.workflowId);
2587
+ function findRefInBatch(byName, ref) {
2588
+ const candidates = byName.get(ref.name);
2280
2589
  if (!(candidates === void 0 || candidates.length === 0))
2281
2590
  return typeof ref.version == "number" ? candidates.find((c) => c.version === ref.version) : candidates.reduce(
2282
2591
  (best, c) => best === void 0 || c.version > best.version ? c : best,
@@ -2286,7 +2595,7 @@ function findRefInBatch(byWorkflowId, ref) {
2286
2595
  async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2287
2596
  const missing = [];
2288
2597
  for (const def of definitions) {
2289
- const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2598
+ const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
2290
2599
  for (const ref of refsOf(def)) {
2291
2600
  if (ctx.findInBatch(ref) !== void 0) continue;
2292
2601
  const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
@@ -2302,16 +2611,16 @@ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2302
2611
  );
2303
2612
  }
2304
2613
  async function resolveDeployedRefLabel(client, ref, tags) {
2305
- const wantsExplicit = typeof ref.version == "number", params = { workflowId: ref.workflowId, engineTags: tags };
2614
+ const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
2306
2615
  if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2307
2616
  definitionLookupGroq(wantsExplicit),
2308
2617
  params
2309
2618
  ))
2310
- return wantsExplicit ? `${ref.workflowId} v${ref.version}` : ref.workflowId;
2619
+ return wantsExplicit ? `${ref.name} v${ref.version}` : ref.name;
2311
2620
  }
2312
2621
  function topoSortDefinitions(definitions, ctx) {
2313
2622
  const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2314
- const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.workflowId, def.version);
2623
+ const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
2315
2624
  if (!visited.has(id)) {
2316
2625
  if (visiting.has(id))
2317
2626
  throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
@@ -2330,9 +2639,9 @@ function refsOf(def) {
2330
2639
  const out = [];
2331
2640
  for (const stage of def.stages)
2332
2641
  for (const task of stage.tasks ?? []) {
2333
- const ref = task.spawns?.definitionRef;
2642
+ const ref = task.subworkflows?.definition;
2334
2643
  ref !== void 0 && out.push({
2335
- workflowId: ref.workflowId,
2644
+ name: ref.name,
2336
2645
  ...ref.version !== void 0 ? { version: ref.version } : {}
2337
2646
  });
2338
2647
  }
@@ -2352,324 +2661,105 @@ function stableStringify(value) {
2352
2661
  ([a], [b]) => a.localeCompare(b)
2353
2662
  ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2354
2663
  }
2355
- async function loadDefinition(client, workflowId, version, tags) {
2664
+ async function loadDefinition(client, definition, version, tags) {
2356
2665
  if (version !== void 0) {
2357
2666
  const doc = await client.fetch(
2358
2667
  definitionLookupGroq(!0),
2359
- { workflowId, version, engineTags: tags }
2668
+ { definition, version, engineTags: tags }
2360
2669
  );
2361
2670
  if (!doc)
2362
- throw new Error(`Workflow definition ${workflowId} v${version} not deployed`);
2671
+ throw new Error(`Workflow definition ${definition} v${version} not deployed`);
2363
2672
  return doc;
2364
2673
  }
2365
2674
  const latest = await client.fetch(definitionLookupGroq(!1), {
2366
- workflowId,
2675
+ definition,
2367
2676
  engineTags: tags
2368
2677
  });
2369
2678
  if (!latest)
2370
- throw new Error(`No deployed definition for workflow ${workflowId}`);
2679
+ throw new Error(`No deployed definition for workflow ${definition}`);
2371
2680
  return latest;
2372
2681
  }
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
2474
- );
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
- }
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
2682
+ async function loadDefinitionVersions(client, definition, tags) {
2683
+ return client.fetch(
2684
+ `*[_type == "${WORKFLOW_DEFINITION_TYPE}" && name == $definition && count(tags[@ in $engineTags]) > 0] | order(version desc)`,
2685
+ { definition, engineTags: tags }
2504
2686
  );
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 };
2508
- }
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 = {
2529
- _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;
2581
2687
  }
2582
2688
  async function fireAction(args) {
2583
- const { client, instanceId, taskId, action, params, options } = args;
2689
+ const { client, instanceId, task, action, params, options } = args;
2584
2690
  for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2585
2691
  const ctx = await loadContext(client, instanceId, {
2586
2692
  ...options?.clock ? { clock: options.clock } : {}
2587
2693
  });
2588
2694
  try {
2589
- return await commitAction(ctx, taskId, action, params, options?.actor);
2695
+ return await commitAction(ctx, task, action, params, options);
2590
2696
  } catch (error) {
2591
2697
  if (!isRevisionConflict(error)) throw error;
2592
2698
  }
2593
2699
  }
2594
2700
  throw new ConcurrentFireActionError({
2595
2701
  instanceId,
2596
- taskId,
2702
+ task,
2597
2703
  action,
2598
2704
  attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
2599
2705
  });
2600
2706
  }
2601
- async function resolveActionCommit(ctx, taskId, actionName, callerParams) {
2602
- const { stage, task } = findTaskInCurrentStage(ctx, taskId), action = (task.actions ?? []).find((a) => a.id === actionName);
2707
+ async function resolveActionCommit(ctx, taskName, actionName, callerParams, options) {
2708
+ const actor = options?.actor, { stage, task } = findTaskInCurrentStage(ctx, taskName), action = (task.actions ?? []).find((a) => a.name === actionName);
2603
2709
  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);
2710
+ throw new Error(`Action "${actionName}" not declared on task "${taskName}"`);
2711
+ const can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0;
2712
+ if (!await ctxEvaluateCondition(ctx, action.filter, {
2713
+ taskName,
2714
+ ...actor !== void 0 ? { actor } : {},
2715
+ ...can !== void 0 ? { vars: { can } } : {}
2716
+ }))
2717
+ throw new Error(`Action "${actionName}" filter rejected on task "${taskName}"`);
2718
+ const entry = findCurrentTasks(ctx.instance).find((t) => t.name === taskName);
2608
2719
  if (entry === void 0 || entry.status !== "active")
2609
2720
  throw new Error(
2610
- `Task "${taskId}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2721
+ `Task "${taskName}" must be active to fire action "${actionName}"; status is ${entry?.status ?? "missing"}`
2611
2722
  );
2612
- const params = validateActionParams(action, taskId, callerParams);
2723
+ const params = validateActionParams(action, taskName, callerParams);
2613
2724
  return { stage, task, action, params };
2614
2725
  }
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(
2726
+ async function commitAction(ctx, taskName, actionName, callerParams, options) {
2727
+ const actor = options?.actor, { stage, action, params } = await resolveActionCommit(
2631
2728
  ctx,
2632
- taskId,
2729
+ taskName,
2633
2730
  actionName,
2634
- callerParams
2635
- ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskId), now = ctx.now;
2731
+ callerParams,
2732
+ options
2733
+ ), mutation = startMutation(ctx.instance), mutEntry = requireMutationTaskEntry(mutation, taskName), now = ctx.now;
2636
2734
  mutation.history.push({
2637
2735
  _key: randomKey(),
2638
- _type: "workflow.history.actionFired",
2736
+ _type: "actionFired",
2639
2737
  at: now,
2640
- stageId: stage.id,
2641
- taskId,
2642
- actionId: actionName,
2738
+ stage: stage.name,
2739
+ task: taskName,
2740
+ action: actionName,
2643
2741
  ...actor !== void 0 ? { actor } : {}
2644
2742
  });
2645
- const ranOps = await runActionOps({
2743
+ const statusBefore = mutEntry.status, ranOps = await runOps({
2646
2744
  ops: action.ops,
2647
2745
  mutation,
2648
- stageId: stage.id,
2649
- taskId,
2650
- actionName,
2746
+ stage: stage.name,
2747
+ origin: { task: taskName, action: actionName },
2651
2748
  params,
2652
2749
  actor,
2653
2750
  self: selfGdr(ctx.instance),
2654
2751
  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(
2752
+ }), newStatus = mutEntry.status !== statusBefore ? mutEntry.status : void 0;
2753
+ return await queueEffects(ctx, mutation, action.effects, { kind: "action", name: actionName }, actor, {
2754
+ callerParams: params,
2755
+ taskName
2756
+ }), await persistThenDeploy(
2667
2757
  ctx,
2668
2758
  mutation,
2669
- () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.id) : Promise.resolve()
2759
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
2670
2760
  ), {
2671
2761
  fired: !0,
2672
- taskId,
2762
+ task: taskName,
2673
2763
  action: actionName,
2674
2764
  ...newStatus !== void 0 ? { newStatus } : {},
2675
2765
  ...ranOps.length > 0 ? { ranOps } : {}
@@ -2677,25 +2767,22 @@ async function commitAction(ctx, taskId, actionName, callerParams, actor) {
2677
2767
  }
2678
2768
  class ActionDisabledError extends Error {
2679
2769
  reason;
2680
- taskId;
2770
+ task;
2681
2771
  action;
2682
2772
  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;
2773
+ super(formatDisabledReason(args.task, args.action, args.reason)), this.name = "ActionDisabledError", this.reason = args.reason, this.task = args.task, this.action = args.action;
2684
2774
  }
2685
2775
  }
2686
2776
  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
2777
  "filter-failed": (r) => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
2691
2778
  "task-not-active": (r) => `task status is "${r.status}"`,
2692
- "stage-terminal": (r) => `stage "${r.stageId}" is terminal`,
2779
+ "stage-terminal": (r) => `stage "${r.stage}" is terminal`,
2693
2780
  "instance-completed": (r) => `instance completed at ${r.completedAt}`,
2694
2781
  "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
2695
2782
  };
2696
- function formatDisabledReason(taskId, action, reason) {
2783
+ function formatDisabledReason(task, action, reason) {
2697
2784
  const detail = disabledReasonDetail[reason.kind](reason);
2698
- return `Action "${taskId}:${action}" is not allowed: ${detail}`;
2785
+ return `Action "${task}:${action}" is not allowed: ${detail}`;
2699
2786
  }
2700
2787
  function bareIdFromSpawnRef(uri) {
2701
2788
  if (!uri.includes(":")) return [uri];
@@ -2744,17 +2831,21 @@ function intersectsTags(docTags, engineTags) {
2744
2831
  function toEffectsContextEntries(ctx) {
2745
2832
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2746
2833
  }
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);
2834
+ function assertActionAllowed(evaluation, task, action) {
2835
+ const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
2749
2836
  if (actionEval?.allowed === !1 && actionEval.disabledReason)
2750
- throw new ActionDisabledError({ taskId, action, reason: actionEval.disabledReason });
2837
+ throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
2751
2838
  }
2752
2839
  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({
2840
+ const { client, instance, task, action, params, actor, grants, clock } = args, options = {
2841
+ actor,
2842
+ ...grants !== void 0 ? { grants } : {},
2843
+ ...clock !== void 0 ? { clock } : {}
2844
+ };
2845
+ return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
2755
2846
  client,
2756
2847
  instanceId: instance._id,
2757
- taskId,
2848
+ task,
2758
2849
  action,
2759
2850
  ...params !== void 0 ? { params } : {},
2760
2851
  options
@@ -2762,18 +2853,14 @@ async function applyAction(args) {
2762
2853
  }
2763
2854
  const workflow = {
2764
2855
  /**
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`).
2856
+ * Deploy a set of workflow definitions as one call. Each lands as a
2857
+ * {@link WORKFLOW_DEFINITION_TYPE} document; subsequent versions are
2858
+ * stored alongside earlier ones — `startInstance` picks the highest
2859
+ * version by default. The SDK figures out the dependency order itself
2860
+ * (children before parents that spawn them via
2861
+ * `subworkflows.definition`), idempotently writes any definition
2862
+ * whose content has changed, and reports a per-definition outcome
2863
+ * (`created` / `updated` / `unchanged`).
2777
2864
  *
2778
2865
  * Refs may point inside the batch OR at already-deployed definitions
2779
2866
  * in the lake — both are valid. A ref pointing at neither errors with
@@ -2789,14 +2876,14 @@ const workflow = {
2789
2876
  const ordered = await sortByDependencies(client, definitions, tags, workflowResource), results = [], prefix = canonicalTag(tags), tx = client.transaction();
2790
2877
  let hasWrites = !1;
2791
2878
  for (const def of ordered) {
2792
- const id = definitionDocId(workflowResource, prefix, def.workflowId, def.version), expected = {
2879
+ const id = definitionDocId(workflowResource, prefix, def.name, def.version), expected = {
2793
2880
  ...def,
2794
2881
  _id: id,
2795
2882
  _type: WORKFLOW_DEFINITION_TYPE,
2796
2883
  tags
2797
2884
  }, existing = await client.getDocument(id);
2798
2885
  if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
2799
- results.push({ workflowId: def.workflowId, version: def.version, status: "unchanged" });
2886
+ results.push({ definition: def.name, version: def.version, status: "unchanged" });
2800
2887
  continue;
2801
2888
  }
2802
2889
  if (existing) {
@@ -2807,7 +2894,7 @@ const workflow = {
2807
2894
  } else
2808
2895
  tx.create(expected);
2809
2896
  hasWrites = !0, results.push({
2810
- workflowId: def.workflowId,
2897
+ definition: def.name,
2811
2898
  version: def.version,
2812
2899
  status: existing ? "updated" : "created"
2813
2900
  });
@@ -2827,20 +2914,20 @@ const workflow = {
2827
2914
  client,
2828
2915
  tags,
2829
2916
  workflowResource,
2830
- workflowId,
2917
+ definition: definitionName,
2831
2918
  version,
2832
2919
  initialState,
2833
2920
  ancestors,
2834
2921
  effectsContext,
2835
2922
  instanceId,
2836
2923
  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)
2924
+ } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
2925
+ if (definition.stages.find((s) => s.name === definition.initialStage) === void 0)
2839
2926
  throw new Error(
2840
- `Initial stage "${definition.initialStageId}" missing in ${workflowId} v${definition.version}`
2927
+ `Initial stage "${definition.initialStage}" missing in ${definitionName} v${definition.version}`
2841
2928
  );
2842
2929
  const now = clock(), effectsContextEntries = effectsContext ? toEffectsContextEntries(effectsContext) : [], resolvedState = await resolveDeclaredState({
2843
- slotDefs: definition.state ?? [],
2930
+ entryDefs: definition.state ?? [],
2844
2931
  initialState: initialState ?? [],
2845
2932
  ctx: {
2846
2933
  client,
@@ -2856,14 +2943,14 @@ const workflow = {
2856
2943
  now,
2857
2944
  tags,
2858
2945
  workflowResource,
2859
- workflowId: definition.workflowId,
2946
+ definitionName: definition.name,
2860
2947
  pinnedVersion: definition.version,
2861
2948
  definition,
2862
2949
  state: resolvedState,
2863
2950
  effectsContext: effectsContextEntries,
2864
2951
  ancestors: ancestors ?? [],
2865
2952
  perspective: effectivePerspective,
2866
- initialStageId: definition.initialStageId,
2953
+ initialStage: definition.initialStage,
2867
2954
  actor
2868
2955
  });
2869
2956
  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 +2970,13 @@ const workflow = {
2883
2970
  client,
2884
2971
  tags,
2885
2972
  instanceId,
2886
- taskId,
2973
+ task,
2887
2974
  action,
2888
2975
  params,
2889
2976
  idempotent,
2890
2977
  resourceClients
2891
2978
  } = 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)
2979
+ if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
2893
2980
  return { instance: before, cascaded: 0, fired: !1 };
2894
2981
  const evaluation = await evaluateInstance({
2895
2982
  client,
@@ -2899,13 +2986,14 @@ const workflow = {
2899
2986
  clock,
2900
2987
  ...resourceClients !== void 0 ? { resourceClients } : {}
2901
2988
  });
2902
- assertActionAllowed(evaluation, taskId, action);
2989
+ assertActionAllowed(evaluation, task, action);
2903
2990
  const ranOps = await applyAction({
2904
2991
  client,
2905
2992
  instance: before,
2906
- taskId,
2993
+ task,
2907
2994
  action,
2908
2995
  actor,
2996
+ ...access.grants !== void 0 ? { grants: access.grants } : {},
2909
2997
  clock,
2910
2998
  ...params !== void 0 ? { params } : {}
2911
2999
  }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
@@ -2953,8 +3041,8 @@ const workflow = {
2953
3041
  * affected instances and the engine re-evaluates.
2954
3042
  */
2955
3043
  tick: async (args) => {
2956
- const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2957
- await reload(client, instanceId, tags);
3044
+ 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);
3045
+ await assertInstanceWriteAllowed({ instance: current, guards, identity: actor.id });
2958
3046
  const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
2959
3047
  return {
2960
3048
  instance: await reload(client, instanceId, tags),
@@ -2963,17 +3051,17 @@ const workflow = {
2963
3051
  };
2964
3052
  },
2965
3053
  /**
2966
- * Admin override — force the instance into `targetStageId` regardless
3054
+ * Admin override — force the instance into `targetStage` regardless
2967
3055
  * of filters or declared transitions. ACL gating should be enforced
2968
3056
  * upstream; this verb performs the mechanical move.
2969
3057
  */
2970
3058
  setStage: async (args) => {
2971
- const { client, tags, instanceId, targetStageId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3059
+ const { client, tags, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
2972
3060
  await reload(client, instanceId, tags);
2973
3061
  const result = await setStage({
2974
3062
  client,
2975
3063
  instanceId,
2976
- targetStageId,
3064
+ targetStage,
2977
3065
  ...reason !== void 0 ? { reason } : {},
2978
3066
  options: { ...actor !== void 0 ? { actor } : {}, clock }
2979
3067
  }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
@@ -2983,6 +3071,28 @@ const workflow = {
2983
3071
  fired: result.fired
2984
3072
  };
2985
3073
  },
3074
+ /**
3075
+ * Admin override — hard-stop an in-flight instance where it stands.
3076
+ * No stage move, no transition effects, pending effects cancelled; see
3077
+ * the engine-level {@link engineAbortInstance} for the full contract.
3078
+ * Ancestors are propagated (not cascaded — the instance is terminal)
3079
+ * so a parent gate waiting on this child re-evaluates.
3080
+ */
3081
+ abortInstance: async (args) => {
3082
+ const { client, tags, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3083
+ await reload(client, instanceId, tags);
3084
+ const result = await abortInstance({
3085
+ client,
3086
+ instanceId,
3087
+ ...reason !== void 0 ? { reason } : {},
3088
+ options: { ...actor !== void 0 ? { actor } : {}, clock }
3089
+ });
3090
+ return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), {
3091
+ instance: await reload(client, instanceId, tags),
3092
+ cascaded: 0,
3093
+ fired: result.fired
3094
+ };
3095
+ },
2986
3096
  /**
2987
3097
  * Fetch a workflow instance by id, scoped to the engine's tags.
2988
3098
  * Throws when the instance doesn't exist or isn't visible to this
@@ -3002,6 +3112,20 @@ const workflow = {
3002
3112
  instance
3003
3113
  });
3004
3114
  },
3115
+ guardsForDefinition: async (args) => {
3116
+ const { client, tags, workflowResource, definition, resourceClients } = args;
3117
+ validateTags(tags);
3118
+ const definitions = await loadDefinitionVersions(client, definition, tags);
3119
+ if (definitions.length === 0)
3120
+ throw new Error(`No deployed definition for workflow ${definition}`);
3121
+ return guardsForDefinition({
3122
+ client,
3123
+ clientForGdr: buildClientForGdr(client, resourceClients),
3124
+ workflowResource,
3125
+ definition,
3126
+ definitions
3127
+ });
3128
+ },
3005
3129
  /**
3006
3130
  * Run a caller-supplied GROQ query with the engine's tags bound as
3007
3131
  * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
@@ -3024,12 +3148,13 @@ const workflow = {
3024
3148
  * Snapshot-aware GROQ — runs against the same in-memory view that
3025
3149
  * filters see for a given instance.
3026
3150
  *
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.
3151
+ * Hydrates the instance's snapshot (instance + ancestors + every doc
3152
+ * declared by a `doc.ref` / `doc.refs` entry in scope), then
3153
+ * evaluates the supplied GROQ in groq-js against that dataset. The
3154
+ * rendered scope is auto-bound: `$self`, `$state`, `$parent`,
3155
+ * `$ancestors`, `$stage`, `$now`, `$effects`, `$tasks`,
3156
+ * `$allTasksDone`, `$anyTaskFailed` — ids in GDR URI form to match
3157
+ * the snapshot's keying.
3033
3158
  *
3034
3159
  * Use when an external consumer (a UI, a debug pane, a test) wants
3035
3160
  * to ask "what does the engine see for this workflow right now?"
@@ -3038,7 +3163,7 @@ const workflow = {
3038
3163
  queryInScope: async (args) => {
3039
3164
  const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
3040
3165
  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 } });
3166
+ 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
3167
  return await (await evaluate(tree, {
3043
3168
  dataset: snapshot.docs,
3044
3169
  params: { ...reserved, ...params }
@@ -3054,7 +3179,7 @@ const workflow = {
3054
3179
  * filters on claim presence; `names` restricts to specific effect
3055
3180
  * names. Both filters compose (AND).
3056
3181
  */
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))),
3182
+ 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
3183
  /**
3059
3184
  * Project the instance from a given actor's perspective. Returns a
3060
3185
  * `WorkflowEvaluation` with per-action verdicts (`allowed` + a
@@ -3067,19 +3192,19 @@ const workflow = {
3067
3192
  /**
3068
3193
  * Materialised spawned children of a parent instance.
3069
3194
  *
3070
- * Walks `history` for `workflow.history.spawned` events — the durable
3195
+ * Walks `history` for `spawned` events — the durable
3071
3196
  * record. (The per-task `spawnedInstances` field on the active stage
3072
3197
  * only exists while that stage is current; history survives.) Strips
3073
3198
  * the GDR URI on each `instanceRef` to a bare `_id`, fetches the
3074
3199
  * instances, drops any that aren't visible to this engine's tags, and
3075
3200
  * returns them sorted by `startedAt` ascending.
3076
3201
  *
3077
- * Pass `taskId` to restrict to a single spawning task on the parent.
3202
+ * Pass `task` to restrict to a single spawning task on the parent.
3078
3203
  */
3079
3204
  children: async (args) => {
3080
- const { client, tags, instanceId, taskId } = args;
3205
+ const { client, tags, instanceId, task } = args;
3081
3206
  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));
3207
+ 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
3208
  return ids.length === 0 ? [] : client.fetch(
3084
3209
  `*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
3085
3210
  { ids, engineTags: tags }
@@ -3100,11 +3225,11 @@ async function verifyDeployedDefinitionsInternal(args) {
3100
3225
  const { client, tags, effectHandlers, missingHandler, logger } = args;
3101
3226
  validateTags(tags);
3102
3227
  const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
3103
- `*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(workflowId asc, version asc)`,
3228
+ `*[_type == "${WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(name asc, version asc)`,
3104
3229
  { engineTags: tags }
3105
3230
  ), seen = [], missingByName = /* @__PURE__ */ new Map();
3106
3231
  for (const def of definitions)
3107
- seen.push({ workflowId: def.workflowId, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
3232
+ seen.push({ name: def.name, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
3108
3233
  if (effectHandlers[name] !== void 0) return;
3109
3234
  const key = `${name}@@${def._id}`;
3110
3235
  let bucket = missingByName.get(key);
@@ -3135,17 +3260,16 @@ async function verifyDeployedDefinitionsInternal(args) {
3135
3260
  }
3136
3261
  function walkEffectNames(def, visit) {
3137
3262
  const visitEffects = (effects, location) => {
3138
- for (const e of effects ?? []) visit(e.id, location);
3263
+ for (const e of effects ?? []) visit(e.name, location);
3139
3264
  };
3140
3265
  for (const stage of def.stages ?? []) {
3141
- visitEffects(stage.onEnter, `stage[${stage.id}].onEnter`), visitEffects(stage.onExit, `stage[${stage.id}].onExit`);
3142
3266
  for (const t of stage.tasks ?? []) {
3143
- visitEffects(t.effects, `stage[${stage.id}].task[${t.id}].effects`);
3267
+ visitEffects(t.effects, `stage[${stage.name}].task[${t.name}].effects`);
3144
3268
  for (const a of t.actions ?? [])
3145
- visitEffects(a.effects, `stage[${stage.id}].task[${t.id}].action[${a.id}].effects`);
3269
+ visitEffects(a.effects, `stage[${stage.name}].task[${t.name}].action[${a.name}].effects`);
3146
3270
  }
3147
3271
  for (const tr of stage.transitions ?? [])
3148
- visitEffects(tr.effects, `stage[${stage.id}].transition[${tr.to}].effects`);
3272
+ visitEffects(tr.effects, `stage[${stage.name}].transition[${tr.name}].effects`);
3149
3273
  }
3150
3274
  }
3151
3275
  async function drainEffectsInternal(args) {
@@ -3169,7 +3293,7 @@ async function drainEffectsInternal(args) {
3169
3293
  (e) => e.claim === void 0 && !skippedKeys.has(e._key)
3170
3294
  );
3171
3295
  if (candidate === void 0) break;
3172
- const handler = effectHandlers[candidate.effectId];
3296
+ const handler = effectHandlers[candidate.name];
3173
3297
  if (handler === void 0) {
3174
3298
  await assertSkippableOrThrow(missingHandler, candidate, instanceId, log), skipped.push(candidate), skippedKeys.add(candidate._key);
3175
3299
  continue;
@@ -3207,16 +3331,16 @@ async function reportEffectOutcome(args) {
3207
3331
  async function assertSkippableOrThrow(missingHandler, candidate, instanceId, log) {
3208
3332
  if (await applyMissingHandler(
3209
3333
  missingHandler,
3210
- { phase: "drain", name: candidate.effectId, effectKey: candidate._key, instanceId },
3334
+ { phase: "drain", name: candidate.name, effectKey: candidate._key, instanceId },
3211
3335
  log
3212
3336
  ) === "fail")
3213
3337
  throw new Error(
3214
- `drainEffects: no handler registered for "${candidate.effectId}" (effectKey=${candidate._key}, instanceId=${instanceId})`
3338
+ `drainEffects: no handler registered for "${candidate.name}" (effectKey=${candidate._key}, instanceId=${instanceId})`
3215
3339
  );
3216
3340
  }
3217
3341
  async function claimPendingEffect(client, instance, effectKey, drainerActor) {
3218
3342
  const claim = {
3219
- _type: "workflow.pendingEffect.claim",
3343
+ _type: "pendingEffect.claim",
3220
3344
  claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
3221
3345
  claimedBy: drainerActor
3222
3346
  };
@@ -3233,7 +3357,7 @@ async function dispatchEffect(handler, candidate, ctx) {
3233
3357
  client: ctx.client,
3234
3358
  instanceId: ctx.instanceId,
3235
3359
  effectKey: candidate._key,
3236
- log: (message, extra) => ctx.logger(`effect.${candidate.effectId}`).info(message, extra)
3360
+ log: (message, extra) => ctx.logger(`effect.${candidate.name}`).info(message, extra)
3237
3361
  });
3238
3362
  return result?.outputs !== void 0 ? { outputs: result.outputs } : {};
3239
3363
  } catch (err) {
@@ -3253,7 +3377,7 @@ async function applyMissingHandler(policy, info, log) {
3253
3377
  }
3254
3378
  function createInstanceSession(args) {
3255
3379
  const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
3256
- let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), committing = !1, buffered;
3380
+ let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), heldGuards = [], committing = !1, buffered;
3257
3381
  const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
3258
3382
  const next = /* @__PURE__ */ new Map();
3259
3383
  for (const ld of docs) {
@@ -3268,13 +3392,14 @@ function createInstanceSession(args) {
3268
3392
  }, access = () => resolveAccess(client, {
3269
3393
  ...args.access !== void 0 ? { override: args.access } : {},
3270
3394
  ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
3271
- }), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held) => {
3395
+ }), snapshotFrom = (held) => buildSnapshot({ docs: [{ doc: instance, resource: instance.workflowResource }, ...held.values()] }), evaluateWith = async (held, guards) => {
3272
3396
  const { actor, grants } = await access();
3273
3397
  return evaluateFromSnapshot({
3274
3398
  instance,
3275
3399
  definition: parseDefinitionSnapshot(instance),
3276
3400
  actor,
3277
3401
  snapshot: snapshotFrom(held),
3402
+ guards,
3278
3403
  ...clock !== void 0 ? { now: clock() } : {},
3279
3404
  ...grants !== void 0 ? { grants } : {}
3280
3405
  });
@@ -3302,24 +3427,29 @@ function createInstanceSession(args) {
3302
3427
  }
3303
3428
  applyUpdate(docs);
3304
3429
  },
3430
+ updateGuards(guards) {
3431
+ heldGuards = guards.map((guard) => structuredClone(guard));
3432
+ },
3305
3433
  evaluate() {
3306
- return evaluateWith(overlay);
3434
+ return evaluateWith(overlay, heldGuards);
3307
3435
  },
3308
3436
  tick() {
3309
3437
  return commit(async (actor, held) => {
3438
+ await assertInstanceWriteAllowed({ instance, guards: heldGuards, identity: actor.id });
3310
3439
  const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3311
3440
  return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
3312
3441
  });
3313
3442
  },
3314
- fireAction({ taskId, action, params }) {
3443
+ fireAction({ task, action, params }) {
3315
3444
  return commit(async (actor, held) => {
3316
- assertActionAllowed(await evaluateWith(held), taskId, action);
3317
- const ranOps = await applyAction({
3445
+ assertActionAllowed(await evaluateWith(held, heldGuards), task, action);
3446
+ const { grants } = await access(), ranOps = await applyAction({
3318
3447
  client,
3319
3448
  instance,
3320
- taskId,
3449
+ task,
3321
3450
  action,
3322
3451
  actor,
3452
+ ...grants !== void 0 ? { grants } : {},
3323
3453
  ...clock !== void 0 ? { clock } : {},
3324
3454
  ...params !== void 0 ? { params } : {}
3325
3455
  }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
@@ -3330,7 +3460,7 @@ function createInstanceSession(args) {
3330
3460
  }
3331
3461
  function asHeldInstance(doc) {
3332
3462
  const candidate = doc;
3333
- if (candidate._type !== "workflow.instance" || typeof candidate.currentStageId != "string" || !Array.isArray(candidate.stages))
3463
+ if (candidate._type !== WORKFLOW_INSTANCE_TYPE || typeof candidate.currentStage != "string" || !Array.isArray(candidate.stages))
3334
3464
  throw new Error(`update: self-doc ${doc._id} is not a valid workflow.instance`);
3335
3465
  return doc;
3336
3466
  }
@@ -3375,6 +3505,7 @@ function createEngine(args) {
3375
3505
  tick: (rest) => workflow.tick(bind(rest)),
3376
3506
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
3377
3507
  setStage: (rest) => workflow.setStage(bind(rest)),
3508
+ abortInstance: (rest) => workflow.abortInstance(bind(rest)),
3378
3509
  getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
3379
3510
  subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
3380
3511
  instance: (instanceDoc, opts) => createInstanceSession({
@@ -3393,12 +3524,19 @@ function createEngine(args) {
3393
3524
  instanceId,
3394
3525
  ...resourceClients !== void 0 ? { resourceClients } : {}
3395
3526
  }),
3396
- children: ({ instanceId, taskId }) => workflow.children({
3527
+ guardsForDefinition: ({ definition }) => workflow.guardsForDefinition({
3528
+ client,
3529
+ tags,
3530
+ workflowResource,
3531
+ definition,
3532
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3533
+ }),
3534
+ children: ({ instanceId, task }) => workflow.children({
3397
3535
  client,
3398
3536
  tags,
3399
3537
  workflowResource,
3400
3538
  instanceId,
3401
- ...taskId !== void 0 ? { taskId } : {}
3539
+ ...task !== void 0 ? { task } : {}
3402
3540
  }),
3403
3541
  query: ({ groq, params }) => workflow.query({
3404
3542
  client,
@@ -3446,111 +3584,127 @@ function createEngine(args) {
3446
3584
  };
3447
3585
  }
3448
3586
  const HISTORY_DISPLAY = {
3449
- "workflow.history.stageEntered": {
3587
+ stageEntered: {
3450
3588
  title: "Stage entered",
3451
3589
  description: "The instance crossed into a new stage."
3452
3590
  },
3453
- "workflow.history.stageExited": {
3591
+ stageExited: {
3454
3592
  title: "Stage exited",
3455
3593
  description: "The instance left a stage on the way to the next one."
3456
3594
  },
3457
- "workflow.history.taskActivated": {
3595
+ taskActivated: {
3458
3596
  title: "Task activated",
3459
3597
  description: "A task flipped from `pending` to `active` and its onEnter effects queued."
3460
3598
  },
3461
- "workflow.history.taskStatusChanged": {
3599
+ taskStatusChanged: {
3462
3600
  title: "Task status changed",
3463
3601
  description: "An action or op flipped a task's status (active \u2192 done / skipped / failed)."
3464
3602
  },
3465
- "workflow.history.actionFired": {
3603
+ actionFired: {
3466
3604
  title: "Action fired",
3467
3605
  description: "An action was invoked against a task \u2014 ops ran, status may have flipped, effects queued."
3468
3606
  },
3469
- "workflow.history.transitionFired": {
3607
+ transitionFired: {
3470
3608
  title: "Transition fired",
3471
3609
  description: "A transition committed and the instance moved to a new stage."
3472
3610
  },
3473
- "workflow.history.effectQueued": {
3611
+ effectQueued: {
3474
3612
  title: "Effect queued",
3475
3613
  description: "A pending effect was added to the queue, waiting for a drainer to dispatch it."
3476
3614
  },
3477
- "workflow.history.effectCompleted": {
3615
+ effectCompleted: {
3478
3616
  title: "Effect completed",
3479
3617
  description: "A drainer ran an effect handler to completion (done or failed) and merged its outputs into effectsContext."
3480
3618
  },
3481
- "workflow.history.spawned": {
3619
+ spawned: {
3482
3620
  title: "Spawned child",
3483
- description: "A `task.spawns` declaration spawned a child workflow instance."
3621
+ description: "A task's `subworkflows` declaration spawned a child workflow instance."
3484
3622
  },
3485
- "workflow.history.opApplied": {
3623
+ aborted: {
3624
+ title: "Instance aborted",
3625
+ description: "An admin hard-stopped the instance \u2014 pending effects cancelled, stage guards lifted, abortedAt + completedAt stamped."
3626
+ },
3627
+ opApplied: {
3486
3628
  title: "Op applied",
3487
3629
  description: "An inline state-mutation op (state.set / state.append / status.set / etc.) ran during an action commit."
3488
3630
  }
3489
3631
  }, STATE_SLOT_DISPLAY = {
3490
- "workflow.state.doc.ref": {
3632
+ "doc.ref": {
3491
3633
  title: "Document reference",
3492
3634
  description: "Single GDR pointer at a document in this or another resource."
3493
3635
  },
3494
- "workflow.state.doc.refs": {
3636
+ "doc.refs": {
3495
3637
  title: "Document references",
3496
3638
  description: "Ordered list of GDR pointers \u2014 multi-doc selection."
3497
3639
  },
3498
- "workflow.state.query": {
3640
+ "release.ref": {
3641
+ title: "Content Release reference",
3642
+ description: "GDR pointer at a Content Release system doc, carrying the release name the engine derives the instance's read perspective from."
3643
+ },
3644
+ query: {
3499
3645
  title: "Query result",
3500
- description: "Snapshot of a GROQ projection captured at slot-resolve time; resolvedAt is stamped on the value."
3646
+ description: "Snapshot of a GROQ projection captured at entry-resolve time; resolvedAt is stamped on the value."
3501
3647
  },
3502
- "workflow.state.value.string": {
3648
+ "value.string": {
3503
3649
  title: "String value",
3504
- description: "Free-form string slot."
3650
+ description: "Free-form string entry."
3505
3651
  },
3506
- "workflow.state.value.url": {
3652
+ "value.url": {
3507
3653
  title: "URL value",
3508
- description: "Validated URL slot."
3654
+ description: "Validated URL entry."
3509
3655
  },
3510
- "workflow.state.value.number": {
3656
+ "value.number": {
3511
3657
  title: "Number value",
3512
- description: "Numeric slot."
3658
+ description: "Numeric entry."
3513
3659
  },
3514
- "workflow.state.value.boolean": {
3660
+ "value.boolean": {
3515
3661
  title: "Boolean value",
3516
- description: "True/false slot."
3662
+ description: "True/false entry."
3517
3663
  },
3518
- "workflow.state.value.dateTime": {
3664
+ "value.dateTime": {
3519
3665
  title: "Date / time value",
3520
- description: "ISO-timestamp slot."
3666
+ description: "ISO-timestamp entry."
3521
3667
  },
3522
- "workflow.state.checklist": {
3668
+ "value.actor": {
3669
+ title: "Actor value",
3670
+ description: "A typed (user|ai|system) actor identity."
3671
+ },
3672
+ checklist: {
3523
3673
  title: "Checklist",
3524
3674
  description: "Append-only list of checkable items; ops add/update/remove entries."
3525
3675
  },
3526
- "workflow.state.notes": {
3676
+ notes: {
3527
3677
  title: "Notes log",
3528
3678
  description: "Append-only structured-note log (signoffs, comments, audit rows)."
3529
3679
  },
3530
- "workflow.state.assignees": {
3680
+ assignees: {
3531
3681
  title: "Assignees",
3532
3682
  description: "Ordered list of typed (user|role) assignees."
3533
3683
  }
3534
3684
  }, OP_DISPLAY = {
3535
- "workflow.op.state.set": {
3536
- title: "Set slot",
3537
- description: "Overwrite a state slot's value with a resolved Source."
3685
+ "state.set": {
3686
+ title: "Set state entry",
3687
+ description: "Overwrite a state entry's value with a resolved Source."
3688
+ },
3689
+ "state.unset": {
3690
+ title: "Unset state entry",
3691
+ description: "Reset a state entry to its default (null for scalars, [] for array kinds)."
3538
3692
  },
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)."
3693
+ "state.append": {
3694
+ title: "Append to state entry",
3695
+ description: "Push a resolved item onto an array-kind entry (notes, checklist, assignees, refs)."
3542
3696
  },
3543
- "workflow.op.state.updateWhere": {
3697
+ "state.updateWhere": {
3544
3698
  title: "Update matching rows",
3545
- description: "Merge fields into rows of an array-typed slot that match the predicate."
3699
+ description: "Merge fields into rows of an array-kind entry that match the predicate."
3546
3700
  },
3547
- "workflow.op.state.removeWhere": {
3701
+ "state.removeWhere": {
3548
3702
  title: "Remove matching rows",
3549
- description: "Drop rows of an array-typed slot that match the predicate."
3703
+ description: "Drop rows of an array-kind entry that match the predicate."
3550
3704
  },
3551
- "workflow.op.status.set": {
3705
+ "status.set": {
3552
3706
  title: "Set task status",
3553
- description: "Flip a task's status explicitly (action.setStatus is the more common path)."
3707
+ description: "Flip a task's status explicitly (the action-level `status:` sugar desugars to this)."
3554
3708
  }
3555
3709
  }, EFFECTS_CONTEXT_DISPLAY = {
3556
3710
  "effectsContext.string": {
@@ -3574,53 +3728,21 @@ const HISTORY_DISPLAY = {
3574
3728
  description: "Stable JSON payload for effect handlers."
3575
3729
  }
3576
3730
  }, 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": {
3731
+ pendingEffect: {
3610
3732
  title: "Pending effect",
3611
3733
  description: "An effect that has been queued but not yet dispatched."
3612
3734
  },
3613
- "workflow.pendingEffect.claim": {
3735
+ "pendingEffect.claim": {
3614
3736
  title: "Claim",
3615
3737
  description: "A drainer's lock on a pending effect while it dispatches."
3616
3738
  },
3617
- "workflow.definition": {
3739
+ [WORKFLOW_DEFINITION_TYPE]: {
3618
3740
  title: "Workflow definition",
3619
- description: "An immutable workflow blueprint pinned by `workflowId` + `version`."
3741
+ description: "An immutable workflow blueprint, addressed by `name` + `version`."
3620
3742
  },
3621
- "workflow.instance": {
3743
+ [WORKFLOW_INSTANCE_TYPE]: {
3622
3744
  title: "Workflow instance",
3623
- description: "A running (or finished) workflow against a subject."
3745
+ description: "A running (or finished) workflow against its declared state."
3624
3746
  }
3625
3747
  }, DISPLAY = {
3626
3748
  ...HISTORY_DISPLAY,
@@ -3659,11 +3781,13 @@ export {
3659
3781
  compileGuard,
3660
3782
  contentReleaseName,
3661
3783
  createEngine,
3784
+ datasetResourceParts,
3662
3785
  defaultLoggerFactory,
3663
3786
  denyingGuards,
3664
3787
  deployStageGuards,
3665
3788
  displayDescription,
3666
3789
  displayTitle,
3790
+ effectsContextMap,
3667
3791
  evaluateFromSnapshot,
3668
3792
  evaluateMutationGuard,
3669
3793
  extractDocumentId,
@@ -3671,10 +3795,13 @@ export {
3671
3795
  gdrRef,
3672
3796
  gdrUri,
3673
3797
  guardMatches,
3798
+ guardsForDefinition,
3674
3799
  guardsForInstance,
3675
3800
  guardsForResource,
3801
+ instanceGuardQuery,
3676
3802
  isDefinitionUnchanged,
3677
3803
  isGdr,
3804
+ isTerminalStage,
3678
3805
  lakeGuardId,
3679
3806
  parseGdr,
3680
3807
  readsRaw,
@@ -3687,10 +3814,12 @@ export {
3687
3814
  retractStageGuards,
3688
3815
  silentLogger,
3689
3816
  stripSystemFields,
3817
+ subscriptionDocument,
3690
3818
  subscriptionDocumentsForInstance,
3691
3819
  tagScopeFilter,
3692
3820
  validateDefinition,
3693
3821
  validateTags,
3822
+ verdictGuardsForInstance,
3694
3823
  wallClock,
3695
3824
  workflow
3696
3825
  };