@sanity/workflow-engine 0.2.0 → 0.4.0

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