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