@sanity/workflow-engine 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -17,13 +17,8 @@ function _interopNamespaceCompat(e) {
17
17
  }), n.default = e, Object.freeze(n);
18
18
  }
19
19
  var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
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;
20
+ function findOpenStageEntry(host) {
21
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
27
22
  }
28
23
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
29
24
  function parseGdr(uri) {
@@ -95,6 +90,21 @@ function gdrFromResource(res, documentId) {
95
90
  }
96
91
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
97
92
  }
93
+ function resourceFromGdrUri(uri) {
94
+ let parsed;
95
+ try {
96
+ parsed = parseGdr(uri);
97
+ } catch {
98
+ return;
99
+ }
100
+ if (parsed.scheme === "dataset")
101
+ return parsed.projectId === void 0 || parsed.dataset === void 0 ? void 0 : { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` };
102
+ if (parsed.resourceId !== void 0)
103
+ return { type: parsed.scheme, id: parsed.resourceId };
104
+ }
105
+ function sameResource(a, b) {
106
+ return a.type === b.type && a.id === b.id;
107
+ }
98
108
  function selfGdr(doc) {
99
109
  return gdrFromResource(doc.workflowResource, doc._id);
100
110
  }
@@ -112,12 +122,100 @@ function gdrRef(res, documentId, type) {
112
122
  function isGdr(value) {
113
123
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
114
124
  }
125
+ function buildParams(args) {
126
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
127
+ return {
128
+ self: selfGdr(instance),
129
+ state: renderedState(instance.state ?? [], snapshot),
130
+ parent: instance.ancestors.at(-1)?.id ?? null,
131
+ ancestors: instance.ancestors.map((a) => a.id),
132
+ stage: instance.currentStage,
133
+ now,
134
+ /** `$effects` — parent context handoff by entry name, completed-effect
135
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
136
+ effects: effectsContextMap(instance),
137
+ /** `$tasks` — the current stage's task rows, statuses included. */
138
+ tasks: currentTasks2,
139
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
140
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
141
+ ...extra
142
+ };
143
+ }
144
+ function renderedState(entries, snapshot) {
145
+ const out = {};
146
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
147
+ return out;
148
+ }
149
+ function renderedValue(entry, snapshot) {
150
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
151
+ }
152
+ function scopedStateOverlay(instance, snapshot, taskName) {
153
+ const stageEntry = findOpenStageEntry(instance);
154
+ if (stageEntry === void 0) return {};
155
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
156
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
157
+ }
158
+ function assignedFor(instance, taskName, actor) {
159
+ if (actor === void 0) return !1;
160
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
161
+ return entry === void 0 ? !1 : entry.value.some(
162
+ (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
163
+ );
164
+ }
165
+ function effectsContextMap(instance) {
166
+ const out = {};
167
+ for (const entry of instance.effectsContext)
168
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
169
+ return out;
170
+ }
171
+ function parseJsonContextEntry(entry) {
172
+ try {
173
+ return JSON.parse(entry.value);
174
+ } catch (err) {
175
+ throw new Error(
176
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
177
+ { cause: err }
178
+ );
179
+ }
180
+ }
181
+ function paramsForLake(params) {
182
+ return {
183
+ ...params,
184
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
185
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
186
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
187
+ state: stripStateForLake(params.state)
188
+ };
189
+ }
190
+ function stripStateForLake(value) {
191
+ if (value == null) return value;
192
+ if (typeof value == "string") return bareId(value);
193
+ if (Array.isArray(value)) return value.map(stripStateForLake);
194
+ if (typeof value == "object") {
195
+ const out = {};
196
+ for (const [k, v2] of Object.entries(value))
197
+ out[k] = stripStateForLake(v2);
198
+ return out;
199
+ }
200
+ return value;
201
+ }
202
+ function bareId(id) {
203
+ return isGdrUri(id) ? extractDocumentId(id) : id;
204
+ }
205
+ function getPath(value, path) {
206
+ let current = value;
207
+ for (const part of path.split(".")) {
208
+ if (current == null || typeof current != "object") return;
209
+ current = current[part];
210
+ }
211
+ return current;
212
+ }
115
213
  function resourceOf(p) {
116
214
  return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
117
215
  }
118
- const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
216
+ const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
119
217
  function isGuardReadExpr(expr) {
120
- return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
218
+ return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
121
219
  }
122
220
  function resolveGuardRead(expr, ctx) {
123
221
  if (expr === "$self") return selfGdr(ctx.instance);
@@ -127,8 +225,13 @@ function resolveGuardRead(expr, ctx) {
127
225
  const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
128
226
  return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
129
227
  }
228
+ const effectsRead = EFFECTS_READ.exec(expr);
229
+ if (effectsRead !== null) {
230
+ const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
231
+ return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
232
+ }
130
233
  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)`
234
+ `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
132
235
  );
133
236
  }
134
237
  function resolveIdRefTarget(expr, ctx) {
@@ -210,7 +313,7 @@ function createDefinitionValidator() {
210
313
  entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
211
314
  }, checkGuardRead: (expr, where) => {
212
315
  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)`
316
+ ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
214
317
  );
215
318
  } };
216
319
  }
@@ -239,6 +342,8 @@ function validateTask(v2, stageName, task) {
239
342
  for (const entry of task.state ?? [])
240
343
  v2.checkEntry(entry, `${where}.state "${entry.name}"`);
241
344
  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`);
345
+ for (const [name, groq] of Object.entries(task.requirements ?? {}))
346
+ v2.checkCondition(groq, `${where}.requirements "${name}"`);
242
347
  for (const [key, groq] of effectBindings(task.effects))
243
348
  v2.tryParse(groq, `${where} effect binding "${key}"`);
244
349
  validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
@@ -387,99 +492,20 @@ async function assertInstanceWriteAllowed(args) {
387
492
  guards: denied
388
493
  });
389
494
  }
390
- function findOpenStageEntry(host) {
391
- return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
392
- }
393
- function buildParams(args) {
394
- const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
395
- return {
396
- self: selfGdr(instance),
397
- state: renderedState(instance.state ?? [], snapshot),
398
- parent: instance.ancestors.at(-1)?.id ?? null,
399
- ancestors: instance.ancestors.map((a) => a.id),
400
- stage: instance.currentStage,
401
- now,
402
- /** `$effects` — parent context handoff by entry name, completed-effect
403
- * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
404
- effects: effectsContextMap(instance),
405
- /** `$tasks` — the current stage's task rows, statuses included. */
406
- tasks: currentTasks2,
407
- allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
408
- anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
409
- ...extra
410
- };
411
- }
412
- function renderedState(entries, snapshot) {
413
- const out = {};
414
- for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
415
- return out;
416
- }
417
- function renderedValue(entry, snapshot) {
418
- return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
419
- }
420
- function scopedStateOverlay(instance, snapshot, taskName) {
421
- const stageEntry = findOpenStageEntry(instance);
422
- if (stageEntry === void 0) return {};
423
- const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
424
- return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
425
- }
426
- function assignedFor(instance, taskName, actor) {
427
- if (actor === void 0) return !1;
428
- const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
429
- return entry === void 0 ? !1 : entry.value.some(
430
- (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
431
- );
432
- }
433
- function effectsContextMap(instance) {
434
- const out = {};
435
- for (const entry of instance.effectsContext)
436
- out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
437
- return out;
438
- }
439
- function parseJsonContextEntry(entry) {
440
- try {
441
- return JSON.parse(entry.value);
442
- } catch (err) {
443
- throw new Error(
444
- `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
445
- { cause: err }
446
- );
447
- }
448
- }
449
- function paramsForLake(params) {
450
- return {
451
- ...params,
452
- self: typeof params.self == "string" ? bareId(params.self) : params.self,
453
- parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
454
- ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
455
- state: stripStateForLake(params.state)
456
- };
457
- }
458
- function stripStateForLake(value) {
459
- if (value == null) return value;
460
- if (typeof value == "string") return bareId(value);
461
- if (Array.isArray(value)) return value.map(stripStateForLake);
462
- if (typeof value == "object") {
463
- const out = {};
464
- for (const [k, v2] of Object.entries(value))
465
- out[k] = stripStateForLake(v2);
466
- return out;
467
- }
468
- return value;
469
- }
470
- function bareId(id) {
471
- return isGdrUri(id) ? extractDocumentId(id) : id;
472
- }
473
495
  function abortReason(instance) {
474
496
  return instance.history.find(
475
497
  (h) => h._type === "aborted"
476
498
  )?.reason;
477
499
  }
478
500
  function diagnoseInputFromEvaluation(evaluation) {
501
+ const stage = openStage(evaluation.instance);
479
502
  return {
480
503
  instance: evaluation.instance,
481
504
  tasks: evaluation.currentStage.tasks,
482
- transitions: evaluation.currentStage.transitions
505
+ transitions: evaluation.currentStage.transitions,
506
+ assignees: Object.fromEntries(
507
+ evaluation.currentStage.tasks.map((t) => [t.task.name, assigneesOf(stage, t.task.name)])
508
+ )
483
509
  };
484
510
  }
485
511
  function openStage(instance) {
@@ -504,18 +530,34 @@ function failedTaskCause(input) {
504
530
  return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
505
531
  }
506
532
  function waitingState(input) {
507
- const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
533
+ const active = input.tasks.find(
534
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length === 0
535
+ );
508
536
  if (active !== void 0)
509
537
  return {
510
538
  state: "waiting",
511
539
  task: active.task.name,
512
- assignees: assigneesOf(openStage(input.instance), active.task.name),
540
+ assignees: input.assignees[active.task.name] ?? [],
513
541
  actions: (active.task.actions ?? []).map((a) => a.name)
514
542
  };
515
543
  }
544
+ function blockedState(input) {
545
+ const blocked = input.tasks.find(
546
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length > 0
547
+ );
548
+ if (blocked !== void 0)
549
+ return {
550
+ state: "blocked",
551
+ task: blocked.task.name,
552
+ requirements: blocked.unmetRequirements ?? [],
553
+ assignees: input.assignees[blocked.task.name] ?? []
554
+ };
555
+ }
516
556
  function noTransitionFiresCause(input) {
517
- if (input.transitions.length !== 0 && !input.transitions.some((t) => t.filterSatisfied) && input.tasks.every((t) => isResolved(t.status)))
518
- return { kind: "no-transition-fires" };
557
+ if (input.transitions.length === 0 || input.transitions.some((t) => t.filterSatisfied) || !input.tasks.every((t) => isResolved(t.status)))
558
+ return;
559
+ const undecidable = input.transitions.filter((t) => t.unevaluable);
560
+ return undecidable.length > 0 ? { kind: "transition-unevaluable", transitions: undecidable.map((t) => t.transition.name) } : { kind: "no-transition-fires" };
519
561
  }
520
562
  function diagnoseInstance(input) {
521
563
  const { instance } = input;
@@ -526,7 +568,124 @@ function diagnoseInstance(input) {
526
568
  if (instance.completedAt !== void 0)
527
569
  return { state: "completed", at: instance.completedAt };
528
570
  const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
529
- return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
571
+ return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? blockedState(input) ?? { state: "progressing" };
572
+ }
573
+ const RUNNABLE_VERBS = /* @__PURE__ */ new Set(["set-stage", "abort"]);
574
+ function remediationsForCause(cause) {
575
+ switch (cause.kind) {
576
+ case "failed-effect":
577
+ return [
578
+ {
579
+ verb: "retry-effect",
580
+ rationale: "Re-run the failed effect once the upstream system is healthy."
581
+ },
582
+ { verb: "abort", rationale: "Abort the instance if it can no longer recover." }
583
+ ];
584
+ case "hung-effect":
585
+ return [
586
+ {
587
+ verb: "drain-effects",
588
+ rationale: "Re-run the effect drainer to re-pick the claimed effect."
589
+ },
590
+ { verb: "abort", rationale: "Abort the instance if the effect never drains." }
591
+ ];
592
+ case "failed-task":
593
+ return [
594
+ { verb: "reset-task", rationale: "Reset or skip the failed task." },
595
+ {
596
+ verb: "set-stage",
597
+ rationale: "Move the instance past the failed task to the intended next stage."
598
+ }
599
+ ];
600
+ case "no-transition-fires":
601
+ return [
602
+ {
603
+ verb: "set-stage",
604
+ rationale: "Move the instance to the intended next stage to force it forward."
605
+ }
606
+ ];
607
+ case "transition-unevaluable":
608
+ return [];
609
+ }
610
+ }
611
+ function remediationsFor(diagnosis) {
612
+ return diagnosis.state !== "stuck" ? [] : remediationsForCause(diagnosis.cause).map((seed) => ({
613
+ ...seed,
614
+ available: RUNNABLE_VERBS.has(seed.verb)
615
+ }));
616
+ }
617
+ class ActionParamsInvalidError extends Error {
618
+ action;
619
+ task;
620
+ issues;
621
+ constructor(args) {
622
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
623
+ `);
624
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
625
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
626
+ }
627
+ }
628
+ class RequiredStateNotProvidedError extends Error {
629
+ definition;
630
+ missing;
631
+ constructor(args) {
632
+ const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
633
+ `), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
634
+ super(
635
+ `Required init state${where} was not provided:
636
+ ${lines}
637
+ Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
638
+ ), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
639
+ }
640
+ }
641
+ class WorkflowStateDivergedError extends Error {
642
+ instanceId;
643
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
644
+ guardError;
645
+ /** The rollback failure, when rollback was attempted and itself failed. */
646
+ rollbackError;
647
+ constructor(args) {
648
+ super(
649
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
650
+ { cause: args.guardError }
651
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
652
+ }
653
+ }
654
+ class PartialGuardDeployError extends Error {
655
+ stageName;
656
+ deployed;
657
+ constructor(args) {
658
+ super(
659
+ `Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
660
+ { cause: args.cause }
661
+ ), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
662
+ }
663
+ }
664
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
665
+ class ConcurrentFireActionError extends Error {
666
+ instanceId;
667
+ task;
668
+ action;
669
+ attempts;
670
+ constructor(args) {
671
+ super(
672
+ `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.`
673
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
674
+ }
675
+ }
676
+ function isRevisionConflict(error) {
677
+ if (typeof error != "object" || error === null) return !1;
678
+ const { statusCode, message } = error;
679
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
680
+ }
681
+ class CascadeLimitError extends Error {
682
+ instanceId;
683
+ limit;
684
+ constructor(args) {
685
+ super(
686
+ `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.`
687
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
688
+ }
530
689
  }
531
690
  function buildSnapshot(args) {
532
691
  const docs = [], knownIds = /* @__PURE__ */ new Set();
@@ -573,6 +732,7 @@ function collectWatchRefs(instance) {
573
732
  function readsRaw(ref) {
574
733
  return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
575
734
  }
735
+ const DEFAULT_CONTENT_PERSPECTIVE = "drafts";
576
736
  function contentReleaseName(args) {
577
737
  const { ref, perspective } = args;
578
738
  if (!readsRaw(ref) && Array.isArray(perspective))
@@ -598,6 +758,11 @@ function entryReleaseRefs(state) {
598
758
  (entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
599
759
  );
600
760
  }
761
+ function entrySubjectRefs(state) {
762
+ return stateEntries(state).flatMap(
763
+ (entry) => (entry._type === "doc.ref" || entry._type === "release.ref") && isGdr(entry.value) ? [entry.value] : []
764
+ );
765
+ }
601
766
  function stateEntries(state) {
602
767
  return Array.isArray(state) ? state.filter(
603
768
  (entry) => !!entry && typeof entry == "object"
@@ -622,7 +787,10 @@ async function hydrateSnapshot(args) {
622
787
  };
623
788
  loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
624
789
  for (const ref of collectWatchRefs(instance))
625
- await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
790
+ await loadInto(
791
+ ref.id,
792
+ readsRaw(ref) ? "raw" : instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
793
+ );
626
794
  return buildSnapshot({ docs: loaded });
627
795
  }
628
796
  async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
@@ -637,7 +805,7 @@ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, pers
637
805
  return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
638
806
  }
639
807
  async function readDoc(client, id, perspective) {
640
- return perspective === void 0 ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
808
+ return perspective === "raw" ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
641
809
  }
642
810
  function resourceFromParsed(parsed) {
643
811
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
@@ -738,7 +906,7 @@ async function queryGuardsAcross(clients, query, params) {
738
906
  function dedupById(guards) {
739
907
  return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
740
908
  }
741
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
909
+ const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
742
910
  function resolveGuard(guard, instance, stageName, now) {
743
911
  const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
744
912
  if (targets === null) return null;
@@ -765,11 +933,11 @@ function resolveGuard(guard, instance, stageName, now) {
765
933
  }
766
934
  async function upsertGuard(client, doc) {
767
935
  if (!await client.getDocument(doc._id)) {
768
- await client.create(doc);
936
+ await client.create(doc, SYNC_COMMIT);
769
937
  return;
770
938
  }
771
939
  const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
772
- await client.patch(doc._id).set(body).commit();
940
+ await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
773
941
  }
774
942
  function resolvedStageGuards(args) {
775
943
  const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
@@ -784,15 +952,22 @@ async function committedInstance(args) {
784
952
  }
785
953
  async function deployStageGuards(args) {
786
954
  const live = await committedInstance(args);
787
- if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
788
- for (const { client, doc } of resolvedStageGuards(args))
955
+ if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
956
+ let deployed = 0;
957
+ for (const { client, doc } of resolvedStageGuards(args)) {
958
+ try {
789
959
  await upsertGuard(client, doc);
960
+ } catch (cause) {
961
+ throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
962
+ }
963
+ deployed += 1;
964
+ }
790
965
  }
791
966
  async function retractStageGuards(args) {
792
967
  const live = await committedInstance(args);
793
968
  if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
794
969
  for (const { client, doc } of resolvedStageGuards(args))
795
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
970
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit(SYNC_COMMIT);
796
971
  }
797
972
  async function deleteOrphanedDefinitionGuards(args) {
798
973
  const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
@@ -816,16 +991,32 @@ function randomKey(length = 12) {
816
991
  const bytes = new Uint8Array(length);
817
992
  return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
818
993
  }
819
- async function evaluateCondition(args) {
994
+ function isUnevaluable(result) {
995
+ return result == null;
996
+ }
997
+ async function evaluateConditionOutcome(args) {
820
998
  const { condition, snapshot, params } = args;
821
- return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
999
+ if (condition === void 0) return "satisfied";
1000
+ const result = await runGroq(condition, params, snapshot);
1001
+ return isUnevaluable(result) ? "unevaluable" : result ? "satisfied" : "unsatisfied";
1002
+ }
1003
+ async function evaluateCondition(args) {
1004
+ return await evaluateConditionOutcome(args) === "satisfied";
822
1005
  }
823
1006
  async function evaluatePredicates(args) {
824
1007
  const out = {};
825
- for (const [name, groq] of Object.entries(args.predicates ?? {}))
826
- out[name] = !!await runGroq(groq, args.params, args.snapshot);
1008
+ for (const [name, groq] of Object.entries(args.predicates ?? {})) {
1009
+ const result = await runGroq(groq, args.params, args.snapshot);
1010
+ out[name] = isUnevaluable(result) ? null : !!result;
1011
+ }
827
1012
  return out;
828
1013
  }
1014
+ async function evaluateRequirements(args) {
1015
+ const unmet = [];
1016
+ for (const [name, condition] of Object.entries(args.requirements ?? {}))
1017
+ await evaluateCondition({ condition, snapshot: args.snapshot, params: args.params }) || unmet.push(name);
1018
+ return unmet;
1019
+ }
829
1020
  async function runGroq(groq, params, snapshot) {
830
1021
  const tree = groqJs.parse(groq, { params });
831
1022
  return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
@@ -959,6 +1150,7 @@ function derivePerspectiveFromState(entries) {
959
1150
  async function resolveDeclaredState(args) {
960
1151
  const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
961
1152
  if (entryDefs === void 0 || entryDefs.length === 0) return [];
1153
+ assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
962
1154
  const out = [];
963
1155
  for (const entry of entryDefs) {
964
1156
  const scopedCtx = { ...ctx, resolvedState: out };
@@ -966,6 +1158,17 @@ async function resolveDeclaredState(args) {
966
1158
  }
967
1159
  return out;
968
1160
  }
1161
+ function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
1162
+ const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
1163
+ const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
1164
+ return match === void 0 || match.value === void 0 || match.value === null;
1165
+ }).map((entry) => ({ name: entry.name, type: entry.type }));
1166
+ if (missing.length !== 0)
1167
+ throw new RequiredStateNotProvidedError({
1168
+ ...definitionName !== void 0 ? { definition: definitionName } : {},
1169
+ missing
1170
+ });
1171
+ }
969
1172
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
970
1173
  "doc.refs",
971
1174
  "checklist",
@@ -993,7 +1196,7 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
993
1196
  tag: ctx.tag
994
1197
  });
995
1198
  try {
996
- const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
1199
+ const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
997
1200
  return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
998
1201
  } catch (err) {
999
1202
  throw new Error(
@@ -1123,13 +1326,16 @@ async function ctxConditionParams(ctx, opts) {
1123
1326
  params
1124
1327
  }), ...params };
1125
1328
  }
1126
- async function ctxEvaluateCondition(ctx, condition, opts) {
1127
- return condition === void 0 ? !0 : evaluateCondition({
1329
+ async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
1330
+ return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
1128
1331
  condition,
1129
1332
  snapshot: ctx.snapshot,
1130
1333
  params: await ctxConditionParams(ctx, opts)
1131
1334
  });
1132
1335
  }
1336
+ async function ctxEvaluateCondition(ctx, condition, opts) {
1337
+ return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
1338
+ }
1133
1339
  async function buildEngineContext(args) {
1134
1340
  const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
1135
1341
  return {
@@ -1187,56 +1393,6 @@ async function resolveTaskStateEntries(args) {
1187
1393
  randomKey
1188
1394
  });
1189
1395
  }
1190
- class ActionParamsInvalidError extends Error {
1191
- action;
1192
- task;
1193
- issues;
1194
- constructor(args) {
1195
- const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1196
- `);
1197
- super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1198
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1199
- }
1200
- }
1201
- class WorkflowStateDivergedError extends Error {
1202
- instanceId;
1203
- /** The guard-deploy failure that triggered the (attempted) rollback. */
1204
- guardError;
1205
- /** The rollback failure, when rollback was attempted and itself failed. */
1206
- rollbackError;
1207
- constructor(args) {
1208
- super(
1209
- `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1210
- { cause: args.guardError }
1211
- ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1212
- }
1213
- }
1214
- const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1215
- class ConcurrentFireActionError extends Error {
1216
- instanceId;
1217
- task;
1218
- action;
1219
- attempts;
1220
- constructor(args) {
1221
- super(
1222
- `Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
1223
- ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1224
- }
1225
- }
1226
- function isRevisionConflict(error) {
1227
- if (typeof error != "object" || error === null) return !1;
1228
- const { statusCode, message } = error;
1229
- return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1230
- }
1231
- class CascadeLimitError extends Error {
1232
- instanceId;
1233
- limit;
1234
- constructor(args) {
1235
- super(
1236
- `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
1237
- ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1238
- }
1239
- }
1240
1396
  async function deployOrRollback(args) {
1241
1397
  try {
1242
1398
  await args.deploy();
@@ -1249,7 +1405,7 @@ async function deployOrRollback(args) {
1249
1405
  });
1250
1406
  try {
1251
1407
  let patch = args.client.patch(args.instanceId).set(args.restore);
1252
- args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1408
+ args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit(SYNC_COMMIT);
1253
1409
  } catch (rollbackError) {
1254
1410
  throw new WorkflowStateDivergedError({
1255
1411
  instanceId: args.instanceId,
@@ -1258,7 +1414,11 @@ async function deployOrRollback(args) {
1258
1414
  reason: "rollback of the committed move failed"
1259
1415
  });
1260
1416
  }
1261
- throw guardError;
1417
+ throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
1418
+ instanceId: args.instanceId,
1419
+ guardError,
1420
+ reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
1421
+ }) : guardError;
1262
1422
  }
1263
1423
  }
1264
1424
  function applyTaskStatusChange(args) {
@@ -1347,7 +1507,8 @@ function materializeInstance(base, mutation) {
1347
1507
  ...base,
1348
1508
  currentStage: mutation.currentStage,
1349
1509
  state: mutation.state,
1350
- stages: mutation.stages
1510
+ stages: mutation.stages,
1511
+ effectsContext: mutation.effectsContext
1351
1512
  };
1352
1513
  }
1353
1514
  async function persist(ctx, mutation) {
@@ -1356,13 +1517,27 @@ async function persist(ctx, mutation) {
1356
1517
  lastChangedAt: ctx.now
1357
1518
  }, pendingCreates = mutation.pendingCreates;
1358
1519
  if (pendingCreates.length === 0)
1359
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1520
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
1360
1521
  const tx = ctx.client.transaction();
1361
1522
  for (const body of pendingCreates) tx.create(body);
1362
1523
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1363
1524
  const actorForPriming = void 0;
1364
1525
  for (const body of pendingCreates)
1365
- await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1526
+ try {
1527
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
1528
+ ctx.client,
1529
+ body._id,
1530
+ actorForPriming,
1531
+ ctx.clientForGdr,
1532
+ ctx.clock
1533
+ ), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1534
+ } catch (cause) {
1535
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
1536
+ instanceId: ctx.instance._id,
1537
+ guardError: cause,
1538
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
1539
+ });
1540
+ }
1366
1541
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1367
1542
  if (!reloaded)
1368
1543
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1509,9 +1684,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1509
1684
  const parentScope = await ctxConditionParams(ctx, {
1510
1685
  taskName: task.name,
1511
1686
  ...actor ? { actor } : {}
1512
- }), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1687
+ }), { rows, discoveryResource } = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1513
1688
  for (const row of rows) {
1514
- const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
1689
+ const initialState = await projectRowState(
1690
+ ctx,
1691
+ sub,
1692
+ definition,
1693
+ parentScope,
1694
+ row,
1695
+ discoveryResource
1696
+ ), { ref, body } = await prepareChildInstance({
1515
1697
  client: ctx.client,
1516
1698
  parent: ctx.instance,
1517
1699
  definition,
@@ -1526,28 +1708,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1526
1708
  }
1527
1709
  async function resolveSpawnRows(ctx, sub) {
1528
1710
  const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
1529
- let value;
1711
+ let value, discoveryResource;
1530
1712
  if (groq.includes("*")) {
1531
- const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1532
- value = await discoverItems({
1713
+ const routingUri = entrySubjectRefs(ctx.instance.state)[0]?.id, client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1714
+ client !== ctx.client && routingUri !== void 0 && (discoveryResource = resourceFromGdrUri(routingUri)), value = await discoverItems({
1533
1715
  client,
1534
1716
  groq,
1535
1717
  params,
1536
- ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
1718
+ // Draft-aware by default (drafts overlay published) when there's no
1719
+ // release, so a `forEach` discovers in-flight items the same way the
1720
+ // engine reads any other content.
1721
+ perspective: ctx.instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
1537
1722
  });
1538
1723
  } else {
1539
1724
  const tree = groqJs.parse(groq, { params });
1540
1725
  value = await (await groqJs.evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
1541
1726
  }
1542
- return Array.isArray(value) ? value : value == null ? [] : [value];
1727
+ return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
1543
1728
  }
1544
- function firstDocRefGdrUri(entries) {
1545
- if (entries) {
1546
- for (const s of entries)
1547
- if (s._type === "doc.ref" && s.value !== null) return s.value.id;
1548
- }
1549
- }
1550
- async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1729
+ async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
1551
1730
  const out = [];
1552
1731
  for (const [name, groq] of Object.entries(sub.with ?? {})) {
1553
1732
  const declared = (childDefinition.state ?? []).find((e) => e.name === name);
@@ -1555,7 +1734,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1555
1734
  throw new Error(
1556
1735
  `subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
1557
1736
  );
1558
- const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
1737
+ const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, {
1738
+ parentResource: ctx.instance.workflowResource,
1739
+ discoveryResource,
1740
+ entryName: name,
1741
+ childName: childDefinition.name
1742
+ });
1559
1743
  value != null && out.push({
1560
1744
  type: declared.type,
1561
1745
  name,
@@ -1572,11 +1756,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
1572
1756
  }
1573
1757
  return out;
1574
1758
  }
1575
- function canonicaliseSpawnValue(kind, value, workflowResource) {
1576
- return kind === "doc.ref" ? coerceSpawnGdr(value, workflowResource) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
1759
+ function canonicaliseSpawnValue(kind, value, cx) {
1760
+ return kind === "doc.ref" ? coerceSpawnGdr(value, cx) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, cx)).filter((v2) => v2 !== null) : value;
1577
1761
  }
1578
- function coerceSpawnGdr(raw, workflowResource) {
1579
- const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1762
+ function assertBareIdRootsCorrectly(id, cx) {
1763
+ if (!(cx.discoveryResource === void 0 || sameResource(cx.discoveryResource, cx.parentResource)))
1764
+ throw new Error(
1765
+ `subworkflows.with["${cx.entryName}"]: subworkflow "${cx.childName}" projected the bare id "${id}", which roots at the parent resource "${cx.parentResource.id}", but spawn discovery ran against "${cx.discoveryResource.id}". The child would reference a document that doesn't exist in the parent resource. Project an explicit GDR URI in the with-GROQ (e.g. "dataset:<projectId>:<dataset>:" + _id) so the child's subject points at the resource the discovered rows came from.`
1766
+ );
1767
+ }
1768
+ function coerceSpawnGdr(raw, cx) {
1769
+ const toUri = (id) => isGdrUri(id) ? id : (assertBareIdRootsCorrectly(id, cx), gdrFromResource(cx.parentResource, id));
1580
1770
  if (raw == null) return null;
1581
1771
  if (typeof raw == "object" && "id" in raw && "type" in raw) {
1582
1772
  const r = raw;
@@ -1612,6 +1802,7 @@ async function prepareChildInstance(args) {
1612
1802
  selfId: childDocId,
1613
1803
  tag: childTag,
1614
1804
  workflowResource,
1805
+ definitionName: definition.name,
1615
1806
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1616
1807
  },
1617
1808
  randomKey
@@ -1658,6 +1849,28 @@ async function resolveBindings(args) {
1658
1849
  resolved[key] = await runGroq(groq, args.params, args.snapshot);
1659
1850
  return { ...resolved, ...args.staticInput };
1660
1851
  }
1852
+ async function persistThenDeploy(ctx, mutation, deploy) {
1853
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1854
+ await deployOrRollback({
1855
+ client: ctx.client,
1856
+ instanceId: ctx.instance._id,
1857
+ committedRev: committed._rev,
1858
+ restore: instanceStateFields(ctx.instance),
1859
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1860
+ reversible: !spawned,
1861
+ deploy
1862
+ });
1863
+ }
1864
+ async function refreshStageGuards(ctx, mutation, stageName) {
1865
+ await deployStageGuards({
1866
+ client: ctx.client,
1867
+ clientForGdr: ctx.clientForGdr,
1868
+ instance: materializeInstance(ctx.instance, mutation),
1869
+ definition: ctx.definition,
1870
+ stageName,
1871
+ now: ctx.now
1872
+ });
1873
+ }
1661
1874
  async function completeEffect(args) {
1662
1875
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1663
1876
  ...options?.clock ? { clock: options.clock } : {},
@@ -1703,9 +1916,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1703
1916
  const mutation = startMutation(ctx.instance);
1704
1917
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1705
1918
  const ranAt = ctx.now;
1706
- return mutation.effectHistory.push(
1919
+ mutation.effectHistory.push(
1707
1920
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1708
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1921
+ );
1922
+ const wroteEffectsContext = status === "done" && outputs !== void 0;
1923
+ return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1709
1924
  _key: randomKey(),
1710
1925
  _type: "effectCompleted",
1711
1926
  at: ranAt,
@@ -1715,7 +1930,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1715
1930
  ...outputs !== void 0 ? { outputs } : {},
1716
1931
  ...detail !== void 0 ? { detail } : {},
1717
1932
  ...actor !== void 0 ? { actor } : {}
1718
- }), await persist(ctx, mutation), { effectKey, status };
1933
+ }), await persistThenDeploy(
1934
+ ctx,
1935
+ mutation,
1936
+ () => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
1937
+ ), { effectKey, status };
1719
1938
  }
1720
1939
  function buildQueuedEffect(effect, origin, params, actor, now) {
1721
1940
  const key = randomKey(), pending = {
@@ -2075,21 +2294,24 @@ function resolveMachineStep(mutation, task, entry, now) {
2075
2294
  async function buildStageTasks(ctx, stage) {
2076
2295
  const entries = [];
2077
2296
  for (const task of stage.tasks ?? []) {
2078
- const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
2297
+ const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
2079
2298
  entries.push({
2080
2299
  _key: randomKey(),
2081
2300
  name: task.name,
2082
2301
  status: inScope ? "pending" : "skipped",
2083
- ...task.filter !== void 0 ? {
2084
- filterEvaluation: {
2085
- at: ctx.now,
2086
- truthy: inScope
2087
- }
2088
- } : {}
2302
+ ...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
2089
2303
  });
2090
2304
  }
2091
2305
  return entries;
2092
2306
  }
2307
+ function buildFilterEvaluation(at, filter, outcome) {
2308
+ return outcome === "unevaluable" ? {
2309
+ at,
2310
+ truthy: !1,
2311
+ unevaluable: !0,
2312
+ detail: `Filter "${filter}" could not be evaluated (GROQ null \u2014 a referenced operand was missing or incomparable). Task kept in scope so the gate is not silently skipped.`
2313
+ } : { at, truthy: outcome === "satisfied" };
2314
+ }
2093
2315
  function isTerminalStage(stage) {
2094
2316
  return (stage.transitions ?? []).length === 0;
2095
2317
  }
@@ -2172,28 +2394,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
2172
2394
  now: ctx.now
2173
2395
  });
2174
2396
  }
2175
- async function persistThenDeploy(ctx, mutation, deploy) {
2176
- const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
2177
- await deployOrRollback({
2178
- client: ctx.client,
2179
- instanceId: ctx.instance._id,
2180
- committedRev: committed._rev,
2181
- restore: instanceStateFields(ctx.instance),
2182
- unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
2183
- reversible: !spawned,
2184
- deploy
2185
- });
2186
- }
2187
- async function refreshStageGuards(ctx, mutation, stageName) {
2188
- await deployStageGuards({
2189
- client: ctx.client,
2190
- clientForGdr: ctx.clientForGdr,
2191
- instance: materializeInstance(ctx.instance, mutation),
2192
- definition: ctx.definition,
2193
- stageName,
2194
- now: ctx.now
2195
- });
2196
- }
2197
2397
  async function commitTransition(ctx, actor) {
2198
2398
  if (isTerminal(ctx))
2199
2399
  return { fired: !1 };
@@ -2239,8 +2439,11 @@ async function commitTransition(ctx, actor) {
2239
2439
  };
2240
2440
  }
2241
2441
  async function pickTransition(ctx, stage) {
2242
- for (const candidate of stage.transitions ?? [])
2243
- if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
2442
+ for (const candidate of stage.transitions ?? []) {
2443
+ const outcome = await ctxEvaluateConditionOutcome(ctx, candidate.filter);
2444
+ if (outcome === "satisfied") return candidate;
2445
+ if (outcome === "unevaluable") return;
2446
+ }
2244
2447
  }
2245
2448
  async function evaluateAutoTransitions(args) {
2246
2449
  const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
@@ -2270,7 +2473,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
2270
2473
  }, committed = await client.patch(instance._id).set({
2271
2474
  stages: [initialStageEntry],
2272
2475
  lastChangedAt: now
2273
- }).ifRevisionId(instance._rev).commit();
2476
+ }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
2274
2477
  await deployOrRollback({
2275
2478
  client,
2276
2479
  instanceId: instance._id,
@@ -2532,15 +2735,18 @@ async function evaluateFromSnapshot(args) {
2532
2735
  })
2533
2736
  );
2534
2737
  const transitionEvaluations = [];
2535
- for (const transition of stage.transitions ?? [])
2738
+ for (const transition of stage.transitions ?? []) {
2739
+ const outcome = await evaluateConditionOutcome({
2740
+ condition: transition.filter,
2741
+ snapshot,
2742
+ params: scope
2743
+ });
2536
2744
  transitionEvaluations.push({
2537
2745
  transition,
2538
- filterSatisfied: await evaluateCondition({
2539
- condition: transition.filter,
2540
- snapshot,
2541
- params: scope
2542
- })
2746
+ filterSatisfied: outcome === "satisfied",
2747
+ unevaluable: outcome === "unevaluable"
2543
2748
  });
2749
+ }
2544
2750
  const currentStage = {
2545
2751
  stage,
2546
2752
  tasks: taskEvaluations,
@@ -2588,7 +2794,11 @@ async function evaluateTask(args) {
2588
2794
  ...scopedStateOverlay(instance, snapshot, task.name)
2589
2795
  },
2590
2796
  assigned
2591
- }, actions = [];
2797
+ }, unmetRequirements = await evaluateRequirements({
2798
+ requirements: task.requirements,
2799
+ snapshot,
2800
+ params: taskScope
2801
+ }), requirementsReason = unmetRequirements.length > 0 ? { kind: "requirements-unmet", unmetRequirements } : void 0, actions = [];
2592
2802
  for (const action of task.actions ?? [])
2593
2803
  actions.push(
2594
2804
  await evaluateAction({
@@ -2598,7 +2808,8 @@ async function evaluateTask(args) {
2598
2808
  snapshot,
2599
2809
  taskScope,
2600
2810
  stageHasExits,
2601
- guardDenial
2811
+ guardDenial,
2812
+ requirementsReason
2602
2813
  })
2603
2814
  );
2604
2815
  return {
@@ -2609,12 +2820,22 @@ async function evaluateTask(args) {
2609
2820
  // they're still inbox items. The inbox reads the assignees-kind state
2610
2821
  // entry BY KIND (who-data is state).
2611
2822
  pendingOnActor: (status === "active" || status === "pending") && assigned,
2823
+ ...unmetRequirements.length > 0 ? { unmetRequirements } : {},
2612
2824
  actions
2613
2825
  };
2614
2826
  }
2615
2827
  async function evaluateAction(args) {
2616
- const { action, status, instance, snapshot, taskScope, stageHasExits, guardDenial } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2617
- return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : action.filter !== void 0 && !await evaluateCondition({
2828
+ const {
2829
+ action,
2830
+ status,
2831
+ instance,
2832
+ snapshot,
2833
+ taskScope,
2834
+ stageHasExits,
2835
+ guardDenial,
2836
+ requirementsReason
2837
+ } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2838
+ return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : requirementsReason !== void 0 ? disabled(action, requirementsReason) : action.filter !== void 0 && !await evaluateCondition({
2618
2839
  condition: action.filter,
2619
2840
  snapshot,
2620
2841
  params: taskScope
@@ -2891,7 +3112,8 @@ const disabledReasonDetail = {
2891
3112
  "task-not-active": (r) => `task status is "${r.status}"`,
2892
3113
  "stage-terminal": (r) => `stage "${r.stage}" is terminal`,
2893
3114
  "instance-completed": (r) => `instance completed at ${r.completedAt}`,
2894
- "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
3115
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`,
3116
+ "requirements-unmet": (r) => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`
2895
3117
  };
2896
3118
  function formatDisabledReason(task, action, reason) {
2897
3119
  const detail = disabledReasonDetail[reason.kind](reason);
@@ -3128,6 +3350,7 @@ const workflow = {
3128
3350
  selfId: id,
3129
3351
  tag,
3130
3352
  workflowResource,
3353
+ definitionName: definition.name,
3131
3354
  ...perspective !== void 0 ? { perspective } : {}
3132
3355
  },
3133
3356
  randomKey
@@ -3146,7 +3369,7 @@ const workflow = {
3146
3369
  initialStage: definition.initialStage,
3147
3370
  actor
3148
3371
  });
3149
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
3372
+ return await client.create(base, SYNC_COMMIT), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
3150
3373
  },
3151
3374
  /**
3152
3375
  * Fire an action against a task. If the task is pending, it is
@@ -3386,8 +3609,8 @@ const workflow = {
3386
3609
  * Pure read.
3387
3610
  */
3388
3611
  diagnose: async (args) => {
3389
- const evaluation = await evaluateInstance(args);
3390
- return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
3612
+ const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
3613
+ return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
3391
3614
  },
3392
3615
  /**
3393
3616
  * List the actions an actor could fire on an instance's current stage,
@@ -3556,7 +3779,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
3556
3779
  claimedBy: drainerActor
3557
3780
  };
3558
3781
  try {
3559
- return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
3782
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(SYNC_COMMIT), !0;
3560
3783
  } catch (error) {
3561
3784
  if (!isRevisionConflict(error)) throw error;
3562
3785
  return !1;
@@ -4004,11 +4227,13 @@ function displayDescription(typeKey) {
4004
4227
  return DISPLAY[typeKey]?.description;
4005
4228
  }
4006
4229
  exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
4230
+ exports.isStartableDefinition = schema.isStartableDefinition;
4007
4231
  exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
4008
4232
  exports.ActionDisabledError = ActionDisabledError;
4009
4233
  exports.ActionParamsInvalidError = ActionParamsInvalidError;
4010
4234
  exports.CascadeLimitError = CascadeLimitError;
4011
4235
  exports.ConcurrentFireActionError = ConcurrentFireActionError;
4236
+ exports.DEFAULT_CONTENT_PERSPECTIVE = DEFAULT_CONTENT_PERSPECTIVE;
4012
4237
  exports.DISPLAY = DISPLAY;
4013
4238
  exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
4014
4239
  exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
@@ -4017,12 +4242,13 @@ exports.GUARD_OWNER = GUARD_OWNER;
4017
4242
  exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
4018
4243
  exports.MutationGuardDeniedError = MutationGuardDeniedError;
4019
4244
  exports.OP_DISPLAY = OP_DISPLAY;
4245
+ exports.PartialGuardDeployError = PartialGuardDeployError;
4246
+ exports.RequiredStateNotProvidedError = RequiredStateNotProvidedError;
4020
4247
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
4021
4248
  exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
4022
4249
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
4023
4250
  exports.abortReason = abortReason;
4024
4251
  exports.actionVerdict = actionVerdict;
4025
- exports.assigneesOf = assigneesOf;
4026
4252
  exports.availableActions = availableActions;
4027
4253
  exports.buildSnapshot = buildSnapshot;
4028
4254
  exports.compileGuard = compileGuard;
@@ -4054,13 +4280,13 @@ exports.isDefinitionUnchanged = isDefinitionUnchanged;
4054
4280
  exports.isGdr = isGdr;
4055
4281
  exports.isTerminalStage = isTerminalStage;
4056
4282
  exports.lakeGuardId = lakeGuardId;
4057
- exports.openStage = openStage;
4058
4283
  exports.parseGdr = parseGdr;
4059
4284
  exports.readsRaw = readsRaw;
4060
4285
  exports.refCanvas = refCanvas;
4061
4286
  exports.refDashboard = refDashboard;
4062
4287
  exports.refDataset = refDataset;
4063
4288
  exports.refMediaLibrary = refMediaLibrary;
4289
+ exports.remediationsFor = remediationsFor;
4064
4290
  exports.resolveAccess = resolveAccess;
4065
4291
  exports.resourceFromParsed = resourceFromParsed;
4066
4292
  exports.retractStageGuards = retractStageGuards;