@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.js CHANGED
@@ -1,13 +1,9 @@
1
1
  import { parse, evaluate } from "groq-js";
2
2
  import { isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
3
+ import { isStartableDefinition } from "./_chunks-es/schema.js";
3
4
  import * as v from "valibot";
4
- function getPath(value, path) {
5
- let current = value;
6
- for (const part of path.split(".")) {
7
- if (current == null || typeof current != "object") return;
8
- current = current[part];
9
- }
10
- return current;
5
+ function findOpenStageEntry(host) {
6
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
11
7
  }
12
8
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
13
9
  function parseGdr(uri) {
@@ -79,6 +75,21 @@ function gdrFromResource(res, documentId) {
79
75
  }
80
76
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
81
77
  }
78
+ function resourceFromGdrUri(uri) {
79
+ let parsed;
80
+ try {
81
+ parsed = parseGdr(uri);
82
+ } catch {
83
+ return;
84
+ }
85
+ if (parsed.scheme === "dataset")
86
+ return parsed.projectId === void 0 || parsed.dataset === void 0 ? void 0 : { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` };
87
+ if (parsed.resourceId !== void 0)
88
+ return { type: parsed.scheme, id: parsed.resourceId };
89
+ }
90
+ function sameResource(a, b) {
91
+ return a.type === b.type && a.id === b.id;
92
+ }
82
93
  function selfGdr(doc) {
83
94
  return gdrFromResource(doc.workflowResource, doc._id);
84
95
  }
@@ -96,12 +107,100 @@ function gdrRef(res, documentId, type) {
96
107
  function isGdr(value) {
97
108
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
98
109
  }
110
+ function buildParams(args) {
111
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
112
+ return {
113
+ self: selfGdr(instance),
114
+ state: renderedState(instance.state ?? [], snapshot),
115
+ parent: instance.ancestors.at(-1)?.id ?? null,
116
+ ancestors: instance.ancestors.map((a) => a.id),
117
+ stage: instance.currentStage,
118
+ now,
119
+ /** `$effects` — parent context handoff by entry name, completed-effect
120
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
121
+ effects: effectsContextMap(instance),
122
+ /** `$tasks` — the current stage's task rows, statuses included. */
123
+ tasks: currentTasks2,
124
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
125
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
126
+ ...extra
127
+ };
128
+ }
129
+ function renderedState(entries, snapshot) {
130
+ const out = {};
131
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
132
+ return out;
133
+ }
134
+ function renderedValue(entry, snapshot) {
135
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
136
+ }
137
+ function scopedStateOverlay(instance, snapshot, taskName) {
138
+ const stageEntry = findOpenStageEntry(instance);
139
+ if (stageEntry === void 0) return {};
140
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
141
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
142
+ }
143
+ function assignedFor(instance, taskName, actor) {
144
+ if (actor === void 0) return !1;
145
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
146
+ return entry === void 0 ? !1 : entry.value.some(
147
+ (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
148
+ );
149
+ }
150
+ function effectsContextMap(instance) {
151
+ const out = {};
152
+ for (const entry of instance.effectsContext)
153
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
154
+ return out;
155
+ }
156
+ function parseJsonContextEntry(entry) {
157
+ try {
158
+ return JSON.parse(entry.value);
159
+ } catch (err) {
160
+ throw new Error(
161
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
162
+ { cause: err }
163
+ );
164
+ }
165
+ }
166
+ function paramsForLake(params) {
167
+ return {
168
+ ...params,
169
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
170
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
171
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
172
+ state: stripStateForLake(params.state)
173
+ };
174
+ }
175
+ function stripStateForLake(value) {
176
+ if (value == null) return value;
177
+ if (typeof value == "string") return bareId(value);
178
+ if (Array.isArray(value)) return value.map(stripStateForLake);
179
+ if (typeof value == "object") {
180
+ const out = {};
181
+ for (const [k, v2] of Object.entries(value))
182
+ out[k] = stripStateForLake(v2);
183
+ return out;
184
+ }
185
+ return value;
186
+ }
187
+ function bareId(id) {
188
+ return isGdrUri(id) ? extractDocumentId(id) : id;
189
+ }
190
+ function getPath(value, path) {
191
+ let current = value;
192
+ for (const part of path.split(".")) {
193
+ if (current == null || typeof current != "object") return;
194
+ current = current[part];
195
+ }
196
+ return current;
197
+ }
99
198
  function resourceOf(p) {
100
199
  return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
101
200
  }
102
- const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
201
+ const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
103
202
  function isGuardReadExpr(expr) {
104
- return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
203
+ return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
105
204
  }
106
205
  function resolveGuardRead(expr, ctx) {
107
206
  if (expr === "$self") return selfGdr(ctx.instance);
@@ -111,8 +210,13 @@ function resolveGuardRead(expr, ctx) {
111
210
  const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
112
211
  return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
113
212
  }
213
+ const effectsRead = EFFECTS_READ.exec(expr);
214
+ if (effectsRead !== null) {
215
+ const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
216
+ return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
217
+ }
114
218
  throw new Error(
115
- `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
219
+ `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)`
116
220
  );
117
221
  }
118
222
  function resolveIdRefTarget(expr, ctx) {
@@ -194,7 +298,7 @@ function createDefinitionValidator() {
194
298
  entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
195
299
  }, checkGuardRead: (expr, where) => {
196
300
  isGuardReadExpr(expr) || errors.push(
197
- ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
301
+ ` \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)`
198
302
  );
199
303
  } };
200
304
  }
@@ -223,6 +327,8 @@ function validateTask(v2, stageName, task) {
223
327
  for (const entry of task.state ?? [])
224
328
  v2.checkEntry(entry, `${where}.state "${entry.name}"`);
225
329
  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`);
330
+ for (const [name, groq] of Object.entries(task.requirements ?? {}))
331
+ v2.checkCondition(groq, `${where}.requirements "${name}"`);
226
332
  for (const [key, groq] of effectBindings(task.effects))
227
333
  v2.tryParse(groq, `${where} effect binding "${key}"`);
228
334
  validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
@@ -371,99 +477,20 @@ async function assertInstanceWriteAllowed(args) {
371
477
  guards: denied
372
478
  });
373
479
  }
374
- function findOpenStageEntry(host) {
375
- return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
376
- }
377
- function buildParams(args) {
378
- const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
379
- return {
380
- self: selfGdr(instance),
381
- state: renderedState(instance.state ?? [], snapshot),
382
- parent: instance.ancestors.at(-1)?.id ?? null,
383
- ancestors: instance.ancestors.map((a) => a.id),
384
- stage: instance.currentStage,
385
- now,
386
- /** `$effects` — parent context handoff by entry name, completed-effect
387
- * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
388
- effects: effectsContextMap(instance),
389
- /** `$tasks` — the current stage's task rows, statuses included. */
390
- tasks: currentTasks2,
391
- allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
392
- anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
393
- ...extra
394
- };
395
- }
396
- function renderedState(entries, snapshot) {
397
- const out = {};
398
- for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
399
- return out;
400
- }
401
- function renderedValue(entry, snapshot) {
402
- return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
403
- }
404
- function scopedStateOverlay(instance, snapshot, taskName) {
405
- const stageEntry = findOpenStageEntry(instance);
406
- if (stageEntry === void 0) return {};
407
- const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
408
- return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
409
- }
410
- function assignedFor(instance, taskName, actor) {
411
- if (actor === void 0) return !1;
412
- const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
413
- return entry === void 0 ? !1 : entry.value.some(
414
- (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
415
- );
416
- }
417
- function effectsContextMap(instance) {
418
- const out = {};
419
- for (const entry of instance.effectsContext)
420
- out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
421
- return out;
422
- }
423
- function parseJsonContextEntry(entry) {
424
- try {
425
- return JSON.parse(entry.value);
426
- } catch (err) {
427
- throw new Error(
428
- `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
429
- { cause: err }
430
- );
431
- }
432
- }
433
- function paramsForLake(params) {
434
- return {
435
- ...params,
436
- self: typeof params.self == "string" ? bareId(params.self) : params.self,
437
- parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
438
- ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
439
- state: stripStateForLake(params.state)
440
- };
441
- }
442
- function stripStateForLake(value) {
443
- if (value == null) return value;
444
- if (typeof value == "string") return bareId(value);
445
- if (Array.isArray(value)) return value.map(stripStateForLake);
446
- if (typeof value == "object") {
447
- const out = {};
448
- for (const [k, v2] of Object.entries(value))
449
- out[k] = stripStateForLake(v2);
450
- return out;
451
- }
452
- return value;
453
- }
454
- function bareId(id) {
455
- return isGdrUri(id) ? extractDocumentId(id) : id;
456
- }
457
480
  function abortReason(instance) {
458
481
  return instance.history.find(
459
482
  (h) => h._type === "aborted"
460
483
  )?.reason;
461
484
  }
462
485
  function diagnoseInputFromEvaluation(evaluation) {
486
+ const stage = openStage(evaluation.instance);
463
487
  return {
464
488
  instance: evaluation.instance,
465
489
  tasks: evaluation.currentStage.tasks,
466
- transitions: evaluation.currentStage.transitions
490
+ transitions: evaluation.currentStage.transitions,
491
+ assignees: Object.fromEntries(
492
+ evaluation.currentStage.tasks.map((t) => [t.task.name, assigneesOf(stage, t.task.name)])
493
+ )
467
494
  };
468
495
  }
469
496
  function openStage(instance) {
@@ -488,18 +515,34 @@ function failedTaskCause(input) {
488
515
  return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
489
516
  }
490
517
  function waitingState(input) {
491
- const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
518
+ const active = input.tasks.find(
519
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length === 0
520
+ );
492
521
  if (active !== void 0)
493
522
  return {
494
523
  state: "waiting",
495
524
  task: active.task.name,
496
- assignees: assigneesOf(openStage(input.instance), active.task.name),
525
+ assignees: input.assignees[active.task.name] ?? [],
497
526
  actions: (active.task.actions ?? []).map((a) => a.name)
498
527
  };
499
528
  }
529
+ function blockedState(input) {
530
+ const blocked = input.tasks.find(
531
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length > 0
532
+ );
533
+ if (blocked !== void 0)
534
+ return {
535
+ state: "blocked",
536
+ task: blocked.task.name,
537
+ requirements: blocked.unmetRequirements ?? [],
538
+ assignees: input.assignees[blocked.task.name] ?? []
539
+ };
540
+ }
500
541
  function noTransitionFiresCause(input) {
501
- if (input.transitions.length !== 0 && !input.transitions.some((t) => t.filterSatisfied) && input.tasks.every((t) => isResolved(t.status)))
502
- return { kind: "no-transition-fires" };
542
+ if (input.transitions.length === 0 || input.transitions.some((t) => t.filterSatisfied) || !input.tasks.every((t) => isResolved(t.status)))
543
+ return;
544
+ const undecidable = input.transitions.filter((t) => t.unevaluable);
545
+ return undecidable.length > 0 ? { kind: "transition-unevaluable", transitions: undecidable.map((t) => t.transition.name) } : { kind: "no-transition-fires" };
503
546
  }
504
547
  function diagnoseInstance(input) {
505
548
  const { instance } = input;
@@ -510,7 +553,124 @@ function diagnoseInstance(input) {
510
553
  if (instance.completedAt !== void 0)
511
554
  return { state: "completed", at: instance.completedAt };
512
555
  const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
513
- return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
556
+ return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? blockedState(input) ?? { state: "progressing" };
557
+ }
558
+ const RUNNABLE_VERBS = /* @__PURE__ */ new Set(["set-stage", "abort"]);
559
+ function remediationsForCause(cause) {
560
+ switch (cause.kind) {
561
+ case "failed-effect":
562
+ return [
563
+ {
564
+ verb: "retry-effect",
565
+ rationale: "Re-run the failed effect once the upstream system is healthy."
566
+ },
567
+ { verb: "abort", rationale: "Abort the instance if it can no longer recover." }
568
+ ];
569
+ case "hung-effect":
570
+ return [
571
+ {
572
+ verb: "drain-effects",
573
+ rationale: "Re-run the effect drainer to re-pick the claimed effect."
574
+ },
575
+ { verb: "abort", rationale: "Abort the instance if the effect never drains." }
576
+ ];
577
+ case "failed-task":
578
+ return [
579
+ { verb: "reset-task", rationale: "Reset or skip the failed task." },
580
+ {
581
+ verb: "set-stage",
582
+ rationale: "Move the instance past the failed task to the intended next stage."
583
+ }
584
+ ];
585
+ case "no-transition-fires":
586
+ return [
587
+ {
588
+ verb: "set-stage",
589
+ rationale: "Move the instance to the intended next stage to force it forward."
590
+ }
591
+ ];
592
+ case "transition-unevaluable":
593
+ return [];
594
+ }
595
+ }
596
+ function remediationsFor(diagnosis) {
597
+ return diagnosis.state !== "stuck" ? [] : remediationsForCause(diagnosis.cause).map((seed) => ({
598
+ ...seed,
599
+ available: RUNNABLE_VERBS.has(seed.verb)
600
+ }));
601
+ }
602
+ class ActionParamsInvalidError extends Error {
603
+ action;
604
+ task;
605
+ issues;
606
+ constructor(args) {
607
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
608
+ `);
609
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
610
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
611
+ }
612
+ }
613
+ class RequiredStateNotProvidedError extends Error {
614
+ definition;
615
+ missing;
616
+ constructor(args) {
617
+ const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
618
+ `), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
619
+ super(
620
+ `Required init state${where} was not provided:
621
+ ${lines}
622
+ Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
623
+ ), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
624
+ }
625
+ }
626
+ class WorkflowStateDivergedError extends Error {
627
+ instanceId;
628
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
629
+ guardError;
630
+ /** The rollback failure, when rollback was attempted and itself failed. */
631
+ rollbackError;
632
+ constructor(args) {
633
+ super(
634
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
635
+ { cause: args.guardError }
636
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
637
+ }
638
+ }
639
+ class PartialGuardDeployError extends Error {
640
+ stageName;
641
+ deployed;
642
+ constructor(args) {
643
+ super(
644
+ `Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
645
+ { cause: args.cause }
646
+ ), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
647
+ }
648
+ }
649
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
650
+ class ConcurrentFireActionError extends Error {
651
+ instanceId;
652
+ task;
653
+ action;
654
+ attempts;
655
+ constructor(args) {
656
+ super(
657
+ `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.`
658
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
659
+ }
660
+ }
661
+ function isRevisionConflict(error) {
662
+ if (typeof error != "object" || error === null) return !1;
663
+ const { statusCode, message } = error;
664
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
665
+ }
666
+ class CascadeLimitError extends Error {
667
+ instanceId;
668
+ limit;
669
+ constructor(args) {
670
+ super(
671
+ `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.`
672
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
673
+ }
514
674
  }
515
675
  function buildSnapshot(args) {
516
676
  const docs = [], knownIds = /* @__PURE__ */ new Set();
@@ -557,6 +717,7 @@ function collectWatchRefs(instance) {
557
717
  function readsRaw(ref) {
558
718
  return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
559
719
  }
720
+ const DEFAULT_CONTENT_PERSPECTIVE = "drafts";
560
721
  function contentReleaseName(args) {
561
722
  const { ref, perspective } = args;
562
723
  if (!readsRaw(ref) && Array.isArray(perspective))
@@ -582,6 +743,11 @@ function entryReleaseRefs(state) {
582
743
  (entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
583
744
  );
584
745
  }
746
+ function entrySubjectRefs(state) {
747
+ return stateEntries(state).flatMap(
748
+ (entry) => (entry._type === "doc.ref" || entry._type === "release.ref") && isGdr(entry.value) ? [entry.value] : []
749
+ );
750
+ }
585
751
  function stateEntries(state) {
586
752
  return Array.isArray(state) ? state.filter(
587
753
  (entry) => !!entry && typeof entry == "object"
@@ -606,7 +772,10 @@ async function hydrateSnapshot(args) {
606
772
  };
607
773
  loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
608
774
  for (const ref of collectWatchRefs(instance))
609
- await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
775
+ await loadInto(
776
+ ref.id,
777
+ readsRaw(ref) ? "raw" : instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
778
+ );
610
779
  return buildSnapshot({ docs: loaded });
611
780
  }
612
781
  async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
@@ -621,7 +790,7 @@ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, pers
621
790
  return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
622
791
  }
623
792
  async function readDoc(client, id, perspective) {
624
- return perspective === void 0 ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
793
+ return perspective === "raw" ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
625
794
  }
626
795
  function resourceFromParsed(parsed) {
627
796
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
@@ -722,7 +891,7 @@ async function queryGuardsAcross(clients, query, params) {
722
891
  function dedupById(guards) {
723
892
  return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
724
893
  }
725
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
894
+ const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
726
895
  function resolveGuard(guard, instance, stageName, now) {
727
896
  const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
728
897
  if (targets === null) return null;
@@ -749,11 +918,11 @@ function resolveGuard(guard, instance, stageName, now) {
749
918
  }
750
919
  async function upsertGuard(client, doc) {
751
920
  if (!await client.getDocument(doc._id)) {
752
- await client.create(doc);
921
+ await client.create(doc, SYNC_COMMIT);
753
922
  return;
754
923
  }
755
924
  const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
756
- await client.patch(doc._id).set(body).commit();
925
+ await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
757
926
  }
758
927
  function resolvedStageGuards(args) {
759
928
  const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
@@ -768,15 +937,22 @@ async function committedInstance(args) {
768
937
  }
769
938
  async function deployStageGuards(args) {
770
939
  const live = await committedInstance(args);
771
- if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
772
- for (const { client, doc } of resolvedStageGuards(args))
940
+ if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
941
+ let deployed = 0;
942
+ for (const { client, doc } of resolvedStageGuards(args)) {
943
+ try {
773
944
  await upsertGuard(client, doc);
945
+ } catch (cause) {
946
+ throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
947
+ }
948
+ deployed += 1;
949
+ }
774
950
  }
775
951
  async function retractStageGuards(args) {
776
952
  const live = await committedInstance(args);
777
953
  if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
778
954
  for (const { client, doc } of resolvedStageGuards(args))
779
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
955
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit(SYNC_COMMIT);
780
956
  }
781
957
  async function deleteOrphanedDefinitionGuards(args) {
782
958
  const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
@@ -800,16 +976,32 @@ function randomKey(length = 12) {
800
976
  const bytes = new Uint8Array(length);
801
977
  return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
802
978
  }
803
- async function evaluateCondition(args) {
979
+ function isUnevaluable(result) {
980
+ return result == null;
981
+ }
982
+ async function evaluateConditionOutcome(args) {
804
983
  const { condition, snapshot, params } = args;
805
- return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
984
+ if (condition === void 0) return "satisfied";
985
+ const result = await runGroq(condition, params, snapshot);
986
+ return isUnevaluable(result) ? "unevaluable" : result ? "satisfied" : "unsatisfied";
987
+ }
988
+ async function evaluateCondition(args) {
989
+ return await evaluateConditionOutcome(args) === "satisfied";
806
990
  }
807
991
  async function evaluatePredicates(args) {
808
992
  const out = {};
809
- for (const [name, groq] of Object.entries(args.predicates ?? {}))
810
- out[name] = !!await runGroq(groq, args.params, args.snapshot);
993
+ for (const [name, groq] of Object.entries(args.predicates ?? {})) {
994
+ const result = await runGroq(groq, args.params, args.snapshot);
995
+ out[name] = isUnevaluable(result) ? null : !!result;
996
+ }
811
997
  return out;
812
998
  }
999
+ async function evaluateRequirements(args) {
1000
+ const unmet = [];
1001
+ for (const [name, condition] of Object.entries(args.requirements ?? {}))
1002
+ await evaluateCondition({ condition, snapshot: args.snapshot, params: args.params }) || unmet.push(name);
1003
+ return unmet;
1004
+ }
813
1005
  async function runGroq(groq, params, snapshot) {
814
1006
  const tree = parse(groq, { params });
815
1007
  return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
@@ -943,6 +1135,7 @@ function derivePerspectiveFromState(entries) {
943
1135
  async function resolveDeclaredState(args) {
944
1136
  const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
945
1137
  if (entryDefs === void 0 || entryDefs.length === 0) return [];
1138
+ assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
946
1139
  const out = [];
947
1140
  for (const entry of entryDefs) {
948
1141
  const scopedCtx = { ...ctx, resolvedState: out };
@@ -950,6 +1143,17 @@ async function resolveDeclaredState(args) {
950
1143
  }
951
1144
  return out;
952
1145
  }
1146
+ function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
1147
+ const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
1148
+ const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
1149
+ return match === void 0 || match.value === void 0 || match.value === null;
1150
+ }).map((entry) => ({ name: entry.name, type: entry.type }));
1151
+ if (missing.length !== 0)
1152
+ throw new RequiredStateNotProvidedError({
1153
+ ...definitionName !== void 0 ? { definition: definitionName } : {},
1154
+ missing
1155
+ });
1156
+ }
953
1157
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
954
1158
  "doc.refs",
955
1159
  "checklist",
@@ -977,7 +1181,7 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
977
1181
  tag: ctx.tag
978
1182
  });
979
1183
  try {
980
- const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
1184
+ const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
981
1185
  return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
982
1186
  } catch (err) {
983
1187
  throw new Error(
@@ -1107,13 +1311,16 @@ async function ctxConditionParams(ctx, opts) {
1107
1311
  params
1108
1312
  }), ...params };
1109
1313
  }
1110
- async function ctxEvaluateCondition(ctx, condition, opts) {
1111
- return condition === void 0 ? !0 : evaluateCondition({
1314
+ async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
1315
+ return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
1112
1316
  condition,
1113
1317
  snapshot: ctx.snapshot,
1114
1318
  params: await ctxConditionParams(ctx, opts)
1115
1319
  });
1116
1320
  }
1321
+ async function ctxEvaluateCondition(ctx, condition, opts) {
1322
+ return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
1323
+ }
1117
1324
  async function buildEngineContext(args) {
1118
1325
  const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
1119
1326
  return {
@@ -1171,56 +1378,6 @@ async function resolveTaskStateEntries(args) {
1171
1378
  randomKey
1172
1379
  });
1173
1380
  }
1174
- class ActionParamsInvalidError extends Error {
1175
- action;
1176
- task;
1177
- issues;
1178
- constructor(args) {
1179
- const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1180
- `);
1181
- super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1182
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1183
- }
1184
- }
1185
- class WorkflowStateDivergedError extends Error {
1186
- instanceId;
1187
- /** The guard-deploy failure that triggered the (attempted) rollback. */
1188
- guardError;
1189
- /** The rollback failure, when rollback was attempted and itself failed. */
1190
- rollbackError;
1191
- constructor(args) {
1192
- super(
1193
- `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1194
- { cause: args.guardError }
1195
- ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1196
- }
1197
- }
1198
- const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1199
- class ConcurrentFireActionError extends Error {
1200
- instanceId;
1201
- task;
1202
- action;
1203
- attempts;
1204
- constructor(args) {
1205
- super(
1206
- `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.`
1207
- ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1208
- }
1209
- }
1210
- function isRevisionConflict(error) {
1211
- if (typeof error != "object" || error === null) return !1;
1212
- const { statusCode, message } = error;
1213
- return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1214
- }
1215
- class CascadeLimitError extends Error {
1216
- instanceId;
1217
- limit;
1218
- constructor(args) {
1219
- super(
1220
- `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.`
1221
- ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1222
- }
1223
- }
1224
1381
  async function deployOrRollback(args) {
1225
1382
  try {
1226
1383
  await args.deploy();
@@ -1233,7 +1390,7 @@ async function deployOrRollback(args) {
1233
1390
  });
1234
1391
  try {
1235
1392
  let patch = args.client.patch(args.instanceId).set(args.restore);
1236
- args.unset && args.unset.length > 0 && (patch = patch.unset(args.unset)), args.committedRev !== void 0 && (patch = patch.ifRevisionId(args.committedRev)), await patch.commit();
1393
+ 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);
1237
1394
  } catch (rollbackError) {
1238
1395
  throw new WorkflowStateDivergedError({
1239
1396
  instanceId: args.instanceId,
@@ -1242,7 +1399,11 @@ async function deployOrRollback(args) {
1242
1399
  reason: "rollback of the committed move failed"
1243
1400
  });
1244
1401
  }
1245
- throw guardError;
1402
+ throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
1403
+ instanceId: args.instanceId,
1404
+ guardError,
1405
+ reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
1406
+ }) : guardError;
1246
1407
  }
1247
1408
  }
1248
1409
  function applyTaskStatusChange(args) {
@@ -1331,7 +1492,8 @@ function materializeInstance(base, mutation) {
1331
1492
  ...base,
1332
1493
  currentStage: mutation.currentStage,
1333
1494
  state: mutation.state,
1334
- stages: mutation.stages
1495
+ stages: mutation.stages,
1496
+ effectsContext: mutation.effectsContext
1335
1497
  };
1336
1498
  }
1337
1499
  async function persist(ctx, mutation) {
@@ -1340,13 +1502,27 @@ async function persist(ctx, mutation) {
1340
1502
  lastChangedAt: ctx.now
1341
1503
  }, pendingCreates = mutation.pendingCreates;
1342
1504
  if (pendingCreates.length === 0)
1343
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1505
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
1344
1506
  const tx = ctx.client.transaction();
1345
1507
  for (const body of pendingCreates) tx.create(body);
1346
1508
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1347
1509
  const actorForPriming = void 0;
1348
1510
  for (const body of pendingCreates)
1349
- 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);
1511
+ try {
1512
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
1513
+ ctx.client,
1514
+ body._id,
1515
+ actorForPriming,
1516
+ ctx.clientForGdr,
1517
+ ctx.clock
1518
+ ), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1519
+ } catch (cause) {
1520
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
1521
+ instanceId: ctx.instance._id,
1522
+ guardError: cause,
1523
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
1524
+ });
1525
+ }
1350
1526
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1351
1527
  if (!reloaded)
1352
1528
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1493,9 +1669,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1493
1669
  const parentScope = await ctxConditionParams(ctx, {
1494
1670
  taskName: task.name,
1495
1671
  ...actor ? { actor } : {}
1496
- }), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1672
+ }), { rows, discoveryResource } = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1497
1673
  for (const row of rows) {
1498
- const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
1674
+ const initialState = await projectRowState(
1675
+ ctx,
1676
+ sub,
1677
+ definition,
1678
+ parentScope,
1679
+ row,
1680
+ discoveryResource
1681
+ ), { ref, body } = await prepareChildInstance({
1499
1682
  client: ctx.client,
1500
1683
  parent: ctx.instance,
1501
1684
  definition,
@@ -1510,28 +1693,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1510
1693
  }
1511
1694
  async function resolveSpawnRows(ctx, sub) {
1512
1695
  const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
1513
- let value;
1696
+ let value, discoveryResource;
1514
1697
  if (groq.includes("*")) {
1515
- const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1516
- value = await discoverItems({
1698
+ const routingUri = entrySubjectRefs(ctx.instance.state)[0]?.id, client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1699
+ client !== ctx.client && routingUri !== void 0 && (discoveryResource = resourceFromGdrUri(routingUri)), value = await discoverItems({
1517
1700
  client,
1518
1701
  groq,
1519
1702
  params,
1520
- ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
1703
+ // Draft-aware by default (drafts overlay published) when there's no
1704
+ // release, so a `forEach` discovers in-flight items the same way the
1705
+ // engine reads any other content.
1706
+ perspective: ctx.instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
1521
1707
  });
1522
1708
  } else {
1523
1709
  const tree = parse(groq, { params });
1524
1710
  value = await (await evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
1525
1711
  }
1526
- return Array.isArray(value) ? value : value == null ? [] : [value];
1712
+ return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
1527
1713
  }
1528
- function firstDocRefGdrUri(entries) {
1529
- if (entries) {
1530
- for (const s of entries)
1531
- if (s._type === "doc.ref" && s.value !== null) return s.value.id;
1532
- }
1533
- }
1534
- async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1714
+ async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
1535
1715
  const out = [];
1536
1716
  for (const [name, groq] of Object.entries(sub.with ?? {})) {
1537
1717
  const declared = (childDefinition.state ?? []).find((e) => e.name === name);
@@ -1539,7 +1719,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1539
1719
  throw new Error(
1540
1720
  `subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
1541
1721
  );
1542
- const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
1722
+ const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, {
1723
+ parentResource: ctx.instance.workflowResource,
1724
+ discoveryResource,
1725
+ entryName: name,
1726
+ childName: childDefinition.name
1727
+ });
1543
1728
  value != null && out.push({
1544
1729
  type: declared.type,
1545
1730
  name,
@@ -1556,11 +1741,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
1556
1741
  }
1557
1742
  return out;
1558
1743
  }
1559
- function canonicaliseSpawnValue(kind, value, workflowResource) {
1560
- return kind === "doc.ref" ? coerceSpawnGdr(value, workflowResource) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, workflowResource)).filter((v2) => v2 !== null) : value;
1744
+ function canonicaliseSpawnValue(kind, value, cx) {
1745
+ return kind === "doc.ref" ? coerceSpawnGdr(value, cx) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, cx)).filter((v2) => v2 !== null) : value;
1561
1746
  }
1562
- function coerceSpawnGdr(raw, workflowResource) {
1563
- const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1747
+ function assertBareIdRootsCorrectly(id, cx) {
1748
+ if (!(cx.discoveryResource === void 0 || sameResource(cx.discoveryResource, cx.parentResource)))
1749
+ throw new Error(
1750
+ `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.`
1751
+ );
1752
+ }
1753
+ function coerceSpawnGdr(raw, cx) {
1754
+ const toUri = (id) => isGdrUri(id) ? id : (assertBareIdRootsCorrectly(id, cx), gdrFromResource(cx.parentResource, id));
1564
1755
  if (raw == null) return null;
1565
1756
  if (typeof raw == "object" && "id" in raw && "type" in raw) {
1566
1757
  const r = raw;
@@ -1596,6 +1787,7 @@ async function prepareChildInstance(args) {
1596
1787
  selfId: childDocId,
1597
1788
  tag: childTag,
1598
1789
  workflowResource,
1790
+ definitionName: definition.name,
1599
1791
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1600
1792
  },
1601
1793
  randomKey
@@ -1642,6 +1834,28 @@ async function resolveBindings(args) {
1642
1834
  resolved[key] = await runGroq(groq, args.params, args.snapshot);
1643
1835
  return { ...resolved, ...args.staticInput };
1644
1836
  }
1837
+ async function persistThenDeploy(ctx, mutation, deploy) {
1838
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1839
+ await deployOrRollback({
1840
+ client: ctx.client,
1841
+ instanceId: ctx.instance._id,
1842
+ committedRev: committed._rev,
1843
+ restore: instanceStateFields(ctx.instance),
1844
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1845
+ reversible: !spawned,
1846
+ deploy
1847
+ });
1848
+ }
1849
+ async function refreshStageGuards(ctx, mutation, stageName) {
1850
+ await deployStageGuards({
1851
+ client: ctx.client,
1852
+ clientForGdr: ctx.clientForGdr,
1853
+ instance: materializeInstance(ctx.instance, mutation),
1854
+ definition: ctx.definition,
1855
+ stageName,
1856
+ now: ctx.now
1857
+ });
1858
+ }
1645
1859
  async function completeEffect(args) {
1646
1860
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1647
1861
  ...options?.clock ? { clock: options.clock } : {},
@@ -1687,9 +1901,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1687
1901
  const mutation = startMutation(ctx.instance);
1688
1902
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1689
1903
  const ranAt = ctx.now;
1690
- return mutation.effectHistory.push(
1904
+ mutation.effectHistory.push(
1691
1905
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1692
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1906
+ );
1907
+ const wroteEffectsContext = status === "done" && outputs !== void 0;
1908
+ return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1693
1909
  _key: randomKey(),
1694
1910
  _type: "effectCompleted",
1695
1911
  at: ranAt,
@@ -1699,7 +1915,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1699
1915
  ...outputs !== void 0 ? { outputs } : {},
1700
1916
  ...detail !== void 0 ? { detail } : {},
1701
1917
  ...actor !== void 0 ? { actor } : {}
1702
- }), await persist(ctx, mutation), { effectKey, status };
1918
+ }), await persistThenDeploy(
1919
+ ctx,
1920
+ mutation,
1921
+ () => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
1922
+ ), { effectKey, status };
1703
1923
  }
1704
1924
  function buildQueuedEffect(effect, origin, params, actor, now) {
1705
1925
  const key = randomKey(), pending = {
@@ -2059,21 +2279,24 @@ function resolveMachineStep(mutation, task, entry, now) {
2059
2279
  async function buildStageTasks(ctx, stage) {
2060
2280
  const entries = [];
2061
2281
  for (const task of stage.tasks ?? []) {
2062
- const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
2282
+ const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
2063
2283
  entries.push({
2064
2284
  _key: randomKey(),
2065
2285
  name: task.name,
2066
2286
  status: inScope ? "pending" : "skipped",
2067
- ...task.filter !== void 0 ? {
2068
- filterEvaluation: {
2069
- at: ctx.now,
2070
- truthy: inScope
2071
- }
2072
- } : {}
2287
+ ...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
2073
2288
  });
2074
2289
  }
2075
2290
  return entries;
2076
2291
  }
2292
+ function buildFilterEvaluation(at, filter, outcome) {
2293
+ return outcome === "unevaluable" ? {
2294
+ at,
2295
+ truthy: !1,
2296
+ unevaluable: !0,
2297
+ 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.`
2298
+ } : { at, truthy: outcome === "satisfied" };
2299
+ }
2077
2300
  function isTerminalStage(stage) {
2078
2301
  return (stage.transitions ?? []).length === 0;
2079
2302
  }
@@ -2156,28 +2379,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
2156
2379
  now: ctx.now
2157
2380
  });
2158
2381
  }
2159
- async function persistThenDeploy(ctx, mutation, deploy) {
2160
- const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
2161
- await deployOrRollback({
2162
- client: ctx.client,
2163
- instanceId: ctx.instance._id,
2164
- committedRev: committed._rev,
2165
- restore: instanceStateFields(ctx.instance),
2166
- unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
2167
- reversible: !spawned,
2168
- deploy
2169
- });
2170
- }
2171
- async function refreshStageGuards(ctx, mutation, stageName) {
2172
- await deployStageGuards({
2173
- client: ctx.client,
2174
- clientForGdr: ctx.clientForGdr,
2175
- instance: materializeInstance(ctx.instance, mutation),
2176
- definition: ctx.definition,
2177
- stageName,
2178
- now: ctx.now
2179
- });
2180
- }
2181
2382
  async function commitTransition(ctx, actor) {
2182
2383
  if (isTerminal(ctx))
2183
2384
  return { fired: !1 };
@@ -2223,8 +2424,11 @@ async function commitTransition(ctx, actor) {
2223
2424
  };
2224
2425
  }
2225
2426
  async function pickTransition(ctx, stage) {
2226
- for (const candidate of stage.transitions ?? [])
2227
- if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
2427
+ for (const candidate of stage.transitions ?? []) {
2428
+ const outcome = await ctxEvaluateConditionOutcome(ctx, candidate.filter);
2429
+ if (outcome === "satisfied") return candidate;
2430
+ if (outcome === "unevaluable") return;
2431
+ }
2228
2432
  }
2229
2433
  async function evaluateAutoTransitions(args) {
2230
2434
  const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
@@ -2254,7 +2458,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
2254
2458
  }, committed = await client.patch(instance._id).set({
2255
2459
  stages: [initialStageEntry],
2256
2460
  lastChangedAt: now
2257
- }).ifRevisionId(instance._rev).commit();
2461
+ }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
2258
2462
  await deployOrRollback({
2259
2463
  client,
2260
2464
  instanceId: instance._id,
@@ -2516,15 +2720,18 @@ async function evaluateFromSnapshot(args) {
2516
2720
  })
2517
2721
  );
2518
2722
  const transitionEvaluations = [];
2519
- for (const transition of stage.transitions ?? [])
2723
+ for (const transition of stage.transitions ?? []) {
2724
+ const outcome = await evaluateConditionOutcome({
2725
+ condition: transition.filter,
2726
+ snapshot,
2727
+ params: scope
2728
+ });
2520
2729
  transitionEvaluations.push({
2521
2730
  transition,
2522
- filterSatisfied: await evaluateCondition({
2523
- condition: transition.filter,
2524
- snapshot,
2525
- params: scope
2526
- })
2731
+ filterSatisfied: outcome === "satisfied",
2732
+ unevaluable: outcome === "unevaluable"
2527
2733
  });
2734
+ }
2528
2735
  const currentStage = {
2529
2736
  stage,
2530
2737
  tasks: taskEvaluations,
@@ -2572,7 +2779,11 @@ async function evaluateTask(args) {
2572
2779
  ...scopedStateOverlay(instance, snapshot, task.name)
2573
2780
  },
2574
2781
  assigned
2575
- }, actions = [];
2782
+ }, unmetRequirements = await evaluateRequirements({
2783
+ requirements: task.requirements,
2784
+ snapshot,
2785
+ params: taskScope
2786
+ }), requirementsReason = unmetRequirements.length > 0 ? { kind: "requirements-unmet", unmetRequirements } : void 0, actions = [];
2576
2787
  for (const action of task.actions ?? [])
2577
2788
  actions.push(
2578
2789
  await evaluateAction({
@@ -2582,7 +2793,8 @@ async function evaluateTask(args) {
2582
2793
  snapshot,
2583
2794
  taskScope,
2584
2795
  stageHasExits,
2585
- guardDenial
2796
+ guardDenial,
2797
+ requirementsReason
2586
2798
  })
2587
2799
  );
2588
2800
  return {
@@ -2593,12 +2805,22 @@ async function evaluateTask(args) {
2593
2805
  // they're still inbox items. The inbox reads the assignees-kind state
2594
2806
  // entry BY KIND (who-data is state).
2595
2807
  pendingOnActor: (status === "active" || status === "pending") && assigned,
2808
+ ...unmetRequirements.length > 0 ? { unmetRequirements } : {},
2596
2809
  actions
2597
2810
  };
2598
2811
  }
2599
2812
  async function evaluateAction(args) {
2600
- const { action, status, instance, snapshot, taskScope, stageHasExits, guardDenial } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2601
- return lifecycle !== void 0 ? disabled(action, lifecycle) : guardDenial !== void 0 ? disabled(action, guardDenial) : action.filter !== void 0 && !await evaluateCondition({
2813
+ const {
2814
+ action,
2815
+ status,
2816
+ instance,
2817
+ snapshot,
2818
+ taskScope,
2819
+ stageHasExits,
2820
+ guardDenial,
2821
+ requirementsReason
2822
+ } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2823
+ 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({
2602
2824
  condition: action.filter,
2603
2825
  snapshot,
2604
2826
  params: taskScope
@@ -2875,7 +3097,8 @@ const disabledReasonDetail = {
2875
3097
  "task-not-active": (r) => `task status is "${r.status}"`,
2876
3098
  "stage-terminal": (r) => `stage "${r.stage}" is terminal`,
2877
3099
  "instance-completed": (r) => `instance completed at ${r.completedAt}`,
2878
- "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
3100
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`,
3101
+ "requirements-unmet": (r) => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`
2879
3102
  };
2880
3103
  function formatDisabledReason(task, action, reason) {
2881
3104
  const detail = disabledReasonDetail[reason.kind](reason);
@@ -3112,6 +3335,7 @@ const workflow = {
3112
3335
  selfId: id,
3113
3336
  tag,
3114
3337
  workflowResource,
3338
+ definitionName: definition.name,
3115
3339
  ...perspective !== void 0 ? { perspective } : {}
3116
3340
  },
3117
3341
  randomKey
@@ -3130,7 +3354,7 @@ const workflow = {
3130
3354
  initialStage: definition.initialStage,
3131
3355
  actor
3132
3356
  });
3133
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
3357
+ 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);
3134
3358
  },
3135
3359
  /**
3136
3360
  * Fire an action against a task. If the task is pending, it is
@@ -3370,8 +3594,8 @@ const workflow = {
3370
3594
  * Pure read.
3371
3595
  */
3372
3596
  diagnose: async (args) => {
3373
- const evaluation = await evaluateInstance(args);
3374
- return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
3597
+ const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
3598
+ return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
3375
3599
  },
3376
3600
  /**
3377
3601
  * List the actions an actor could fire on an instance's current stage,
@@ -3540,7 +3764,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
3540
3764
  claimedBy: drainerActor
3541
3765
  };
3542
3766
  try {
3543
- return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
3767
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(SYNC_COMMIT), !0;
3544
3768
  } catch (error) {
3545
3769
  if (!isRevisionConflict(error)) throw error;
3546
3770
  return !1;
@@ -3993,6 +4217,7 @@ export {
3993
4217
  ActionParamsInvalidError,
3994
4218
  CascadeLimitError,
3995
4219
  ConcurrentFireActionError,
4220
+ DEFAULT_CONTENT_PERSPECTIVE,
3996
4221
  DISPLAY,
3997
4222
  EFFECTS_CONTEXT_DISPLAY,
3998
4223
  GUARD_DOC_TYPE,
@@ -4001,13 +4226,14 @@ export {
4001
4226
  HISTORY_DISPLAY,
4002
4227
  MutationGuardDeniedError,
4003
4228
  OP_DISPLAY,
4229
+ PartialGuardDeployError,
4230
+ RequiredStateNotProvidedError,
4004
4231
  STATE_SLOT_DISPLAY,
4005
4232
  WORKFLOW_DEFINITION_TYPE,
4006
4233
  WORKFLOW_INSTANCE_TYPE,
4007
4234
  WorkflowStateDivergedError,
4008
4235
  abortReason,
4009
4236
  actionVerdict,
4010
- assigneesOf,
4011
4237
  availableActions,
4012
4238
  buildSnapshot,
4013
4239
  compileGuard,
@@ -4037,15 +4263,16 @@ export {
4037
4263
  instanceGuardQuery,
4038
4264
  isDefinitionUnchanged,
4039
4265
  isGdr,
4266
+ isStartableDefinition,
4040
4267
  isTerminalStage,
4041
4268
  lakeGuardId,
4042
- openStage,
4043
4269
  parseGdr,
4044
4270
  readsRaw,
4045
4271
  refCanvas,
4046
4272
  refDashboard,
4047
4273
  refDataset,
4048
4274
  refMediaLibrary,
4275
+ remediationsFor,
4049
4276
  resolveAccess,
4050
4277
  resourceFromParsed,
4051
4278
  retractStageGuards,