@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.js CHANGED
@@ -79,6 +79,21 @@ function gdrFromResource(res, documentId) {
79
79
  }
80
80
  return gdrUri({ scheme: res.type, resourceId: res.id, documentId });
81
81
  }
82
+ function resourceFromGdrUri(uri) {
83
+ let parsed;
84
+ try {
85
+ parsed = parseGdr(uri);
86
+ } catch {
87
+ return;
88
+ }
89
+ if (parsed.scheme === "dataset")
90
+ return parsed.projectId === void 0 || parsed.dataset === void 0 ? void 0 : { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` };
91
+ if (parsed.resourceId !== void 0)
92
+ return { type: parsed.scheme, id: parsed.resourceId };
93
+ }
94
+ function sameResource(a, b) {
95
+ return a.type === b.type && a.id === b.id;
96
+ }
82
97
  function selfGdr(doc) {
83
98
  return gdrFromResource(doc.workflowResource, doc._id);
84
99
  }
@@ -223,6 +238,8 @@ function validateTask(v2, stageName, task) {
223
238
  for (const entry of task.state ?? [])
224
239
  v2.checkEntry(entry, `${where}.state "${entry.name}"`);
225
240
  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`);
241
+ for (const [name, groq] of Object.entries(task.requirements ?? {}))
242
+ v2.checkCondition(groq, `${where}.requirements "${name}"`);
226
243
  for (const [key, groq] of effectBindings(task.effects))
227
244
  v2.tryParse(groq, `${where} effect binding "${key}"`);
228
245
  validateTaskActions(v2, task), task.subworkflows !== void 0 && validateSubworkflows(v2, where, task.subworkflows);
@@ -460,10 +477,14 @@ function abortReason(instance) {
460
477
  )?.reason;
461
478
  }
462
479
  function diagnoseInputFromEvaluation(evaluation) {
480
+ const stage = openStage(evaluation.instance);
463
481
  return {
464
482
  instance: evaluation.instance,
465
483
  tasks: evaluation.currentStage.tasks,
466
- transitions: evaluation.currentStage.transitions
484
+ transitions: evaluation.currentStage.transitions,
485
+ assignees: Object.fromEntries(
486
+ evaluation.currentStage.tasks.map((t) => [t.task.name, assigneesOf(stage, t.task.name)])
487
+ )
467
488
  };
468
489
  }
469
490
  function openStage(instance) {
@@ -488,18 +509,34 @@ function failedTaskCause(input) {
488
509
  return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
489
510
  }
490
511
  function waitingState(input) {
491
- const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
512
+ const active = input.tasks.find(
513
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length === 0
514
+ );
492
515
  if (active !== void 0)
493
516
  return {
494
517
  state: "waiting",
495
518
  task: active.task.name,
496
- assignees: assigneesOf(openStage(input.instance), active.task.name),
519
+ assignees: input.assignees[active.task.name] ?? [],
497
520
  actions: (active.task.actions ?? []).map((a) => a.name)
498
521
  };
499
522
  }
523
+ function blockedState(input) {
524
+ const blocked = input.tasks.find(
525
+ (t) => t.status === "active" && (t.task.actions ?? []).length > 0 && (t.unmetRequirements ?? []).length > 0
526
+ );
527
+ if (blocked !== void 0)
528
+ return {
529
+ state: "blocked",
530
+ task: blocked.task.name,
531
+ requirements: blocked.unmetRequirements ?? [],
532
+ assignees: input.assignees[blocked.task.name] ?? []
533
+ };
534
+ }
500
535
  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" };
536
+ if (input.transitions.length === 0 || input.transitions.some((t) => t.filterSatisfied) || !input.tasks.every((t) => isResolved(t.status)))
537
+ return;
538
+ const undecidable = input.transitions.filter((t) => t.unevaluable);
539
+ return undecidable.length > 0 ? { kind: "transition-unevaluable", transitions: undecidable.map((t) => t.transition.name) } : { kind: "no-transition-fires" };
503
540
  }
504
541
  function diagnoseInstance(input) {
505
542
  const { instance } = input;
@@ -510,7 +547,51 @@ function diagnoseInstance(input) {
510
547
  if (instance.completedAt !== void 0)
511
548
  return { state: "completed", at: instance.completedAt };
512
549
  const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
513
- return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
550
+ return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? blockedState(input) ?? { state: "progressing" };
551
+ }
552
+ const RUNNABLE_VERBS = /* @__PURE__ */ new Set(["set-stage", "abort"]);
553
+ function remediationsForCause(cause) {
554
+ switch (cause.kind) {
555
+ case "failed-effect":
556
+ return [
557
+ {
558
+ verb: "retry-effect",
559
+ rationale: "Re-run the failed effect once the upstream system is healthy."
560
+ },
561
+ { verb: "abort", rationale: "Abort the instance if it can no longer recover." }
562
+ ];
563
+ case "hung-effect":
564
+ return [
565
+ {
566
+ verb: "drain-effects",
567
+ rationale: "Re-run the effect drainer to re-pick the claimed effect."
568
+ },
569
+ { verb: "abort", rationale: "Abort the instance if the effect never drains." }
570
+ ];
571
+ case "failed-task":
572
+ return [
573
+ { verb: "reset-task", rationale: "Reset or skip the failed task." },
574
+ {
575
+ verb: "set-stage",
576
+ rationale: "Move the instance past the failed task to the intended next stage."
577
+ }
578
+ ];
579
+ case "no-transition-fires":
580
+ return [
581
+ {
582
+ verb: "set-stage",
583
+ rationale: "Move the instance to the intended next stage to force it forward."
584
+ }
585
+ ];
586
+ case "transition-unevaluable":
587
+ return [];
588
+ }
589
+ }
590
+ function remediationsFor(diagnosis) {
591
+ return diagnosis.state !== "stuck" ? [] : remediationsForCause(diagnosis.cause).map((seed) => ({
592
+ ...seed,
593
+ available: RUNNABLE_VERBS.has(seed.verb)
594
+ }));
514
595
  }
515
596
  function buildSnapshot(args) {
516
597
  const docs = [], knownIds = /* @__PURE__ */ new Set();
@@ -557,6 +638,7 @@ function collectWatchRefs(instance) {
557
638
  function readsRaw(ref) {
558
639
  return ref.type === WORKFLOW_INSTANCE_TYPE || ref.type === "system.release";
559
640
  }
641
+ const DEFAULT_CONTENT_PERSPECTIVE = "drafts";
560
642
  function contentReleaseName(args) {
561
643
  const { ref, perspective } = args;
562
644
  if (!readsRaw(ref) && Array.isArray(perspective))
@@ -582,6 +664,11 @@ function entryReleaseRefs(state) {
582
664
  (entry) => entry._type === "release.ref" && isGdr(entry.value) ? [entry.value] : []
583
665
  );
584
666
  }
667
+ function entrySubjectRefs(state) {
668
+ return stateEntries(state).flatMap(
669
+ (entry) => (entry._type === "doc.ref" || entry._type === "release.ref") && isGdr(entry.value) ? [entry.value] : []
670
+ );
671
+ }
585
672
  function stateEntries(state) {
586
673
  return Array.isArray(state) ? state.filter(
587
674
  (entry) => !!entry && typeof entry == "object"
@@ -606,7 +693,10 @@ async function hydrateSnapshot(args) {
606
693
  };
607
694
  loaded.push({ doc: instance, resource: instance.workflowResource }), visited.add(selfGdr(instance));
608
695
  for (const ref of collectWatchRefs(instance))
609
- await loadInto(ref.id, readsRaw(ref) ? void 0 : instance.perspective);
696
+ await loadInto(
697
+ ref.id,
698
+ readsRaw(ref) ? "raw" : instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
699
+ );
610
700
  return buildSnapshot({ docs: loaded });
611
701
  }
612
702
  async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, perspective) {
@@ -621,7 +711,7 @@ async function loadByGdr(defaultClient, clientForGdr, defaultResource, uri, pers
621
711
  return doc ? { doc, resource: resourceFromParsed(parsed) } : null;
622
712
  }
623
713
  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;
714
+ return perspective === "raw" ? await client.getDocument(id) ?? null : await client.fetch("*[_id == $id][0]", { id }, { perspective }) ?? null;
625
715
  }
626
716
  function resourceFromParsed(parsed) {
627
717
  return parsed.scheme === "dataset" ? { type: "dataset", id: `${parsed.projectId}.${parsed.dataset}` } : { type: parsed.scheme, id: parsed.resourceId ?? "" };
@@ -722,7 +812,7 @@ async function queryGuardsAcross(clients, query, params) {
722
812
  function dedupById(guards) {
723
813
  return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
724
814
  }
725
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
815
+ const SYNC_COMMIT = { visibility: "sync" }, GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
726
816
  function resolveGuard(guard, instance, stageName, now) {
727
817
  const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
728
818
  if (targets === null) return null;
@@ -749,11 +839,11 @@ function resolveGuard(guard, instance, stageName, now) {
749
839
  }
750
840
  async function upsertGuard(client, doc) {
751
841
  if (!await client.getDocument(doc._id)) {
752
- await client.create(doc);
842
+ await client.create(doc, SYNC_COMMIT);
753
843
  return;
754
844
  }
755
845
  const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
756
- await client.patch(doc._id).set(body).commit();
846
+ await client.patch(doc._id).set(body).commit(SYNC_COMMIT);
757
847
  }
758
848
  function resolvedStageGuards(args) {
759
849
  const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
@@ -776,7 +866,7 @@ async function retractStageGuards(args) {
776
866
  const live = await committedInstance(args);
777
867
  if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
778
868
  for (const { client, doc } of resolvedStageGuards(args))
779
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
869
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit(SYNC_COMMIT);
780
870
  }
781
871
  async function deleteOrphanedDefinitionGuards(args) {
782
872
  const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
@@ -800,16 +890,32 @@ function randomKey(length = 12) {
800
890
  const bytes = new Uint8Array(length);
801
891
  return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
802
892
  }
803
- async function evaluateCondition(args) {
893
+ function isUnevaluable(result) {
894
+ return result == null;
895
+ }
896
+ async function evaluateConditionOutcome(args) {
804
897
  const { condition, snapshot, params } = args;
805
- return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
898
+ if (condition === void 0) return "satisfied";
899
+ const result = await runGroq(condition, params, snapshot);
900
+ return isUnevaluable(result) ? "unevaluable" : result ? "satisfied" : "unsatisfied";
901
+ }
902
+ async function evaluateCondition(args) {
903
+ return await evaluateConditionOutcome(args) === "satisfied";
806
904
  }
807
905
  async function evaluatePredicates(args) {
808
906
  const out = {};
809
- for (const [name, groq] of Object.entries(args.predicates ?? {}))
810
- out[name] = !!await runGroq(groq, args.params, args.snapshot);
907
+ for (const [name, groq] of Object.entries(args.predicates ?? {})) {
908
+ const result = await runGroq(groq, args.params, args.snapshot);
909
+ out[name] = isUnevaluable(result) ? null : !!result;
910
+ }
811
911
  return out;
812
912
  }
913
+ async function evaluateRequirements(args) {
914
+ const unmet = [];
915
+ for (const [name, condition] of Object.entries(args.requirements ?? {}))
916
+ await evaluateCondition({ condition, snapshot: args.snapshot, params: args.params }) || unmet.push(name);
917
+ return unmet;
918
+ }
813
919
  async function runGroq(groq, params, snapshot) {
814
920
  const tree = parse(groq, { params });
815
921
  return (await evaluate(tree, { dataset: snapshot.docs, params })).get();
@@ -977,7 +1083,7 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
977
1083
  tag: ctx.tag
978
1084
  });
979
1085
  try {
980
- const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
1086
+ const fetchOptions = { perspective: ctx.perspective ?? DEFAULT_CONTENT_PERSPECTIVE }, result = await client.fetch(source.query, params, fetchOptions);
981
1087
  return normalizeQueryResult(entry.type, result, ctx.workflowResource) ?? defaultValue;
982
1088
  } catch (err) {
983
1089
  throw new Error(
@@ -1107,13 +1213,16 @@ async function ctxConditionParams(ctx, opts) {
1107
1213
  params
1108
1214
  }), ...params };
1109
1215
  }
1110
- async function ctxEvaluateCondition(ctx, condition, opts) {
1111
- return condition === void 0 ? !0 : evaluateCondition({
1216
+ async function ctxEvaluateConditionOutcome(ctx, condition, opts) {
1217
+ return condition === void 0 ? "satisfied" : evaluateConditionOutcome({
1112
1218
  condition,
1113
1219
  snapshot: ctx.snapshot,
1114
1220
  params: await ctxConditionParams(ctx, opts)
1115
1221
  });
1116
1222
  }
1223
+ async function ctxEvaluateCondition(ctx, condition, opts) {
1224
+ return await ctxEvaluateConditionOutcome(ctx, condition, opts) === "satisfied";
1225
+ }
1117
1226
  async function buildEngineContext(args) {
1118
1227
  const { client, clientForGdr, instance, definition } = args, clock = args.clock ?? wallClock;
1119
1228
  return {
@@ -1233,7 +1342,7 @@ async function deployOrRollback(args) {
1233
1342
  });
1234
1343
  try {
1235
1344
  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();
1345
+ 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
1346
  } catch (rollbackError) {
1238
1347
  throw new WorkflowStateDivergedError({
1239
1348
  instanceId: args.instanceId,
@@ -1340,7 +1449,7 @@ async function persist(ctx, mutation) {
1340
1449
  lastChangedAt: ctx.now
1341
1450
  }, pendingCreates = mutation.pendingCreates;
1342
1451
  if (pendingCreates.length === 0)
1343
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1452
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
1344
1453
  const tx = ctx.client.transaction();
1345
1454
  for (const body of pendingCreates) tx.create(body);
1346
1455
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
@@ -1493,9 +1602,16 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1493
1602
  const parentScope = await ctxConditionParams(ctx, {
1494
1603
  taskName: task.name,
1495
1604
  ...actor ? { actor } : {}
1496
- }), rows = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1605
+ }), { rows, discoveryResource } = await resolveSpawnRows(ctx, sub), effectsContext = await resolveContextHandoff(ctx, sub, parentScope), refs = [];
1497
1606
  for (const row of rows) {
1498
- const initialState = await projectRowState(ctx, sub, definition, parentScope, row), { ref, body } = await prepareChildInstance({
1607
+ const initialState = await projectRowState(
1608
+ ctx,
1609
+ sub,
1610
+ definition,
1611
+ parentScope,
1612
+ row,
1613
+ discoveryResource
1614
+ ), { ref, body } = await prepareChildInstance({
1499
1615
  client: ctx.client,
1500
1616
  parent: ctx.instance,
1501
1617
  definition,
@@ -1510,28 +1626,25 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1510
1626
  }
1511
1627
  async function resolveSpawnRows(ctx, sub) {
1512
1628
  const params = buildParams({ instance: ctx.instance, now: ctx.now, snapshot: ctx.snapshot }), groq = sub.forEach;
1513
- let value;
1629
+ let value, discoveryResource;
1514
1630
  if (groq.includes("*")) {
1515
- const routingUri = firstDocRefGdrUri(ctx.instance.state), client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1516
- value = await discoverItems({
1631
+ const routingUri = entrySubjectRefs(ctx.instance.state)[0]?.id, client = clientForDiscovery(ctx.client, ctx.clientForGdr, routingUri);
1632
+ client !== ctx.client && routingUri !== void 0 && (discoveryResource = resourceFromGdrUri(routingUri)), value = await discoverItems({
1517
1633
  client,
1518
1634
  groq,
1519
1635
  params,
1520
- ...ctx.instance.perspective !== void 0 ? { perspective: ctx.instance.perspective } : {}
1636
+ // Draft-aware by default (drafts overlay published) when there's no
1637
+ // release, so a `forEach` discovers in-flight items the same way the
1638
+ // engine reads any other content.
1639
+ perspective: ctx.instance.perspective ?? DEFAULT_CONTENT_PERSPECTIVE
1521
1640
  });
1522
1641
  } else {
1523
1642
  const tree = parse(groq, { params });
1524
1643
  value = await (await evaluate(tree, { dataset: [ctx.instance], params, root: ctx.instance })).get();
1525
1644
  }
1526
- return Array.isArray(value) ? value : value == null ? [] : [value];
1527
- }
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
- }
1645
+ return Array.isArray(value) ? { rows: value, discoveryResource } : value == null ? { rows: [], discoveryResource } : { rows: [value], discoveryResource };
1533
1646
  }
1534
- async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1647
+ async function projectRowState(ctx, sub, childDefinition, parentScope, row, discoveryResource) {
1535
1648
  const out = [];
1536
1649
  for (const [name, groq] of Object.entries(sub.with ?? {})) {
1537
1650
  const declared = (childDefinition.state ?? []).find((e) => e.name === name);
@@ -1539,7 +1652,12 @@ async function projectRowState(ctx, sub, childDefinition, parentScope, row) {
1539
1652
  throw new Error(
1540
1653
  `subworkflows.with["${name}"]: subworkflow "${childDefinition.name}" declares no state entry "${name}"`
1541
1654
  );
1542
- const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, ctx.instance.workflowResource);
1655
+ const raw = await runGroq(groq, { ...parentScope, row }, ctx.snapshot), value = canonicaliseSpawnValue(declared.type, raw, {
1656
+ parentResource: ctx.instance.workflowResource,
1657
+ discoveryResource,
1658
+ entryName: name,
1659
+ childName: childDefinition.name
1660
+ });
1543
1661
  value != null && out.push({
1544
1662
  type: declared.type,
1545
1663
  name,
@@ -1556,11 +1674,17 @@ async function resolveContextHandoff(ctx, sub, parentScope) {
1556
1674
  }
1557
1675
  return out;
1558
1676
  }
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;
1677
+ function canonicaliseSpawnValue(kind, value, cx) {
1678
+ return kind === "doc.ref" ? coerceSpawnGdr(value, cx) : kind === "doc.refs" && Array.isArray(value) ? value.map((v2) => coerceSpawnGdr(v2, cx)).filter((v2) => v2 !== null) : value;
1679
+ }
1680
+ function assertBareIdRootsCorrectly(id, cx) {
1681
+ if (!(cx.discoveryResource === void 0 || sameResource(cx.discoveryResource, cx.parentResource)))
1682
+ throw new Error(
1683
+ `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.`
1684
+ );
1561
1685
  }
1562
- function coerceSpawnGdr(raw, workflowResource) {
1563
- const toUri = (id) => isGdrUri(id) ? id : gdrFromResource(workflowResource, id);
1686
+ function coerceSpawnGdr(raw, cx) {
1687
+ const toUri = (id) => isGdrUri(id) ? id : (assertBareIdRootsCorrectly(id, cx), gdrFromResource(cx.parentResource, id));
1564
1688
  if (raw == null) return null;
1565
1689
  if (typeof raw == "object" && "id" in raw && "type" in raw) {
1566
1690
  const r = raw;
@@ -2059,21 +2183,24 @@ function resolveMachineStep(mutation, task, entry, now) {
2059
2183
  async function buildStageTasks(ctx, stage) {
2060
2184
  const entries = [];
2061
2185
  for (const task of stage.tasks ?? []) {
2062
- const inScope = await ctxEvaluateCondition(ctx, task.filter, { taskName: task.name });
2186
+ const outcome = await ctxEvaluateConditionOutcome(ctx, task.filter, { taskName: task.name }), inScope = outcome !== "unsatisfied";
2063
2187
  entries.push({
2064
2188
  _key: randomKey(),
2065
2189
  name: task.name,
2066
2190
  status: inScope ? "pending" : "skipped",
2067
- ...task.filter !== void 0 ? {
2068
- filterEvaluation: {
2069
- at: ctx.now,
2070
- truthy: inScope
2071
- }
2072
- } : {}
2191
+ ...task.filter !== void 0 ? { filterEvaluation: buildFilterEvaluation(ctx.now, task.filter, outcome) } : {}
2073
2192
  });
2074
2193
  }
2075
2194
  return entries;
2076
2195
  }
2196
+ function buildFilterEvaluation(at, filter, outcome) {
2197
+ return outcome === "unevaluable" ? {
2198
+ at,
2199
+ truthy: !1,
2200
+ unevaluable: !0,
2201
+ 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.`
2202
+ } : { at, truthy: outcome === "satisfied" };
2203
+ }
2077
2204
  function isTerminalStage(stage) {
2078
2205
  return (stage.transitions ?? []).length === 0;
2079
2206
  }
@@ -2223,8 +2350,11 @@ async function commitTransition(ctx, actor) {
2223
2350
  };
2224
2351
  }
2225
2352
  async function pickTransition(ctx, stage) {
2226
- for (const candidate of stage.transitions ?? [])
2227
- if (await ctxEvaluateCondition(ctx, candidate.filter)) return candidate;
2353
+ for (const candidate of stage.transitions ?? []) {
2354
+ const outcome = await ctxEvaluateConditionOutcome(ctx, candidate.filter);
2355
+ if (outcome === "satisfied") return candidate;
2356
+ if (outcome === "unevaluable") return;
2357
+ }
2228
2358
  }
2229
2359
  async function evaluateAutoTransitions(args) {
2230
2360
  const { client, instanceId, options, clientForGdr, clock, overlay } = args, ctx = await loadContext(client, instanceId, {
@@ -2254,7 +2384,7 @@ async function primeInitialStage(client, instanceId, actor, clientForGdr, clock)
2254
2384
  }, committed = await client.patch(instance._id).set({
2255
2385
  stages: [initialStageEntry],
2256
2386
  lastChangedAt: now
2257
- }).ifRevisionId(instance._rev).commit();
2387
+ }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
2258
2388
  await deployOrRollback({
2259
2389
  client,
2260
2390
  instanceId: instance._id,
@@ -2516,15 +2646,18 @@ async function evaluateFromSnapshot(args) {
2516
2646
  })
2517
2647
  );
2518
2648
  const transitionEvaluations = [];
2519
- for (const transition of stage.transitions ?? [])
2649
+ for (const transition of stage.transitions ?? []) {
2650
+ const outcome = await evaluateConditionOutcome({
2651
+ condition: transition.filter,
2652
+ snapshot,
2653
+ params: scope
2654
+ });
2520
2655
  transitionEvaluations.push({
2521
2656
  transition,
2522
- filterSatisfied: await evaluateCondition({
2523
- condition: transition.filter,
2524
- snapshot,
2525
- params: scope
2526
- })
2657
+ filterSatisfied: outcome === "satisfied",
2658
+ unevaluable: outcome === "unevaluable"
2527
2659
  });
2660
+ }
2528
2661
  const currentStage = {
2529
2662
  stage,
2530
2663
  tasks: taskEvaluations,
@@ -2572,7 +2705,11 @@ async function evaluateTask(args) {
2572
2705
  ...scopedStateOverlay(instance, snapshot, task.name)
2573
2706
  },
2574
2707
  assigned
2575
- }, actions = [];
2708
+ }, unmetRequirements = await evaluateRequirements({
2709
+ requirements: task.requirements,
2710
+ snapshot,
2711
+ params: taskScope
2712
+ }), requirementsReason = unmetRequirements.length > 0 ? { kind: "requirements-unmet", unmetRequirements } : void 0, actions = [];
2576
2713
  for (const action of task.actions ?? [])
2577
2714
  actions.push(
2578
2715
  await evaluateAction({
@@ -2582,7 +2719,8 @@ async function evaluateTask(args) {
2582
2719
  snapshot,
2583
2720
  taskScope,
2584
2721
  stageHasExits,
2585
- guardDenial
2722
+ guardDenial,
2723
+ requirementsReason
2586
2724
  })
2587
2725
  );
2588
2726
  return {
@@ -2593,12 +2731,22 @@ async function evaluateTask(args) {
2593
2731
  // they're still inbox items. The inbox reads the assignees-kind state
2594
2732
  // entry BY KIND (who-data is state).
2595
2733
  pendingOnActor: (status === "active" || status === "pending") && assigned,
2734
+ ...unmetRequirements.length > 0 ? { unmetRequirements } : {},
2596
2735
  actions
2597
2736
  };
2598
2737
  }
2599
2738
  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({
2739
+ const {
2740
+ action,
2741
+ status,
2742
+ instance,
2743
+ snapshot,
2744
+ taskScope,
2745
+ stageHasExits,
2746
+ guardDenial,
2747
+ requirementsReason
2748
+ } = args, lifecycle = lifecycleReason(instance, status, stageHasExits);
2749
+ 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
2750
  condition: action.filter,
2603
2751
  snapshot,
2604
2752
  params: taskScope
@@ -2875,7 +3023,8 @@ const disabledReasonDetail = {
2875
3023
  "task-not-active": (r) => `task status is "${r.status}"`,
2876
3024
  "stage-terminal": (r) => `stage "${r.stage}" is terminal`,
2877
3025
  "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})` : ""}`
3026
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`,
3027
+ "requirements-unmet": (r) => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`
2879
3028
  };
2880
3029
  function formatDisabledReason(task, action, reason) {
2881
3030
  const detail = disabledReasonDetail[reason.kind](reason);
@@ -3130,7 +3279,7 @@ const workflow = {
3130
3279
  initialStage: definition.initialStage,
3131
3280
  actor
3132
3281
  });
3133
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
3282
+ 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
3283
  },
3135
3284
  /**
3136
3285
  * Fire an action against a task. If the task is pending, it is
@@ -3370,8 +3519,8 @@ const workflow = {
3370
3519
  * Pure read.
3371
3520
  */
3372
3521
  diagnose: async (args) => {
3373
- const evaluation = await evaluateInstance(args);
3374
- return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
3522
+ const evaluation = await evaluateInstance(args), diagnosis = diagnoseInstance(diagnoseInputFromEvaluation(evaluation));
3523
+ return { evaluation, diagnosis, remediations: remediationsFor(diagnosis) };
3375
3524
  },
3376
3525
  /**
3377
3526
  * List the actions an actor could fire on an instance's current stage,
@@ -3540,7 +3689,7 @@ async function claimPendingEffect(client, instance, effectKey, drainerActor) {
3540
3689
  claimedBy: drainerActor
3541
3690
  };
3542
3691
  try {
3543
- return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(), !0;
3692
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({ [`pendingEffects[_key=="${effectKey}"].claim`]: claim }).commit(SYNC_COMMIT), !0;
3544
3693
  } catch (error) {
3545
3694
  if (!isRevisionConflict(error)) throw error;
3546
3695
  return !1;
@@ -3993,6 +4142,7 @@ export {
3993
4142
  ActionParamsInvalidError,
3994
4143
  CascadeLimitError,
3995
4144
  ConcurrentFireActionError,
4145
+ DEFAULT_CONTENT_PERSPECTIVE,
3996
4146
  DISPLAY,
3997
4147
  EFFECTS_CONTEXT_DISPLAY,
3998
4148
  GUARD_DOC_TYPE,
@@ -4007,7 +4157,6 @@ export {
4007
4157
  WorkflowStateDivergedError,
4008
4158
  abortReason,
4009
4159
  actionVerdict,
4010
- assigneesOf,
4011
4160
  availableActions,
4012
4161
  buildSnapshot,
4013
4162
  compileGuard,
@@ -4039,13 +4188,13 @@ export {
4039
4188
  isGdr,
4040
4189
  isTerminalStage,
4041
4190
  lakeGuardId,
4042
- openStage,
4043
4191
  parseGdr,
4044
4192
  readsRaw,
4045
4193
  refCanvas,
4046
4194
  refDashboard,
4047
4195
  refDataset,
4048
4196
  refMediaLibrary,
4197
+ remediationsFor,
4049
4198
  resolveAccess,
4050
4199
  resourceFromParsed,
4051
4200
  retractStageGuards,