@sanity/workflow-engine 0.6.0 → 0.7.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
@@ -95,6 +95,21 @@ function gdrFromResource(res, documentId) {
95
95
  }
96
96
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
97
97
  }
98
+ function resourceFromGdrUri(uri) {
99
+ let parsed;
100
+ try {
101
+ parsed = parseGdr(uri);
102
+ } catch {
103
+ return;
104
+ }
105
+ if (parsed.scheme === "dataset")
106
+ return parsed.projectId === void 0 || parsed.dataset === void 0 ? void 0 : { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` };
107
+ if (parsed.resourceId !== void 0)
108
+ return { type: parsed.scheme, id: parsed.resourceId };
109
+ }
110
+ function sameResource(a, b) {
111
+ return a.type === b.type && a.id === b.id;
112
+ }
98
113
  function selfGdr(doc) {
99
114
  return gdrFromResource(doc.workflowResource, doc._id);
100
115
  }
@@ -239,6 +254,8 @@ function validateTask(v2, stageName, task) {
239
254
  for (const entry of task.state ?? [])
240
255
  v2.checkEntry(entry, `${where}.state "${entry.name}"`);
241
256
  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`);
257
+ for (const [name, groq] of Object.entries(task.requirements ?? {}))
258
+ v2.checkCondition(groq, `${where}.requirements "${name}"`);
242
259
  for (const [key, groq] of effectBindings(task.effects))
243
260
  v2.tryParse(groq, `${where} effect binding "${key}"`);
244
261
  validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
@@ -476,10 +493,14 @@ function abortReason(instance) {
476
493
  )?.reason;
477
494
  }
478
495
  function diagnoseInputFromEvaluation(evaluation) {
496
+ const stage = openStage(evaluation.instance);
479
497
  return {
480
498
  instance: evaluation.instance,
481
499
  tasks: evaluation.currentStage.tasks,
482
- transitions: evaluation.currentStage.transitions
500
+ transitions: evaluation.currentStage.transitions,
501
+ assignees: Object.fromEntries(
502
+ evaluation.currentStage.tasks.map((t) => [t.task.name, assigneesOf(stage, t.task.name)])
503
+ )
483
504
  };
484
505
  }
485
506
  function openStage(instance) {
@@ -504,18 +525,34 @@ function failedTaskCause(input) {
504
525
  return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
505
526
  }
506
527
  function waitingState(input) {
507
- const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
528
+ const active = input.tasks.find(
529
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length === 0
530
+ );
508
531
  if (active !== void 0)
509
532
  return {
510
533
  state: "waiting",
511
534
  task: active.task.name,
512
- assignees: assigneesOf(openStage(input.instance), active.task.name),
535
+ assignees: input.assignees[active.task.name] ?? [],
513
536
  actions: (active.task.actions ?? []).map((a) => a.name)
514
537
  };
515
538
  }
539
+ function blockedState(input) {
540
+ const blocked = input.tasks.find(
541
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length > 0
542
+ );
543
+ if (blocked !== void 0)
544
+ return {
545
+ state: "blocked",
546
+ task: blocked.task.name,
547
+ requirements: blocked.unmetRequirements ?? [],
548
+ assignees: input.assignees[blocked.task.name] ?? []
549
+ };
550
+ }
516
551
  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" };
552
+ if (input.transitions.length === 0 || input.transitions.some((t) => t.filterSatisfied) || !input.tasks.every((t) => isResolved(t.status)))
553
+ return;
554
+ const undecidable = input.transitions.filter((t) => t.unevaluable);
555
+ return undecidable.length > 0 ? { kind: "transition-unevaluable", transitions: undecidable.map((t) => t.transition.name) } : { kind: "no-transition-fires" };
519
556
  }
520
557
  function diagnoseInstance(input) {
521
558
  const { instance } = input;
@@ -526,7 +563,51 @@ function diagnoseInstance(input) {
526
563
  if (instance.completedAt !== void 0)
527
564
  return { state: "completed", at: instance.completedAt };
528
565
  const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
529
- return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
566
+ return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? blockedState(input) ?? { state: "progressing" };
567
+ }
568
+ const RUNNABLE_VERBS = /* @__PURE__ */ new Set(["set-stage", "abort"]);
569
+ function remediationsForCause(cause) {
570
+ switch (cause.kind) {
571
+ case "failed-effect":
572
+ return [
573
+ {
574
+ verb: "retry-effect",
575
+ rationale: "Re-run the failed effect once the upstream system is healthy."
576
+ },
577
+ { verb: "abort", rationale: "Abort the instance if it can no longer recover." }
578
+ ];
579
+ case "hung-effect":
580
+ return [
581
+ {
582
+ verb: "drain-effects",
583
+ rationale: "Re-run the effect drainer to re-pick the claimed effect."
584
+ },
585
+ { verb: "abort", rationale: "Abort the instance if the effect never drains." }
586
+ ];
587
+ case "failed-task":
588
+ return [
589
+ { verb: "reset-task", rationale: "Reset or skip the failed task." },
590
+ {
591
+ verb: "set-stage",
592
+ rationale: "Move the instance past the failed task to the intended next stage."
593
+ }
594
+ ];
595
+ case "no-transition-fires":
596
+ return [
597
+ {
598
+ verb: "set-stage",
599
+ rationale: "Move the instance to the intended next stage to force it forward."
600
+ }
601
+ ];
602
+ case "transition-unevaluable":
603
+ return [];
604
+ }
605
+ }
606
+ function remediationsFor(diagnosis) {
607
+ return diagnosis.state !== "stuck" ? [] : remediationsForCause(diagnosis.cause).map((seed) => ({
608
+ ...seed,
609
+ available: RUNNABLE_VERBS.has(seed.verb)
610
+ }));
530
611
  }
531
612
  function buildSnapshot(args) {
532
613
  const docs = [], knownIds = /* @__PURE__ */ new Set();
@@ -573,6 +654,7 @@ function collectWatchRefs(instance) {
573
654
  function readsRaw(ref) {
574
655
  return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
575
656
  }
657
+ const DEFAULT_CONTENT_PERSPECTIVE = "drafts";
576
658
  function contentReleaseName(args) {
577
659
  const { ref, perspective } = args;
578
660
  if (!readsRaw(ref) && Array.isArray(perspective))
@@ -598,6 +680,11 @@ function entryReleaseRefs(state) {
598
680
  (entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
599
681
  );
600
682
  }
683
+ function entrySubjectRefs(state) {
684
+ return stateEntries(state).flatMap(
685
+ (entry) => (entry._type === "doc.ref" || entry._type === "release.ref") && isGdr(entry.value) ? [entry.value] : []
686
+ );
687
+ }
601
688
  function stateEntries(state) {
602
689
  return Array.isArray(state) ? state.filter(
603
690
  (entry) => !!entry && typeof entry == "object"
@@ -622,7 +709,10 @@ async function hydrateSnapshot(args) {
622
709
  };
623
710
  loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
624
711
  for (const ref of collectWatchRefs(instance))
625
- await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
712
+ await loadInto(
713
+ ref.id,
714
+ readsRaw(ref) ? "raw" : instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
715
+ );
626
716
  return buildSnapshot({ docs: loaded });
627
717
  }
628
718
  async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
@@ -637,7 +727,7 @@ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, pers
637
727
  return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
638
728
  }
639
729
  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;
730
+ return perspective === "raw" ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
641
731
  }
642
732
  function resourceFromParsed(parsed) {
643
733
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
@@ -738,7 +828,7 @@ async function queryGuardsAcross(clients, query, params) {
738
828
  function dedupById(guards) {
739
829
  return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
740
830
  }
741
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
831
+ const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
742
832
  function resolveGuard(guard, instance, stageName, now) {
743
833
  const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
744
834
  if (targets === null) return null;
@@ -765,11 +855,11 @@ function resolveGuard(guard, instance, stageName, now) {
765
855
  }
766
856
  async function upsertGuard(client, doc) {
767
857
  if (!await client.getDocument(doc._id)) {
768
- await client.create(doc);
858
+ await client.create(doc, SYNC_COMMIT);
769
859
  return;
770
860
  }
771
861
  const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
772
- await client.patch(doc._id).set(body).commit();
862
+ await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
773
863
  }
774
864
  function resolvedStageGuards(args) {
775
865
  const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
@@ -792,7 +882,7 @@ async function retractStageGuards(args) {
792
882
  const live = await committedInstance(args);
793
883
  if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
794
884
  for (const { client, doc } of resolvedStageGuards(args))
795
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
885
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit(SYNC_COMMIT);
796
886
  }
797
887
  async function deleteOrphanedDefinitionGuards(args) {
798
888
  const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
@@ -816,16 +906,32 @@ function randomKey(length = 12) {
816
906
  const bytes = new Uint8Array(length);
817
907
  return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
818
908
  }
819
- async function evaluateCondition(args) {
909
+ function isUnevaluable(result) {
910
+ return result == null;
911
+ }
912
+ async function evaluateConditionOutcome(args) {
820
913
  const { condition, snapshot, params } = args;
821
- return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
914
+ if (condition === void 0) return "satisfied";
915
+ const result = await runGroq(condition, params, snapshot);
916
+ return isUnevaluable(result) ? "unevaluable" : result ? "satisfied" : "unsatisfied";
917
+ }
918
+ async function evaluateCondition(args) {
919
+ return await evaluateConditionOutcome(args) === "satisfied";
822
920
  }
823
921
  async function evaluatePredicates(args) {
824
922
  const out = {};
825
- for (const [name, groq] of Object.entries(args.predicates ?? {}))
826
- out[name] = !!await runGroq(groq, args.params, args.snapshot);
923
+ for (const [name, groq] of Object.entries(args.predicates ?? {})) {
924
+ const result = await runGroq(groq, args.params, args.snapshot);
925
+ out[name] = isUnevaluable(result) ? null : !!result;
926
+ }
827
927
  return out;
828
928
  }
929
+ async function evaluateRequirements(args) {
930
+ const unmet = [];
931
+ for (const [name, condition] of Object.entries(args.requirements ?? {}))
932
+ await evaluateCondition({ condition, snapshot: args.snapshot, params: args.params }) || unmet.push(name);
933
+ return unmet;
934
+ }
829
935
  async function runGroq(groq, params, snapshot) {
830
936
  const tree = groqJs.parse(groq, { params });
831
937
  return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
@@ -993,7 +1099,7 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
993
1099
  tag: ctx.tag
994
1100
  });
995
1101
  try {
996
- const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
1102
+ const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
997
1103
  return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
998
1104
  } catch (err) {
999
1105
  throw new Error(
@@ -1123,13 +1229,16 @@ async function ctxConditionParams(ctx, opts) {
1123
1229
  params
1124
1230
  }), ...params };
1125
1231
  }
1126
- async function ctxEvaluateCondition(ctx, condition, opts) {
1127
- return condition === void 0 ? !0 : evaluateCondition({
1232
+ async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
1233
+ return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
1128
1234
  condition,
1129
1235
  snapshot: ctx.snapshot,
1130
1236
  params: await ctxConditionParams(ctx, opts)
1131
1237
  });
1132
1238
  }
1239
+ async function ctxEvaluateCondition(ctx, condition, opts) {
1240
+ return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
1241
+ }
1133
1242
  async function buildEngineContext(args) {
1134
1243
  const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
1135
1244
  return {
@@ -1249,7 +1358,7 @@ async function deployOrRollback(args) {
1249
1358
  });
1250
1359
  try {
1251
1360
  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();
1361
+ 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
1362
  } catch (rollbackError) {
1254
1363
  throw new WorkflowStateDivergedError({
1255
1364
  instanceId: args.instanceId,
@@ -1356,7 +1465,7 @@ async function persist(ctx, mutation) {
1356
1465
  lastChangedAt: ctx.now
1357
1466
  }, pendingCreates = mutation.pendingCreates;
1358
1467
  if (pendingCreates.length === 0)
1359
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1468
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
1360
1469
  const tx = ctx.client.transaction();
1361
1470
  for (const body of pendingCreates) tx.create(body);
1362
1471
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
@@ -1509,9 +1618,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1509
1618
  const parentScope = await ctxConditionParams(ctx, {
1510
1619
  taskName: task.name,
1511
1620
  ...actor ? { actor } : {}
1512
- }), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1621
+ }), { rows, discoveryResource } = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1513
1622
  for (const row of rows) {
1514
- const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
1623
+ const initialState = await projectRowState(
1624
+ ctx,
1625
+ sub,
1626
+ definition,
1627
+ parentScope,
1628
+ row,
1629
+ discoveryResource
1630
+ ), { ref, body } = await prepareChildInstance({
1515
1631
  client: ctx.client,
1516
1632
  parent: ctx.instance,
1517
1633
  definition,
@@ -1526,28 +1642,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1526
1642
  }
1527
1643
  async function resolveSpawnRows(ctx, sub) {
1528
1644
  const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
1529
- let value;
1645
+ let value, discoveryResource;
1530
1646
  if (groq.includes("*")) {
1531
- const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1532
- value = await discoverItems({
1647
+ const routingUri = entrySubjectRefs(ctx.instance.state)[0]?.id, client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1648
+ client !== ctx.client && routingUri !== void 0 && (discoveryResource = resourceFromGdrUri(routingUri)), value = await discoverItems({
1533
1649
  client,
1534
1650
  groq,
1535
1651
  params,
1536
- ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
1652
+ // Draft-aware by default (drafts overlay published) when there's no
1653
+ // release, so a `forEach` discovers in-flight items the same way the
1654
+ // engine reads any other content.
1655
+ perspective: ctx.instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
1537
1656
  });
1538
1657
  } else {
1539
1658
  const tree = groqJs.parse(groq, { params });
1540
1659
  value = await (await groqJs.evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
1541
1660
  }
1542
- return Array.isArray(value) ? value : value == null ? [] : [value];
1543
- }
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
- }
1661
+ return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
1549
1662
  }
1550
- async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1663
+ async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
1551
1664
  const out = [];
1552
1665
  for (const [name, groq] of Object.entries(sub.with ?? {})) {
1553
1666
  const declared = (childDefinition.state ?? []).find((e) => e.name === name);
@@ -1555,7 +1668,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1555
1668
  throw new Error(
1556
1669
  `subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
1557
1670
  );
1558
- const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
1671
+ const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, {
1672
+ parentResource: ctx.instance.workflowResource,
1673
+ discoveryResource,
1674
+ entryName: name,
1675
+ childName: childDefinition.name
1676
+ });
1559
1677
  value != null && out.push({
1560
1678
  type: declared.type,
1561
1679
  name,
@@ -1572,11 +1690,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
1572
1690
  }
1573
1691
  return out;
1574
1692
  }
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;
1693
+ function canonicaliseSpawnValue(kind, value, cx) {
1694
+ return kind === "doc.ref" ? coerceSpawnGdr(value, cx) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, cx)).filter((v2) => v2 !== null) : value;
1695
+ }
1696
+ function assertBareIdRootsCorrectly(id, cx) {
1697
+ if (!(cx.discoveryResource === void 0 || sameResource(cx.discoveryResource, cx.parentResource)))
1698
+ throw new Error(
1699
+ `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.`
1700
+ );
1577
1701
  }
1578
- function coerceSpawnGdr(raw, workflowResource) {
1579
- const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1702
+ function coerceSpawnGdr(raw, cx) {
1703
+ const toUri = (id) => isGdrUri(id) ? id : (assertBareIdRootsCorrectly(id, cx), gdrFromResource(cx.parentResource, id));
1580
1704
  if (raw == null) return null;
1581
1705
  if (typeof raw == "object" && "id" in raw && "type" in raw) {
1582
1706
  const r = raw;
@@ -2075,21 +2199,24 @@ function resolveMachineStep(mutation, task, entry, now) {
2075
2199
  async function buildStageTasks(ctx, stage) {
2076
2200
  const entries = [];
2077
2201
  for (const task of stage.tasks ?? []) {
2078
- const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
2202
+ const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
2079
2203
  entries.push({
2080
2204
  _key: randomKey(),
2081
2205
  name: task.name,
2082
2206
  status: inScope ? "pending" : "skipped",
2083
- ...task.filter !== void 0 ? {
2084
- filterEvaluation: {
2085
- at: ctx.now,
2086
- truthy: inScope
2087
- }
2088
- } : {}
2207
+ ...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
2089
2208
  });
2090
2209
  }
2091
2210
  return entries;
2092
2211
  }
2212
+ function buildFilterEvaluation(at, filter, outcome) {
2213
+ return outcome === "unevaluable" ? {
2214
+ at,
2215
+ truthy: !1,
2216
+ unevaluable: !0,
2217
+ 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.`
2218
+ } : { at, truthy: outcome === "satisfied" };
2219
+ }
2093
2220
  function isTerminalStage(stage) {
2094
2221
  return (stage.transitions ?? []).length === 0;
2095
2222
  }
@@ -2239,8 +2366,11 @@ async function commitTransition(ctx, actor) {
2239
2366
  };
2240
2367
  }
2241
2368
  async function pickTransition(ctx, stage) {
2242
- for (const candidate of stage.transitions ?? [])
2243
- if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
2369
+ for (const candidate of stage.transitions ?? []) {
2370
+ const outcome = await ctxEvaluateConditionOutcome(ctx, candidate.filter);
2371
+ if (outcome === "satisfied") return candidate;
2372
+ if (outcome === "unevaluable") return;
2373
+ }
2244
2374
  }
2245
2375
  async function evaluateAutoTransitions(args) {
2246
2376
  const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
@@ -2270,7 +2400,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
2270
2400
  }, committed = await client.patch(instance._id).set({
2271
2401
  stages: [initialStageEntry],
2272
2402
  lastChangedAt: now
2273
- }).ifRevisionId(instance._rev).commit();
2403
+ }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
2274
2404
  await deployOrRollback({
2275
2405
  client,
2276
2406
  instanceId: instance._id,
@@ -2532,15 +2662,18 @@ async function evaluateFromSnapshot(args) {
2532
2662
  })
2533
2663
  );
2534
2664
  const transitionEvaluations = [];
2535
- for (const transition of stage.transitions ?? [])
2665
+ for (const transition of stage.transitions ?? []) {
2666
+ const outcome = await evaluateConditionOutcome({
2667
+ condition: transition.filter,
2668
+ snapshot,
2669
+ params: scope
2670
+ });
2536
2671
  transitionEvaluations.push({
2537
2672
  transition,
2538
- filterSatisfied: await evaluateCondition({
2539
- condition: transition.filter,
2540
- snapshot,
2541
- params: scope
2542
- })
2673
+ filterSatisfied: outcome === "satisfied",
2674
+ unevaluable: outcome === "unevaluable"
2543
2675
  });
2676
+ }
2544
2677
  const currentStage = {
2545
2678
  stage,
2546
2679
  tasks: taskEvaluations,
@@ -2588,7 +2721,11 @@ async function evaluateTask(args) {
2588
2721
  ...scopedStateOverlay(instance, snapshot, task.name)
2589
2722
  },
2590
2723
  assigned
2591
- }, actions = [];
2724
+ }, unmetRequirements = await evaluateRequirements({
2725
+ requirements: task.requirements,
2726
+ snapshot,
2727
+ params: taskScope
2728
+ }), requirementsReason = unmetRequirements.length > 0 ? { kind: "requirements-unmet", unmetRequirements } : void 0, actions = [];
2592
2729
  for (const action of task.actions ?? [])
2593
2730
  actions.push(
2594
2731
  await evaluateAction({
@@ -2598,7 +2735,8 @@ async function evaluateTask(args) {
2598
2735
  snapshot,
2599
2736
  taskScope,
2600
2737
  stageHasExits,
2601
- guardDenial
2738
+ guardDenial,
2739
+ requirementsReason
2602
2740
  })
2603
2741
  );
2604
2742
  return {
@@ -2609,12 +2747,22 @@ async function evaluateTask(args) {
2609
2747
  // they're still inbox items. The inbox reads the assignees-kind state
2610
2748
  // entry BY KIND (who-data is state).
2611
2749
  pendingOnActor: (status === "active" || status === "pending") && assigned,
2750
+ ...unmetRequirements.length > 0 ? { unmetRequirements } : {},
2612
2751
  actions
2613
2752
  };
2614
2753
  }
2615
2754
  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({
2755
+ const {
2756
+ action,
2757
+ status,
2758
+ instance,
2759
+ snapshot,
2760
+ taskScope,
2761
+ stageHasExits,
2762
+ guardDenial,
2763
+ requirementsReason
2764
+ } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2765
+ 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
2766
  condition: action.filter,
2619
2767
  snapshot,
2620
2768
  params: taskScope
@@ -2891,7 +3039,8 @@ const disabledReasonDetail = {
2891
3039
  "task-not-active": (r) => `task status is "${r.status}"`,
2892
3040
  "stage-terminal": (r) => `stage "${r.stage}" is terminal`,
2893
3041
  "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})` : ""}`
3042
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`,
3043
+ "requirements-unmet": (r) => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`
2895
3044
  };
2896
3045
  function formatDisabledReason(task, action, reason) {
2897
3046
  const detail = disabledReasonDetail[reason.kind](reason);
@@ -3146,7 +3295,7 @@ const workflow = {
3146
3295
  initialStage: definition.initialStage,
3147
3296
  actor
3148
3297
  });
3149
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
3298
+ 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
3299
  },
3151
3300
  /**
3152
3301
  * Fire an action against a task. If the task is pending, it is
@@ -3386,8 +3535,8 @@ const workflow = {
3386
3535
  * Pure read.
3387
3536
  */
3388
3537
  diagnose: async (args) => {
3389
- const evaluation = await evaluateInstance(args);
3390
- return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
3538
+ const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
3539
+ return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
3391
3540
  },
3392
3541
  /**
3393
3542
  * List the actions an actor could fire on an instance's current stage,
@@ -3556,7 +3705,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
3556
3705
  claimedBy: drainerActor
3557
3706
  };
3558
3707
  try {
3559
- return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
3708
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(SYNC_COMMIT), !0;
3560
3709
  } catch (error) {
3561
3710
  if (!isRevisionConflict(error)) throw error;
3562
3711
  return !1;
@@ -4009,6 +4158,7 @@ exports.ActionDisabledError = ActionDisabledError;
4009
4158
  exports.ActionParamsInvalidError = ActionParamsInvalidError;
4010
4159
  exports.CascadeLimitError = CascadeLimitError;
4011
4160
  exports.ConcurrentFireActionError = ConcurrentFireActionError;
4161
+ exports.DEFAULT_CONTENT_PERSPECTIVE = DEFAULT_CONTENT_PERSPECTIVE;
4012
4162
  exports.DISPLAY = DISPLAY;
4013
4163
  exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
4014
4164
  exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
@@ -4022,7 +4172,6 @@ exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
4022
4172
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
4023
4173
  exports.abortReason = abortReason;
4024
4174
  exports.actionVerdict = actionVerdict;
4025
- exports.assigneesOf = assigneesOf;
4026
4175
  exports.availableActions = availableActions;
4027
4176
  exports.buildSnapshot = buildSnapshot;
4028
4177
  exports.compileGuard = compileGuard;
@@ -4054,13 +4203,13 @@ exports.isDefinitionUnchanged = isDefinitionUnchanged;
4054
4203
  exports.isGdr = isGdr;
4055
4204
  exports.isTerminalStage = isTerminalStage;
4056
4205
  exports.lakeGuardId = lakeGuardId;
4057
- exports.openStage = openStage;
4058
4206
  exports.parseGdr = parseGdr;
4059
4207
  exports.readsRaw = readsRaw;
4060
4208
  exports.refCanvas = refCanvas;
4061
4209
  exports.refDashboard = refDashboard;
4062
4210
  exports.refDataset = refDataset;
4063
4211
  exports.refMediaLibrary = refMediaLibrary;
4212
+ exports.remediationsFor = remediationsFor;
4064
4213
  exports.resolveAccess = resolveAccess;
4065
4214
  exports.resourceFromParsed = resourceFromParsed;
4066
4215
  exports.retractStageGuards = retractStageGuards;