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