@sanity/workflow-engine 0.4.0 → 0.6.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
@@ -262,6 +262,20 @@ function effectBindings(effects) {
262
262
  (e) => Object.entries(e.bindings ?? {}).map(([k, groq]) => [`${e.name}.${k}`, groq])
263
263
  );
264
264
  }
265
+ function actionVerdict(task, action) {
266
+ return {
267
+ task: task.task.name,
268
+ taskStatus: task.status,
269
+ action: action.action.name,
270
+ title: action.action.title,
271
+ allowed: action.allowed,
272
+ disabledReason: action.disabledReason,
273
+ params: action.action.params ?? []
274
+ };
275
+ }
276
+ function availableActions(tasks) {
277
+ return tasks.flatMap((t) => t.actions.map((a) => actionVerdict(t, a)));
278
+ }
265
279
  const wallClock = () => (/* @__PURE__ */ new Date()).toISOString(), GUARD_DOC_TYPE = "temp.system.guard";
266
280
  class MutationGuardDeniedError extends Error {
267
281
  denied;
@@ -456,79 +470,63 @@ function stripStateForLake(value) {
456
470
  function bareId(id) {
457
471
  return isGdrUri(id) ? extractDocumentId(id) : id;
458
472
  }
459
- const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
460
- function resolveGuard(guard, instance, stageName, now) {
461
- const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
462
- if (targets === null) return null;
463
- const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
464
- return { doc: compileGuard({
465
- id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
466
- resourceType: resource.type,
467
- resourceId: resource.id,
468
- owner: GUARD_OWNER,
469
- sourceInstanceId: instance._id,
470
- sourceDefinition: instance.definition,
471
- sourceStage: stageName,
472
- name: guard.name,
473
- ...guard.description !== void 0 ? { description: guard.description } : {},
474
- match: {
475
- ...types !== void 0 ? { types } : {},
476
- idRefs: bareIdRefs(targets),
477
- ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
478
- actions: guard.match.actions
479
- },
480
- predicate: guard.predicate ?? "",
481
- metadata: resolveMetadata(guard.metadata, ctx)
482
- }), routeGdr: targets[0].parsed };
473
+ function abortReason(instance) {
474
+ return instance.history.find(
475
+ (h) => h._type === "aborted"
476
+ )?.reason;
483
477
  }
484
- async function upsertGuard(client, doc) {
485
- if (!await client.getDocument(doc._id)) {
486
- await client.create(doc);
487
- return;
488
- }
489
- const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
490
- await client.patch(doc._id).set(body).commit();
478
+ function diagnoseInputFromEvaluation(evaluation) {
479
+ return {
480
+ instance: evaluation.instance,
481
+ tasks: evaluation.currentStage.tasks,
482
+ transitions: evaluation.currentStage.transitions
483
+ };
491
484
  }
492
- function resolvedStageGuards(args) {
493
- const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
494
- for (const guard of stage?.guards ?? []) {
495
- const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
496
- resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
497
- }
498
- return out;
485
+ function openStage(instance) {
486
+ return findOpenStageEntry(instance);
499
487
  }
500
- async function committedInstance(args) {
501
- return await args.client.getDocument(args.instance._id) ?? void 0;
488
+ function assigneesOf(stage, taskName) {
489
+ return (stage?.tasks.find((t) => t.name === taskName)?.state ?? []).filter((s) => s._type === "assignees").flatMap((s) => s.value);
502
490
  }
503
- async function deployStageGuards(args) {
504
- const live = await committedInstance(args);
505
- if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
506
- for (const { client, doc } of resolvedStageGuards(args))
507
- await upsertGuard(client, doc);
491
+ function isResolved(status) {
492
+ return status === "done" || status === "skipped";
508
493
  }
509
- async function retractStageGuards(args) {
510
- const live = await committedInstance(args);
511
- if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
512
- for (const { client, doc } of resolvedStageGuards(args))
513
- await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
494
+ function failedEffectCause(input) {
495
+ const stalled = new Set(input.tasks.filter((t) => !isResolved(t.status)).map((t) => t.task.name)), effect = [...input.instance.effectHistory].reverse().find((e) => e.status === "failed" && e.origin.kind === "task" && stalled.has(e.origin.name));
496
+ return effect ? { kind: "failed-effect", effect } : void 0;
514
497
  }
515
- function randomKey(length = 12) {
516
- const bytes = new Uint8Array(length);
517
- return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
498
+ function hungEffectCause(input) {
499
+ const effect = input.instance.pendingEffects.find((e) => e.claim !== void 0);
500
+ return effect ? { kind: "hung-effect", effect } : void 0;
518
501
  }
519
- async function evaluateCondition(args) {
520
- const { condition, snapshot, params } = args;
521
- return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
502
+ function failedTaskCause(input) {
503
+ const failed = input.tasks.find((t) => t.status === "failed");
504
+ return failed ? { kind: "failed-task", task: failed.task.name } : void 0;
522
505
  }
523
- async function evaluatePredicates(args) {
524
- const out = {};
525
- for (const [name, groq] of Object.entries(args.predicates ?? {}))
526
- out[name] = !!await runGroq(groq, args.params, args.snapshot);
527
- return out;
506
+ function waitingState(input) {
507
+ const active = input.tasks.find((t) => t.status === "active" && (t.task.actions ?? []).length > 0);
508
+ if (active !== void 0)
509
+ return {
510
+ state: "waiting",
511
+ task: active.task.name,
512
+ assignees: assigneesOf(openStage(input.instance), active.task.name),
513
+ actions: (active.task.actions ?? []).map((a) => a.name)
514
+ };
528
515
  }
529
- async function runGroq(groq, params, snapshot) {
530
- const tree = groqJs.parse(groq, { params });
531
- return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
516
+ 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" };
519
+ }
520
+ function diagnoseInstance(input) {
521
+ const { instance } = input;
522
+ if (instance.abortedAt !== void 0) {
523
+ const reason = abortReason(instance);
524
+ return { state: "aborted", at: instance.abortedAt, ...reason !== void 0 ? { reason } : {} };
525
+ }
526
+ if (instance.completedAt !== void 0)
527
+ return { state: "completed", at: instance.completedAt };
528
+ const cause = failedEffectCause(input) ?? failedTaskCause(input) ?? hungEffectCause(input) ?? noTransitionFiresCause(input);
529
+ return cause !== void 0 ? { state: "stuck", cause } : waitingState(input) ?? { state: "progressing" };
532
530
  }
533
531
  function buildSnapshot(args) {
534
532
  const docs = [], knownIds = /* @__PURE__ */ new Set();
@@ -647,6 +645,191 @@ function resourceFromParsed(parsed) {
647
645
  function collectEntryDocUris(resolvedStateEntries) {
648
646
  return entryDocRefs(resolvedStateEntries).map((ref) => ref.id);
649
647
  }
648
+ const resourceKey = (r) => `${r.type}.${r.id}`;
649
+ function guardsForResource(client) {
650
+ return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
651
+ }
652
+ function instanceGuardQuery(instanceId) {
653
+ return {
654
+ query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
655
+ params: { guardType: GUARD_DOC_TYPE, instanceId }
656
+ };
657
+ }
658
+ function verdictGuardsForInstance(client, instanceId) {
659
+ const { query, params } = instanceGuardQuery(instanceId);
660
+ return client.fetch(query, params);
661
+ }
662
+ async function guardsForInstance(args) {
663
+ const { client, clientForGdr, instance } = args, stateUris = [
664
+ ...collectEntryDocUris(instance.state),
665
+ ...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
666
+ ], clients = resourceClientMap(
667
+ instance.workflowResource,
668
+ client,
669
+ clientForGdr,
670
+ stateUris.map((uri) => tryParseGdr(uri))
671
+ ), { query, params } = instanceGuardQuery(instance._id);
672
+ return queryGuardsAcross(clients.values(), query, params);
673
+ }
674
+ async function guardsForDefinition(args) {
675
+ const perClient = await guardsForDefinitionByClient(args);
676
+ return dedupById(perClient.flatMap((entry) => entry.guards));
677
+ }
678
+ async function guardsForDefinitionByClient(args) {
679
+ const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
680
+ (def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
681
+ ), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs), query = "*[_type == $t && sourceDefinition == $definition]", params = { t: GUARD_DOC_TYPE, definition };
682
+ return Promise.all(
683
+ [...clients.values()].map(async (resourceClient) => ({
684
+ client: resourceClient,
685
+ guards: await resourceClient.fetch(query, params)
686
+ }))
687
+ );
688
+ }
689
+ function guardIdRefs(definition) {
690
+ return definition.stages.flatMap(
691
+ (stage) => (stage.guards ?? []).flatMap(
692
+ (guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
693
+ )
694
+ );
695
+ }
696
+ function staticIdRefGdr(idRef, stage, definition) {
697
+ const read = /^\$state\.([\w-]+)/.exec(idRef);
698
+ if (read === null) return;
699
+ const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
700
+ return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
701
+ }
702
+ function entriesInScope(stage, definition) {
703
+ return [
704
+ ...definition.state ?? [],
705
+ ...stage.state ?? [],
706
+ ...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
707
+ ];
708
+ }
709
+ function gdrFromValue(value) {
710
+ if (typeof value == "string") return tryParseGdr(value);
711
+ if (typeof value == "object" && value !== null && "id" in value) {
712
+ const id = value.id;
713
+ return typeof id == "string" ? tryParseGdr(id) : void 0;
714
+ }
715
+ }
716
+ function tryParseGdr(uri) {
717
+ try {
718
+ return parseGdr(uri);
719
+ } catch {
720
+ return;
721
+ }
722
+ }
723
+ function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
724
+ const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
725
+ for (const parsed of gdrs) {
726
+ if (parsed === void 0) continue;
727
+ const key = resourceKey(resourceFromParsed(parsed));
728
+ map.has(key) || map.set(key, clientForGdr(parsed));
729
+ }
730
+ return map;
731
+ }
732
+ async function queryGuardsAcross(clients, query, params) {
733
+ const perResource = await Promise.all(
734
+ [...clients].map((c) => c.fetch(query, params))
735
+ );
736
+ return dedupById(perResource.flat());
737
+ }
738
+ function dedupById(guards) {
739
+ return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
740
+ }
741
+ const GUARD_OWNER = "robot:workflow-engine", GUARD_LIFTED_PREDICATE = "true";
742
+ function resolveGuard(guard, instance, stageName, now) {
743
+ const ctx = { instance, now }, targets = resolveIdRefTargets(guard.match.idRefs, ctx);
744
+ if (targets === null) return null;
745
+ const resource = assertSingleResource(targets), types = resolveMatchTypes(targets, guard.match.types);
746
+ return { doc: compileGuard({
747
+ id: lakeGuardId({ instanceDocId: instance._id, guardName: guard.name }),
748
+ resourceType: resource.type,
749
+ resourceId: resource.id,
750
+ owner: GUARD_OWNER,
751
+ sourceInstanceId: instance._id,
752
+ sourceDefinition: instance.definition,
753
+ sourceStage: stageName,
754
+ name: guard.name,
755
+ ...guard.description !== void 0 ? { description: guard.description } : {},
756
+ match: {
757
+ ...types !== void 0 ? { types } : {},
758
+ idRefs: bareIdRefs(targets),
759
+ ...guard.match.idPatterns !== void 0 ? { idPatterns: guard.match.idPatterns } : {},
760
+ actions: guard.match.actions
761
+ },
762
+ predicate: guard.predicate ?? "",
763
+ metadata: resolveMetadata(guard.metadata, ctx)
764
+ }), routeGdr: targets[0].parsed };
765
+ }
766
+ async function upsertGuard(client, doc) {
767
+ if (!await client.getDocument(doc._id)) {
768
+ await client.create(doc);
769
+ return;
770
+ }
771
+ const { _id, _type, _rev, _createdAt, _updatedAt, ...body } = doc;
772
+ await client.patch(doc._id).set(body).commit();
773
+ }
774
+ function resolvedStageGuards(args) {
775
+ const stage = args.definition.stages.find((s) => s.name === args.stageName), out = [];
776
+ for (const guard of stage?.guards ?? []) {
777
+ const resolved = resolveGuard(guard, args.instance, args.stageName, args.now);
778
+ resolved !== null && out.push({ client: args.clientForGdr(resolved.routeGdr), doc: resolved.doc });
779
+ }
780
+ return out;
781
+ }
782
+ async function committedInstance(args) {
783
+ return await args.client.getDocument(args.instance._id) ?? void 0;
784
+ }
785
+ async function deployStageGuards(args) {
786
+ const live = await committedInstance(args);
787
+ if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
788
+ for (const { client, doc } of resolvedStageGuards(args))
789
+ await upsertGuard(client, doc);
790
+ }
791
+ async function retractStageGuards(args) {
792
+ const live = await committedInstance(args);
793
+ if (live !== void 0 && !(live.currentStage === args.stageName && live.abortedAt === void 0))
794
+ for (const { client, doc } of resolvedStageGuards(args))
795
+ await client.getDocument(doc._id) && await client.patch(doc._id).set({ predicate: GUARD_LIFTED_PREDICATE }).commit();
796
+ }
797
+ async function deleteOrphanedDefinitionGuards(args) {
798
+ const { client, clientForGdr, workflowResource, definition, definitions, tag } = args, perClient = await guardsForDefinitionByClient({
799
+ client,
800
+ clientForGdr,
801
+ workflowResource,
802
+ definition,
803
+ definitions
804
+ }), ownPartitionPrefix = `${tag}.`;
805
+ let count = 0;
806
+ for (const { client: resourceClient, guards } of perClient) {
807
+ const own = guards.filter((guard) => guard.sourceInstanceId.startsWith(ownPartitionPrefix));
808
+ if (own.length === 0) continue;
809
+ const tx = resourceClient.transaction();
810
+ for (const guard of own) tx.delete(guard._id);
811
+ await tx.commit(), count += own.length;
812
+ }
813
+ return count;
814
+ }
815
+ function randomKey(length = 12) {
816
+ const bytes = new Uint8Array(length);
817
+ return globalThis.crypto.getRandomValues(bytes), [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, length);
818
+ }
819
+ async function evaluateCondition(args) {
820
+ const { condition, snapshot, params } = args;
821
+ return condition === void 0 ? !0 : !!await runGroq(condition, params, snapshot);
822
+ }
823
+ async function evaluatePredicates(args) {
824
+ const out = {};
825
+ for (const [name, groq] of Object.entries(args.predicates ?? {}))
826
+ out[name] = !!await runGroq(groq, args.params, args.snapshot);
827
+ return out;
828
+ }
829
+ async function runGroq(groq, params, snapshot) {
830
+ const tree = groqJs.parse(groq, { params });
831
+ return (await groqJs.evaluate(tree, { dataset: snapshot.docs, params })).get();
832
+ }
650
833
  class StateValueShapeError extends Error {
651
834
  entryType;
652
835
  entryName;
@@ -807,7 +990,7 @@ async function resolveQueryValue(entry, source, ctx, client, defaultValue) {
807
990
  stage: ctx.stageName ?? null,
808
991
  task: ctx.taskName ?? null,
809
992
  now: ctx.now,
810
- engineTags: ctx.tags ?? []
993
+ tag: ctx.tag
811
994
  });
812
995
  try {
813
996
  const fetchOptions = ctx.perspective !== void 0 ? { perspective: ctx.perspective } : void 0, result = await client.fetch(source.query, params, fetchOptions);
@@ -974,7 +1157,7 @@ async function resolveStageStateEntries(args) {
974
1157
  client,
975
1158
  now,
976
1159
  selfId: instance._id,
977
- tags: instance.tags ?? [],
1160
+ tag: instance.tag,
978
1161
  stageName: stage.name,
979
1162
  workflowResource: instance.workflowResource,
980
1163
  // Allow stage-scope entries' `source: { type: "stateRead", scope:
@@ -994,7 +1177,7 @@ async function resolveTaskStateEntries(args) {
994
1177
  client,
995
1178
  now,
996
1179
  selfId: instance._id,
997
- tags: instance.tags ?? [],
1180
+ tag: instance.tag,
998
1181
  stageName: stage.name,
999
1182
  workflowResource: instance.workflowResource,
1000
1183
  taskName: task.name,
@@ -1004,53 +1187,6 @@ async function resolveTaskStateEntries(args) {
1004
1187
  randomKey
1005
1188
  });
1006
1189
  }
1007
- async function resolveBindings(args) {
1008
- const resolved = {};
1009
- for (const [key, groq] of Object.entries(args.bindings ?? {}))
1010
- resolved[key] = await runGroq(groq, args.params, args.snapshot);
1011
- return { ...resolved, ...args.staticInput };
1012
- }
1013
- function effectsContextEntry(name, value) {
1014
- const _key = randomKey();
1015
- return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
1016
- }
1017
- function effectsContextJsonEntry(name, value) {
1018
- return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
1019
- }
1020
- function buildInstanceBase(args) {
1021
- const { id, now, actor, perspective } = args;
1022
- return {
1023
- _id: id,
1024
- _type: WORKFLOW_INSTANCE_TYPE,
1025
- _rev: "",
1026
- _createdAt: now,
1027
- _updatedAt: now,
1028
- tags: args.tags,
1029
- workflowResource: args.workflowResource,
1030
- definition: args.definitionName,
1031
- pinnedVersion: args.pinnedVersion,
1032
- definitionSnapshot: JSON.stringify(args.definition),
1033
- state: args.state,
1034
- effectsContext: args.effectsContext,
1035
- ancestors: args.ancestors,
1036
- ...perspective !== void 0 ? { perspective } : {},
1037
- currentStage: args.initialStage,
1038
- stages: [],
1039
- pendingEffects: [],
1040
- effectHistory: [],
1041
- history: [
1042
- {
1043
- _key: randomKey(),
1044
- _type: "stageEntered",
1045
- at: now,
1046
- stage: args.initialStage,
1047
- ...actor !== void 0 ? { actor } : {}
1048
- }
1049
- ],
1050
- startedAt: now,
1051
- lastChangedAt: now
1052
- };
1053
- }
1054
1190
  class ActionParamsInvalidError extends Error {
1055
1191
  action;
1056
1192
  task;
@@ -1170,21 +1306,108 @@ function stageTransitionHistory(args) {
1170
1306
  }
1171
1307
  ];
1172
1308
  }
1173
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
1174
- function validateTags(tags) {
1175
- if (tags.length === 0)
1176
- throw new Error("tags: must be a non-empty array");
1177
- for (const tag of tags)
1178
- if (!TAG_RE.test(tag))
1179
- throw new Error(
1180
- `tags: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
1181
- );
1309
+ function startMutation(instance) {
1310
+ return {
1311
+ currentStage: instance.currentStage,
1312
+ // Deep-ish copy. Per-scope state entry arrays need their own copies
1313
+ // so ops can mutate without leaking into the source.
1314
+ state: (instance.state ?? []).map((s) => ({ ...s })),
1315
+ stages: instance.stages.map((s) => ({
1316
+ ...s,
1317
+ state: (s.state ?? []).map((entry) => ({ ...entry })),
1318
+ tasks: s.tasks.map((t) => ({
1319
+ ...t,
1320
+ ...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
1321
+ }))
1322
+ })),
1323
+ pendingEffects: [...instance.pendingEffects],
1324
+ effectHistory: [...instance.effectHistory],
1325
+ effectsContext: [...instance.effectsContext],
1326
+ history: [...instance.history],
1327
+ lastChangedAt: instance.lastChangedAt,
1328
+ ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
1329
+ ...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
1330
+ pendingCreates: []
1331
+ };
1182
1332
  }
1183
- function canonicalTag(tags) {
1184
- return tags[0];
1333
+ function instanceStateFields(src) {
1334
+ const fields = {
1335
+ currentStage: src.currentStage,
1336
+ state: src.state,
1337
+ stages: src.stages,
1338
+ pendingEffects: src.pendingEffects,
1339
+ effectHistory: src.effectHistory,
1340
+ effectsContext: src.effectsContext,
1341
+ history: src.history
1342
+ };
1343
+ return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
1185
1344
  }
1186
- function tagScopeFilter(param = "engineTags") {
1187
- return `count(tags[@ in $${param}]) > 0`;
1345
+ function materializeInstance(base, mutation) {
1346
+ return {
1347
+ ...base,
1348
+ currentStage: mutation.currentStage,
1349
+ state: mutation.state,
1350
+ stages: mutation.stages
1351
+ };
1352
+ }
1353
+ async function persist(ctx, mutation) {
1354
+ const set = {
1355
+ ...instanceStateFields(mutation),
1356
+ lastChangedAt: ctx.now
1357
+ }, pendingCreates = mutation.pendingCreates;
1358
+ if (pendingCreates.length === 0)
1359
+ return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
1360
+ const tx = ctx.client.transaction();
1361
+ for (const body of pendingCreates) tx.create(body);
1362
+ tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1363
+ const actorForPriming = void 0;
1364
+ for (const body of pendingCreates)
1365
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1366
+ const reloaded = await ctx.client.getDocument(ctx.instance._id);
1367
+ if (!reloaded)
1368
+ throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
1369
+ return reloaded;
1370
+ }
1371
+ function currentStageEntry(mutation) {
1372
+ const entry = findOpenStageEntry(mutation);
1373
+ if (entry === void 0)
1374
+ throw new Error(
1375
+ `Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
1376
+ );
1377
+ return entry;
1378
+ }
1379
+ function currentTasks(mutation) {
1380
+ return currentStageEntry(mutation).tasks;
1381
+ }
1382
+ function findTaskInCurrentStage(ctx, taskName) {
1383
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
1384
+ if (task === void 0)
1385
+ throw new Error(
1386
+ `Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
1387
+ );
1388
+ return { stage, task };
1389
+ }
1390
+ function requireMutationTaskEntry(mutation, task) {
1391
+ const mutEntry = currentTasks(mutation).find((t) => t.name === task);
1392
+ if (mutEntry === void 0)
1393
+ throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
1394
+ return mutEntry;
1395
+ }
1396
+ function findCurrentStageEntry(instance) {
1397
+ return findOpenStageEntry(instance);
1398
+ }
1399
+ function findCurrentTasks(instance) {
1400
+ return findCurrentStageEntry(instance)?.tasks ?? [];
1401
+ }
1402
+ const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
1403
+ function validateTag(tag) {
1404
+ if (!TAG_RE.test(tag))
1405
+ throw new Error(
1406
+ `tag: invalid tag "${tag}" \u2014 must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`
1407
+ );
1408
+ }
1409
+ function tagScopeFilter() {
1410
+ return "tag == $tag";
1188
1411
  }
1189
1412
  function definitionLookupGroq(explicit) {
1190
1413
  const scoped = `_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}`;
@@ -1202,6 +1425,55 @@ function clientForDiscovery(defaultClient, clientForGdr, subjectGdrUri) {
1202
1425
  return defaultClient;
1203
1426
  }
1204
1427
  }
1428
+ async function reload(client, instanceId, tag) {
1429
+ const doc = await client.getDocument(instanceId);
1430
+ if (!doc)
1431
+ throw new Error(`Workflow instance ${instanceId} not found`);
1432
+ if (doc.tag !== tag)
1433
+ throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
1434
+ return doc;
1435
+ }
1436
+ function effectsContextEntry(name, value) {
1437
+ const _key = randomKey();
1438
+ return typeof value == "string" ? { _key, _type: "effectsContext.string", name, value } : typeof value == "number" ? { _key, _type: "effectsContext.number", name, value } : typeof value == "boolean" ? { _key, _type: "effectsContext.boolean", name, value } : { _key, _type: "effectsContext.ref", name, value };
1439
+ }
1440
+ function effectsContextJsonEntry(name, value) {
1441
+ return { _key: randomKey(), _type: "effectsContext.json", name, value: JSON.stringify(value) };
1442
+ }
1443
+ function buildInstanceBase(args) {
1444
+ const { id, now, actor, perspective } = args;
1445
+ return {
1446
+ _id: id,
1447
+ _type: WORKFLOW_INSTANCE_TYPE,
1448
+ _rev: "",
1449
+ _createdAt: now,
1450
+ _updatedAt: now,
1451
+ tag: args.tag,
1452
+ workflowResource: args.workflowResource,
1453
+ definition: args.definitionName,
1454
+ pinnedVersion: args.pinnedVersion,
1455
+ definitionSnapshot: JSON.stringify(args.definition),
1456
+ state: args.state,
1457
+ effectsContext: args.effectsContext,
1458
+ ancestors: args.ancestors,
1459
+ ...perspective !== void 0 ? { perspective } : {},
1460
+ currentStage: args.initialStage,
1461
+ stages: [],
1462
+ pendingEffects: [],
1463
+ effectHistory: [],
1464
+ history: [
1465
+ {
1466
+ _key: randomKey(),
1467
+ _type: "stageEntered",
1468
+ at: now,
1469
+ stage: args.initialStage,
1470
+ ...actor !== void 0 ? { actor } : {}
1471
+ }
1472
+ ],
1473
+ startedAt: now,
1474
+ lastChangedAt: now
1475
+ };
1476
+ }
1205
1477
  const MAX_SPAWN_DEPTH = 6, SUBWORKFLOWS_ALL_DONE = "count($subworkflows[status != 'done']) == 0", FAIL_WHEN_SYSTEM_ID = "engine.failWhen", COMPLETE_WHEN_SYSTEM_ID = "engine.completeWhen";
1206
1478
  function gateActor(outcome) {
1207
1479
  return {
@@ -1227,7 +1499,7 @@ async function spawnSubworkflows(ctx, mutation, task, sub, actor) {
1227
1499
  `Subworkflow depth limit (${MAX_SPAWN_DEPTH}) exceeded on task "${task.name}" of ${ctx.instance._id}. Ancestor chain: ${chain}`
1228
1500
  );
1229
1501
  }
1230
- const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tags ?? []);
1502
+ const now = ctx.now, definition = await resolveDefinitionRef(ctx.client, sub.definition, ctx.instance.tag);
1231
1503
  if (definition === null) {
1232
1504
  const versionLabel = sub.definition.version === void 0 || sub.definition.version === "latest" ? "latest" : `v${sub.definition.version}`;
1233
1505
  throw new Error(
@@ -1317,15 +1589,15 @@ function coerceSpawnGdr(raw, workflowResource) {
1317
1589
  }
1318
1590
  return typeof raw == "string" && raw.length > 0 ? { id: toUri(raw), type: "document" } : null;
1319
1591
  }
1320
- async function resolveDefinitionRef(client, ref, tags) {
1321
- const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
1592
+ async function resolveDefinitionRef(client, ref, tag) {
1593
+ const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, tag };
1322
1594
  return wantsExplicit && (params.version = ref.version), await client.fetch(
1323
1595
  definitionLookupGroq(wantsExplicit),
1324
1596
  params
1325
1597
  ) ?? null;
1326
1598
  }
1327
1599
  async function prepareChildInstance(args) {
1328
- const { client, parent, definition, initialState, effectsContext, actor, now } = args, childTags = parent.tags ?? [], childPrefix = childTags[0] ?? "wf", workflowResource = parent.workflowResource, childDocId = `${childPrefix}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
1600
+ const { client, parent, definition, initialState, effectsContext, actor, now } = args, childTag = parent.tag, workflowResource = parent.workflowResource, childDocId = `${childTag}.wf-instance.${randomKey()}`, childRef = { id: gdrFromResource(workflowResource, childDocId), type: WORKFLOW_INSTANCE_TYPE }, ancestors = [
1329
1601
  ...parent.ancestors,
1330
1602
  {
1331
1603
  id: selfGdr(parent),
@@ -1338,7 +1610,7 @@ async function prepareChildInstance(args) {
1338
1610
  client,
1339
1611
  now,
1340
1612
  selfId: childDocId,
1341
- tags: childTags,
1613
+ tag: childTag,
1342
1614
  workflowResource,
1343
1615
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1344
1616
  },
@@ -1357,7 +1629,7 @@ async function prepareChildInstance(args) {
1357
1629
  ), base = buildInstanceBase({
1358
1630
  id: childDocId,
1359
1631
  now,
1360
- tags: childTags,
1632
+ tag: childTag,
1361
1633
  workflowResource,
1362
1634
  definitionName: definition.name,
1363
1635
  pinnedVersion: definition.version,
@@ -1369,16 +1641,121 @@ async function prepareChildInstance(args) {
1369
1641
  initialStage: definition.initialStage,
1370
1642
  actor
1371
1643
  });
1372
- return { ref: childRef, body: base };
1373
- }
1374
- async function subworkflowRows(ctx, refs) {
1375
- if (refs.length === 0) return [];
1376
- const ids = refs.map((r) => gdrToBareId(r.id));
1377
- return (await ctx.client.fetch("*[_id in $ids]{_id, currentStage, completedAt}", { ids })).map((c) => ({
1378
- _id: c._id,
1379
- stage: c.currentStage,
1380
- status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
1381
- }));
1644
+ return { ref: childRef, body: base };
1645
+ }
1646
+ async function subworkflowRows(ctx, refs) {
1647
+ if (refs.length === 0) return [];
1648
+ const ids = refs.map((r) => gdrToBareId(r.id));
1649
+ return (await ctx.client.fetch("*[_id in $ids]{_id, currentStage, completedAt}", { ids })).map((c) => ({
1650
+ _id: c._id,
1651
+ stage: c.currentStage,
1652
+ status: c.completedAt !== void 0 && c.completedAt !== null ? "done" : "active"
1653
+ }));
1654
+ }
1655
+ async function resolveBindings(args) {
1656
+ const resolved = {};
1657
+ for (const [key, groq] of Object.entries(args.bindings ?? {}))
1658
+ resolved[key] = await runGroq(groq, args.params, args.snapshot);
1659
+ return { ...resolved, ...args.staticInput };
1660
+ }
1661
+ async function completeEffect(args) {
1662
+ const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1663
+ ...options?.clock ? { clock: options.clock } : {},
1664
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1665
+ });
1666
+ return commitCompleteEffect(
1667
+ ctx,
1668
+ effectKey,
1669
+ status,
1670
+ outputs,
1671
+ detail,
1672
+ error,
1673
+ durationMs,
1674
+ options?.actor
1675
+ );
1676
+ }
1677
+ function buildEffectHistoryEntry(pending, outcome) {
1678
+ const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
1679
+ return {
1680
+ _key: pending._key,
1681
+ name: pending.name,
1682
+ ...pending.title !== void 0 ? { title: pending.title } : {},
1683
+ ...pending.description !== void 0 ? { description: pending.description } : {},
1684
+ params: pending.params,
1685
+ origin: pending.origin,
1686
+ ...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
1687
+ ranAt,
1688
+ ...durationMs !== void 0 ? { durationMs } : {},
1689
+ status,
1690
+ ...detail !== void 0 ? { detail } : {},
1691
+ ...error !== void 0 ? { error } : {},
1692
+ ...outputs !== void 0 ? { outputs } : {}
1693
+ };
1694
+ }
1695
+ function upsertEffectsContext(mutation, effectName, outputs) {
1696
+ const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
1697
+ existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
1698
+ }
1699
+ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
1700
+ const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
1701
+ if (pending === void 0)
1702
+ throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
1703
+ const mutation = startMutation(ctx.instance);
1704
+ mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1705
+ const ranAt = ctx.now;
1706
+ return mutation.effectHistory.push(
1707
+ buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1708
+ ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1709
+ _key: randomKey(),
1710
+ _type: "effectCompleted",
1711
+ at: ranAt,
1712
+ effectKey,
1713
+ effect: pending.name,
1714
+ status,
1715
+ ...outputs !== void 0 ? { outputs } : {},
1716
+ ...detail !== void 0 ? { detail } : {},
1717
+ ...actor !== void 0 ? { actor } : {}
1718
+ }), await persist(ctx, mutation), { effectKey, status };
1719
+ }
1720
+ function buildQueuedEffect(effect, origin, params, actor, now) {
1721
+ const key = randomKey(), pending = {
1722
+ _key: key,
1723
+ _type: "pendingEffect",
1724
+ name: effect.name,
1725
+ ...effect.title !== void 0 ? { title: effect.title } : {},
1726
+ ...effect.description !== void 0 ? { description: effect.description } : {},
1727
+ ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
1728
+ params,
1729
+ origin,
1730
+ ...actor !== void 0 ? { actor } : {},
1731
+ queuedAt: now
1732
+ }, history = {
1733
+ _key: randomKey(),
1734
+ _type: "effectQueued",
1735
+ at: now,
1736
+ effectKey: key,
1737
+ effect: effect.name,
1738
+ origin
1739
+ };
1740
+ return { pending, history };
1741
+ }
1742
+ async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
1743
+ if (!effects || effects.length === 0) return;
1744
+ const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
1745
+ ...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
1746
+ ...actor !== void 0 ? { actor } : {},
1747
+ // Caller-supplied action params, readable in bindings as `$params.<name>`.
1748
+ vars: { params: opts?.callerParams ?? {} }
1749
+ });
1750
+ for (const effect of effects) {
1751
+ const resolved = await resolveBindings({
1752
+ bindings: effect.bindings,
1753
+ staticInput: effect.input,
1754
+ snapshot: ctx.snapshot,
1755
+ params
1756
+ }), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
1757
+ mutation.pendingEffects.push(pending), mutation.history.push(history);
1758
+ }
1382
1759
  }
1383
1760
  function resolveStaticSource(src, ctx) {
1384
1761
  switch (src.type) {
@@ -1609,7 +1986,8 @@ function evalOpPredicate(p, row, ctx) {
1609
1986
  }
1610
1987
  async function invokeTask(args) {
1611
1988
  const { client, instanceId, task: taskName, options } = args, ctx = await loadContext(client, instanceId, {
1612
- ...options?.clock ? { clock: options.clock } : {}
1989
+ ...options?.clock ? { clock: options.clock } : {},
1990
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1613
1991
  });
1614
1992
  return commitInvoke(ctx, taskName, options?.actor);
1615
1993
  }
@@ -1717,7 +2095,8 @@ function isTerminalStage(stage) {
1717
2095
  }
1718
2096
  async function setStage(args) {
1719
2097
  const { client, instanceId, targetStage, reason, options } = args, ctx = await loadContext(client, instanceId, {
1720
- ...options?.clock ? { clock: options.clock } : {}
2098
+ ...options?.clock ? { clock: options.clock } : {},
2099
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1721
2100
  });
1722
2101
  return commitSetStage(ctx, targetStage, reason, options?.actor);
1723
2102
  }
@@ -1973,282 +2352,53 @@ async function cascadeAutoTransitions(client, instanceId, actor, clientForGdr, c
1973
2352
  ...actor !== void 0 ? { options: { actor } } : {},
1974
2353
  ...clientForGdr ? { clientForGdr } : {},
1975
2354
  ...clock ? { clock } : {},
1976
- ...overlay ? { overlay } : {}
1977
- })).fired) return count;
1978
- if (count++, count >= CASCADE_LIMIT)
1979
- throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
1980
- }
1981
- }
1982
- async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
1983
- const parentRef = child.ancestors.at(-1);
1984
- if (parentRef === void 0) return;
1985
- const parent = await client.getDocument(gdrToBareId(parentRef.id));
1986
- if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
1987
- const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
1988
- if (stage === void 0) return;
1989
- const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.subworkflows === void 0 ? !1 : parentCurrentTasks.find((e) => e.name === t.name)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
1990
- if (task?.subworkflows === void 0) return;
1991
- const entry = parentCurrentTasks.find((e) => e.name === task.name);
1992
- if (entry === void 0 || entry.status !== "active") return;
1993
- const ctx = await buildEngineContext({
1994
- client,
1995
- clientForGdr,
1996
- instance: parent,
1997
- definition,
1998
- ...clock ? { clock } : {}
1999
- }), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
2000
- if (outcome !== void 0)
2001
- return { parent, definition, stage, task, outcome };
2002
- }
2003
- async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
2004
- const instance = await client.getDocument(instanceId);
2005
- if (!instance) return;
2006
- const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
2007
- if (found === void 0) return;
2008
- const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
2009
- client,
2010
- clientForGdr: resolvedClientForGdr,
2011
- instance: parent,
2012
- definition,
2013
- ...clock ? { clock } : {}
2014
- }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
2015
- mutEntry !== void 0 && (applyTaskStatusChange({
2016
- entry: mutEntry,
2017
- history: mutation.history,
2018
- stage: stage.name,
2019
- to: outcome,
2020
- at: ctx.now,
2021
- actor: gateActor(outcome)
2022
- }), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
2023
- }
2024
- function startMutation(instance) {
2025
- return {
2026
- currentStage: instance.currentStage,
2027
- // Deep-ish copy. Per-scope state entry arrays need their own copies
2028
- // so ops can mutate without leaking into the source.
2029
- state: (instance.state ?? []).map((s) => ({ ...s })),
2030
- stages: instance.stages.map((s) => ({
2031
- ...s,
2032
- state: (s.state ?? []).map((entry) => ({ ...entry })),
2033
- tasks: s.tasks.map((t) => ({
2034
- ...t,
2035
- ...t.state !== void 0 ? { state: t.state.map((entry) => ({ ...entry })) } : {}
2036
- }))
2037
- })),
2038
- pendingEffects: [...instance.pendingEffects],
2039
- effectHistory: [...instance.effectHistory],
2040
- effectsContext: [...instance.effectsContext],
2041
- history: [...instance.history],
2042
- lastChangedAt: instance.lastChangedAt,
2043
- ...instance.completedAt !== void 0 ? { completedAt: instance.completedAt } : {},
2044
- ...instance.abortedAt !== void 0 ? { abortedAt: instance.abortedAt } : {},
2045
- pendingCreates: []
2046
- };
2047
- }
2048
- function instanceStateFields(src) {
2049
- const fields = {
2050
- currentStage: src.currentStage,
2051
- state: src.state,
2052
- stages: src.stages,
2053
- pendingEffects: src.pendingEffects,
2054
- effectHistory: src.effectHistory,
2055
- effectsContext: src.effectsContext,
2056
- history: src.history
2057
- };
2058
- return src.completedAt !== void 0 && (fields.completedAt = src.completedAt), src.abortedAt !== void 0 && (fields.abortedAt = src.abortedAt), fields;
2059
- }
2060
- function materializeInstance(base, mutation) {
2061
- return {
2062
- ...base,
2063
- currentStage: mutation.currentStage,
2064
- state: mutation.state,
2065
- stages: mutation.stages
2066
- };
2067
- }
2068
- async function persist(ctx, mutation) {
2069
- const set = {
2070
- ...instanceStateFields(mutation),
2071
- lastChangedAt: ctx.now
2072
- }, pendingCreates = mutation.pendingCreates;
2073
- if (pendingCreates.length === 0)
2074
- return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit();
2075
- const tx = ctx.client.transaction();
2076
- for (const body of pendingCreates) tx.create(body);
2077
- tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
2078
- const actorForPriming = void 0;
2079
- for (const body of pendingCreates)
2080
- 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);
2081
- const reloaded = await ctx.client.getDocument(ctx.instance._id);
2082
- if (!reloaded)
2083
- throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
2084
- return reloaded;
2085
- }
2086
- function currentStageEntry(mutation) {
2087
- const entry = findOpenStageEntry(mutation);
2088
- if (entry === void 0)
2089
- throw new Error(
2090
- `Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`
2091
- );
2092
- return entry;
2093
- }
2094
- function currentTasks(mutation) {
2095
- return currentStageEntry(mutation).tasks;
2096
- }
2097
- function findTaskInCurrentStage(ctx, taskName) {
2098
- const stage = findStage(ctx.definition, ctx.instance.currentStage), task = (stage.tasks ?? []).find((t) => t.name === taskName);
2099
- if (task === void 0)
2100
- throw new Error(
2101
- `Task "${taskName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`
2102
- );
2103
- return { stage, task };
2104
- }
2105
- function requireMutationTaskEntry(mutation, task) {
2106
- const mutEntry = currentTasks(mutation).find((t) => t.name === task);
2107
- if (mutEntry === void 0)
2108
- throw new Error(`Task "${task}" disappeared from mutation copy \u2014 invariant broken`);
2109
- return mutEntry;
2110
- }
2111
- function findCurrentStageEntry(instance) {
2112
- return findOpenStageEntry(instance);
2113
- }
2114
- function findCurrentTasks(instance) {
2115
- return findCurrentStageEntry(instance)?.tasks ?? [];
2116
- }
2117
- async function completeEffect(args) {
2118
- const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
2119
- ...options?.clock ? { clock: options.clock } : {}
2120
- });
2121
- return commitCompleteEffect(
2122
- ctx,
2123
- effectKey,
2124
- status,
2125
- outputs,
2126
- detail,
2127
- error,
2128
- durationMs,
2129
- options?.actor
2130
- );
2131
- }
2132
- function buildEffectHistoryEntry(pending, outcome) {
2133
- const { status, ranAt, actor, detail, error, durationMs, outputs } = outcome, resolvedActor = actor ?? pending.actor;
2134
- return {
2135
- _key: pending._key,
2136
- name: pending.name,
2137
- ...pending.title !== void 0 ? { title: pending.title } : {},
2138
- ...pending.description !== void 0 ? { description: pending.description } : {},
2139
- params: pending.params,
2140
- origin: pending.origin,
2141
- ...resolvedActor !== void 0 ? { actor: resolvedActor } : {},
2142
- ranAt,
2143
- ...durationMs !== void 0 ? { durationMs } : {},
2144
- status,
2145
- ...detail !== void 0 ? { detail } : {},
2146
- ...error !== void 0 ? { error } : {},
2147
- ...outputs !== void 0 ? { outputs } : {}
2148
- };
2149
- }
2150
- function upsertEffectsContext(mutation, effectName, outputs) {
2151
- const existingIndex = mutation.effectsContext.findIndex((e) => e.name === effectName), entry = effectsContextJsonEntry(effectName, outputs);
2152
- existingIndex >= 0 ? mutation.effectsContext[existingIndex] = entry : mutation.effectsContext.push(entry);
2153
- }
2154
- async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, error, durationMs, actor) {
2155
- const pending = ctx.instance.pendingEffects.find((e) => e._key === effectKey);
2156
- if (pending === void 0)
2157
- throw new Error(`Pending effect "${effectKey}" not found on instance ${ctx.instance._id}`);
2158
- const mutation = startMutation(ctx.instance);
2159
- mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
2160
- const ranAt = ctx.now;
2161
- return mutation.effectHistory.push(
2162
- buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
2163
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
2164
- _key: randomKey(),
2165
- _type: "effectCompleted",
2166
- at: ranAt,
2167
- effectKey,
2168
- effect: pending.name,
2169
- status,
2170
- ...outputs !== void 0 ? { outputs } : {},
2171
- ...detail !== void 0 ? { detail } : {},
2172
- ...actor !== void 0 ? { actor } : {}
2173
- }), await persist(ctx, mutation), { effectKey, status };
2174
- }
2175
- function buildQueuedEffect(effect, origin, params, actor, now) {
2176
- const key = randomKey(), pending = {
2177
- _key: key,
2178
- _type: "pendingEffect",
2179
- name: effect.name,
2180
- ...effect.title !== void 0 ? { title: effect.title } : {},
2181
- ...effect.description !== void 0 ? { description: effect.description } : {},
2182
- ...effect.bindings !== void 0 ? { bindings: effect.bindings } : {},
2183
- params,
2184
- origin,
2185
- ...actor !== void 0 ? { actor } : {},
2186
- queuedAt: now
2187
- }, history = {
2188
- _key: randomKey(),
2189
- _type: "effectQueued",
2190
- at: now,
2191
- effectKey: key,
2192
- effect: effect.name,
2193
- origin
2194
- };
2195
- return { pending, history };
2196
- }
2197
- async function queueEffects(ctx, mutation, effects, origin, actor, opts) {
2198
- if (!effects || effects.length === 0) return;
2199
- const now = ctx.now, liveCtx = { ...ctx, instance: materializeInstance(ctx.instance, mutation) }, params = await ctxConditionParams(liveCtx, {
2200
- ...opts?.taskName !== void 0 ? { taskName: opts.taskName } : {},
2201
- ...actor !== void 0 ? { actor } : {},
2202
- // Caller-supplied action params, readable in bindings as `$params.<name>`.
2203
- vars: { params: opts?.callerParams ?? {} }
2204
- });
2205
- for (const effect of effects) {
2206
- const resolved = await resolveBindings({
2207
- bindings: effect.bindings,
2208
- staticInput: effect.input,
2209
- snapshot: ctx.snapshot,
2210
- params
2211
- }), { pending, history } = buildQueuedEffect(effect, origin, resolved, actor, now);
2212
- mutation.pendingEffects.push(pending), mutation.history.push(history);
2355
+ ...overlay ? { overlay } : {}
2356
+ })).fired) return count;
2357
+ if (count++, count >= CASCADE_LIMIT)
2358
+ throw new CascadeLimitError({ instanceId, limit: CASCADE_LIMIT });
2213
2359
  }
2214
2360
  }
2215
- async function abortInstance(args) {
2216
- const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
2217
- ...options?.clock ? { clock: options.clock } : {}
2218
- });
2219
- return commitAbort(ctx, reason, options?.actor);
2361
+ async function findResolvedParentSpawn(client, child, clientForGdr, clock) {
2362
+ const parentRef = child.ancestors.at(-1);
2363
+ if (parentRef === void 0) return;
2364
+ const parent = await client.getDocument(gdrToBareId(parentRef.id));
2365
+ if (!parent || parent._type !== WORKFLOW_INSTANCE_TYPE || typeof parent.definitionSnapshot != "string") return;
2366
+ const definition = parseDefinitionSnapshot(parent), stage = definition.stages.find((s) => s.name === parent.currentStage);
2367
+ if (stage === void 0) return;
2368
+ const parentCurrentTasks = findCurrentTasks(parent), task = (stage.tasks ?? []).find((t) => t.subworkflows === void 0 ? !1 : parentCurrentTasks.find((e) => e.name === t.name)?.spawnedInstances?.some((r) => gdrToBareId(r.id) === child._id) ?? !1);
2369
+ if (task?.subworkflows === void 0) return;
2370
+ const entry = parentCurrentTasks.find((e) => e.name === task.name);
2371
+ if (entry === void 0 || entry.status !== "active") return;
2372
+ const ctx = await buildEngineContext({
2373
+ client,
2374
+ clientForGdr,
2375
+ instance: parent,
2376
+ definition,
2377
+ ...clock ? { clock } : {}
2378
+ }), outcome = await evaluateResolutionGate(ctx, task, await taskConditionVars(ctx, entry));
2379
+ if (outcome !== void 0)
2380
+ return { parent, definition, stage, task, outcome };
2220
2381
  }
2221
- async function commitAbort(ctx, reason, actor) {
2222
- if (ctx.instance.completedAt !== void 0)
2223
- return { fired: !1 };
2224
- const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
2225
- return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
2226
- ...mutation.pendingEffects.map(
2227
- (pending) => buildEffectHistoryEntry(pending, {
2228
- status: "failed",
2229
- ranAt: at,
2230
- actor,
2231
- detail: "instance aborted before dispatch",
2232
- error: void 0,
2233
- durationMs: void 0,
2234
- outputs: void 0
2235
- })
2236
- )
2237
- ), mutation.pendingEffects = [], mutation.history.push({
2238
- _key: randomKey(),
2239
- _type: "aborted",
2240
- at,
2382
+ async function propagateToAncestors(client, instanceId, actor, clientForGdr, clock) {
2383
+ const instance = await client.getDocument(instanceId);
2384
+ if (!instance) return;
2385
+ const resolvedClientForGdr = clientForGdr ?? (() => client), found = await findResolvedParentSpawn(client, instance, resolvedClientForGdr, clock);
2386
+ if (found === void 0) return;
2387
+ const { parent, definition, stage, task, outcome } = found, ctx = await buildEngineContext({
2388
+ client,
2389
+ clientForGdr: resolvedClientForGdr,
2390
+ instance: parent,
2391
+ definition,
2392
+ ...clock ? { clock } : {}
2393
+ }), mutation = startMutation(parent), mutEntry = currentTasks(mutation).find((e) => e.name === task.name);
2394
+ mutEntry !== void 0 && (applyTaskStatusChange({
2395
+ entry: mutEntry,
2396
+ history: mutation.history,
2241
2397
  stage: stage.name,
2242
- ...reason !== void 0 ? { reason } : {},
2243
- ...actor !== void 0 ? { actor } : {}
2244
- }), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
2245
- client: ctx.client,
2246
- clientForGdr: ctx.clientForGdr,
2247
- instance: ctx.instance,
2248
- definition: ctx.definition,
2249
- stageName: stage.name,
2250
- now: ctx.now
2251
- }), { fired: !0, stage: stage.name };
2398
+ to: outcome,
2399
+ at: ctx.now,
2400
+ actor: gateActor(outcome)
2401
+ }), await persist(ctx, mutation), await cascadeAutoTransitions(client, parent._id, actor, clientForGdr, clock), await propagateToAncestors(client, parent._id, actor, clientForGdr, clock));
2252
2402
  }
2253
2403
  const parsedFilters = /* @__PURE__ */ new Map();
2254
2404
  async function matchesFilter(args) {
@@ -2344,105 +2494,13 @@ async function fetchGrantsCached(requestFn, resourcePath) {
2344
2494
  return;
2345
2495
  }
2346
2496
  }
2347
- const resourceKey = (r) => `${r.type}.${r.id}`;
2348
- function guardsForResource(client) {
2349
- return client.fetch("*[_type == $t] | order(_id asc)", { t: GUARD_DOC_TYPE });
2350
- }
2351
- function instanceGuardQuery(instanceId) {
2352
- return {
2353
- query: "*[_type == $guardType && sourceInstanceId == $instanceId]",
2354
- params: { guardType: GUARD_DOC_TYPE, instanceId }
2355
- };
2356
- }
2357
- function verdictGuardsForInstance(client, instanceId) {
2358
- const { query, params } = instanceGuardQuery(instanceId);
2359
- return client.fetch(query, params);
2360
- }
2361
- async function guardsForInstance(args) {
2362
- const { client, clientForGdr, instance } = args, stateUris = [
2363
- ...collectEntryDocUris(instance.state),
2364
- ...instance.stages.flatMap((stage) => collectEntryDocUris(stage.state))
2365
- ], clients = resourceClientMap(
2366
- instance.workflowResource,
2367
- client,
2368
- clientForGdr,
2369
- stateUris.map((uri) => tryParseGdr(uri))
2370
- ), { query, params } = instanceGuardQuery(instance._id);
2371
- return queryGuardsAcross(clients.values(), query, params);
2372
- }
2373
- async function guardsForDefinition(args) {
2374
- const { client, clientForGdr, workflowResource, definition, definitions } = args, gdrs = definitions.flatMap(
2375
- (def) => guardIdRefs(def).map(({ idRef, stage }) => staticIdRefGdr(idRef, stage, def))
2376
- ), clients = resourceClientMap(workflowResource, client, clientForGdr, gdrs);
2377
- return queryGuardsAcross(clients.values(), "*[_type == $t && sourceDefinition == $definition]", {
2378
- t: GUARD_DOC_TYPE,
2379
- definition
2380
- });
2381
- }
2382
- function guardIdRefs(definition) {
2383
- return definition.stages.flatMap(
2384
- (stage) => (stage.guards ?? []).flatMap(
2385
- (guard) => (guard.match.idRefs ?? []).map((idRef) => ({ idRef, stage }))
2386
- )
2387
- );
2388
- }
2389
- function staticIdRefGdr(idRef, stage, definition) {
2390
- const read = /^\$state\.([\w-]+)/.exec(idRef);
2391
- if (read === null) return;
2392
- const source = entriesInScope(stage, definition).find((e) => e.name === read[1])?.source;
2393
- return source?.type === "literal" ? gdrFromValue(source.value) : void 0;
2394
- }
2395
- function entriesInScope(stage, definition) {
2396
- return [
2397
- ...definition.state ?? [],
2398
- ...stage.state ?? [],
2399
- ...(stage.tasks ?? []).flatMap((task) => task.state ?? [])
2400
- ];
2401
- }
2402
- function gdrFromValue(value) {
2403
- if (typeof value == "string") return tryParseGdr(value);
2404
- if (typeof value == "object" && value !== null && "id" in value) {
2405
- const id = value.id;
2406
- return typeof id == "string" ? tryParseGdr(id) : void 0;
2407
- }
2408
- }
2409
- function tryParseGdr(uri) {
2410
- try {
2411
- return parseGdr(uri);
2412
- } catch {
2413
- return;
2414
- }
2415
- }
2416
- function resourceClientMap(base, baseClient, clientForGdr, gdrs) {
2417
- const map = /* @__PURE__ */ new Map([[resourceKey(base), baseClient]]);
2418
- for (const parsed of gdrs) {
2419
- if (parsed === void 0) continue;
2420
- const key = resourceKey(resourceFromParsed(parsed));
2421
- map.has(key) || map.set(key, clientForGdr(parsed));
2422
- }
2423
- return map;
2424
- }
2425
- async function queryGuardsAcross(clients, query, params) {
2426
- const perResource = await Promise.all(
2427
- [...clients].map((c) => c.fetch(query, params))
2428
- );
2429
- return dedupById(perResource.flat());
2430
- }
2431
- function dedupById(guards) {
2432
- return [...new Map(guards.map((g) => [g._id, g])).values()].sort((a, b) => a._id.localeCompare(b._id));
2433
- }
2434
2497
  async function evaluateInstance(args) {
2435
- const { client, tags, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2436
- validateTags(tags);
2498
+ const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2499
+ validateTag(tag);
2437
2500
  const { actor, grants } = await resolveAccess(client, {
2438
2501
  ...args.access !== void 0 ? { override: args.access } : {},
2439
2502
  ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
2440
- }), instance = await client.getDocument(instanceId);
2441
- if (!instance)
2442
- throw new Error(`Workflow instance "${instanceId}" not found`);
2443
- if (!tags.some((t) => instance.tags?.includes(t)))
2444
- throw new Error(`Workflow instance "${instanceId}" not visible to this engine (tag mismatch)`);
2445
- const definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
2503
+ }), instance = await reload(client, instanceId, tag), definition = parseDefinitionSnapshot(instance), snapshot = await hydrateSnapshot({ client, clientForGdr: resourceClients !== void 0 ? (parsed) => resourceClients(parsed) ?? client : () => client, instance }), guards = await verdictGuardsForInstance(client, instance._id);
2446
2504
  return evaluateFromSnapshot({
2447
2505
  instance,
2448
2506
  definition,
@@ -2583,22 +2641,21 @@ function lifecycleReason(instance, status, stageHasExits) {
2583
2641
  function disabled(action, reason) {
2584
2642
  return { action, allowed: !1, disabledReason: reason };
2585
2643
  }
2586
- function definitionDocId(_workflowResource, prefix, definition, version) {
2587
- return `${prefix}.${definition}.v${version}`;
2644
+ function definitionDocId(_workflowResource, tag, definition, version) {
2645
+ return `${tag}.${definition}.v${version}`;
2588
2646
  }
2589
- async function sortByDependencies(client, definitions, tags, workflowResource) {
2590
- const prefix = canonicalTag(tags), byName = /* @__PURE__ */ new Map();
2647
+ async function sortByDependencies(client, definitions, tag, workflowResource) {
2648
+ const byName = /* @__PURE__ */ new Map();
2591
2649
  for (const def of definitions) {
2592
2650
  const list = byName.get(def.name) ?? [];
2593
2651
  list.push(def), byName.set(def.name, list);
2594
2652
  }
2595
2653
  const findInBatch = (ref) => findRefInBatch(byName, ref);
2596
2654
  return await assertCrossBatchRefsDeployed(client, definitions, {
2597
- tags,
2655
+ tag,
2598
2656
  workflowResource,
2599
- prefix,
2600
2657
  findInBatch
2601
- }), topoSortDefinitions(definitions, { workflowResource, prefix, findInBatch });
2658
+ }), topoSortDefinitions(definitions, { workflowResource, tag, findInBatch });
2602
2659
  }
2603
2660
  function findRefInBatch(byName, ref) {
2604
2661
  const candidates = byName.get(ref.name);
@@ -2611,10 +2668,10 @@ function findRefInBatch(byName, ref) {
2611
2668
  async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2612
2669
  const missing = [];
2613
2670
  for (const def of definitions) {
2614
- const fromId = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
2671
+ const fromId = definitionDocId(ctx.workflowResource, ctx.tag, def.name, def.version);
2615
2672
  for (const ref of refsOf(def)) {
2616
2673
  if (ctx.findInBatch(ref) !== void 0) continue;
2617
- const label = await resolveDeployedRefLabel(client, ref, ctx.tags);
2674
+ const label = await resolveDeployedRefLabel(client, ref, ctx.tag);
2618
2675
  label !== void 0 && missing.push({ from: fromId, ref: label });
2619
2676
  }
2620
2677
  }
@@ -2626,8 +2683,8 @@ async function assertCrossBatchRefsDeployed(client, definitions, ctx) {
2626
2683
  `)
2627
2684
  );
2628
2685
  }
2629
- async function resolveDeployedRefLabel(client, ref, tags) {
2630
- const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, engineTags: tags };
2686
+ async function resolveDeployedRefLabel(client, ref, tag) {
2687
+ const wantsExplicit = typeof ref.version == "number", params = { definition: ref.name, tag };
2631
2688
  if (wantsExplicit && (params.version = ref.version), !await client.fetch(
2632
2689
  definitionLookupGroq(wantsExplicit),
2633
2690
  params
@@ -2636,7 +2693,7 @@ async function resolveDeployedRefLabel(client, ref, tags) {
2636
2693
  }
2637
2694
  function topoSortDefinitions(definitions, ctx) {
2638
2695
  const visited = /* @__PURE__ */ new Set(), visiting = /* @__PURE__ */ new Set(), ordered = [], visit = (def) => {
2639
- const id = definitionDocId(ctx.workflowResource, ctx.prefix, def.name, def.version);
2696
+ const id = definitionDocId(ctx.workflowResource, ctx.tag, def.name, def.version);
2640
2697
  if (!visited.has(id)) {
2641
2698
  if (visiting.has(id))
2642
2699
  throw new Error(`workflow.deployDefinitions: dependency cycle involving "${id}"`);
@@ -2677,11 +2734,11 @@ function stableStringify(value) {
2677
2734
  ([a], [b]) => a.localeCompare(b)
2678
2735
  ).map(([k, v2]) => `${JSON.stringify(k)}:${stableStringify(v2)}`).join(",")}}`;
2679
2736
  }
2680
- async function loadDefinition(client, definition, version, tags) {
2737
+ async function loadDefinition(client, definition, version, tag) {
2681
2738
  if (version !== void 0) {
2682
2739
  const doc = await client.fetch(
2683
2740
  definitionLookupGroq(!0),
2684
- { definition, version, engineTags: tags }
2741
+ { definition, version, tag }
2685
2742
  );
2686
2743
  if (!doc)
2687
2744
  throw new Error(`Workflow definition ${definition} v${version} not deployed`);
@@ -2689,23 +2746,63 @@ async function loadDefinition(client, definition, version, tags) {
2689
2746
  }
2690
2747
  const latest = await client.fetch(definitionLookupGroq(!1), {
2691
2748
  definition,
2692
- engineTags: tags
2749
+ tag
2693
2750
  });
2694
2751
  if (!latest)
2695
2752
  throw new Error(`No deployed definition for workflow ${definition}`);
2696
2753
  return latest;
2697
2754
  }
2698
- async function loadDefinitionVersions(client, definition, tags) {
2755
+ async function loadDefinitionVersions(client, definition, tag) {
2699
2756
  return client.fetch(
2700
- `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && count(tags[@ in $engineTags]) > 0] | order(version desc)`,
2701
- { definition, engineTags: tags }
2757
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && name == $definition && ${tagScopeFilter()}] | order(version desc)`,
2758
+ { definition, tag }
2702
2759
  );
2703
2760
  }
2761
+ async function abortInstance(args) {
2762
+ const { client, instanceId, reason, options } = args, ctx = await loadContext(client, instanceId, {
2763
+ ...options?.clock ? { clock: options.clock } : {},
2764
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
2765
+ });
2766
+ return commitAbort(ctx, reason, options?.actor);
2767
+ }
2768
+ async function commitAbort(ctx, reason, actor) {
2769
+ if (ctx.instance.completedAt !== void 0)
2770
+ return { fired: !1 };
2771
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), at = ctx.now, openEntry = findOpenStageEntry(mutation);
2772
+ return openEntry !== void 0 && (openEntry.exitedAt = at), mutation.effectHistory.push(
2773
+ ...mutation.pendingEffects.map(
2774
+ (pending) => buildEffectHistoryEntry(pending, {
2775
+ status: "failed",
2776
+ ranAt: at,
2777
+ actor,
2778
+ detail: "instance aborted before dispatch",
2779
+ error: void 0,
2780
+ durationMs: void 0,
2781
+ outputs: void 0
2782
+ })
2783
+ )
2784
+ ), mutation.pendingEffects = [], mutation.history.push({
2785
+ _key: randomKey(),
2786
+ _type: "aborted",
2787
+ at,
2788
+ stage: stage.name,
2789
+ ...reason !== void 0 ? { reason } : {},
2790
+ ...actor !== void 0 ? { actor } : {}
2791
+ }), mutation.completedAt = at, mutation.abortedAt = at, await persist(ctx, mutation), await retractStageGuards({
2792
+ client: ctx.client,
2793
+ clientForGdr: ctx.clientForGdr,
2794
+ instance: ctx.instance,
2795
+ definition: ctx.definition,
2796
+ stageName: stage.name,
2797
+ now: ctx.now
2798
+ }), { fired: !0, stage: stage.name };
2799
+ }
2704
2800
  async function fireAction(args) {
2705
2801
  const { client, instanceId, task, action, params, options } = args;
2706
2802
  for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2707
2803
  const ctx = await loadContext(client, instanceId, {
2708
- ...options?.clock ? { clock: options.clock } : {}
2804
+ ...options?.clock ? { clock: options.clock } : {},
2805
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
2709
2806
  });
2710
2807
  try {
2711
2808
  return await commitAction(ctx, task, action, params, options);
@@ -2809,7 +2906,7 @@ function bareIdFromSpawnRef(uri) {
2809
2906
  }
2810
2907
  }
2811
2908
  async function resolveOperationContext(args) {
2812
- validateTags(args.tags);
2909
+ validateTag(args.tag);
2813
2910
  const access = await resolveAccess(args.client, {
2814
2911
  ...args.access !== void 0 ? { override: args.access } : {},
2815
2912
  ...args.grantsFromPath !== void 0 ? { grantsFromPath: args.grantsFromPath } : {}
@@ -2831,19 +2928,18 @@ async function cascade(client, instanceId, actor, clientForGdr, clock, overlay)
2831
2928
  );
2832
2929
  return await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), count;
2833
2930
  }
2931
+ async function abortAndPropagate(args) {
2932
+ const { client, instanceId, reason, actor, clientForGdr, clock } = args, result = await abortInstance({
2933
+ client,
2934
+ instanceId,
2935
+ ...reason !== void 0 ? { reason } : {},
2936
+ options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
2937
+ });
2938
+ return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), result.fired;
2939
+ }
2834
2940
  function buildClientForGdr(defaultClient, resolver) {
2835
2941
  return resolver === void 0 ? () => defaultClient : (parsed) => resolver(parsed) ?? defaultClient;
2836
2942
  }
2837
- async function reload(client, instanceId, tags) {
2838
- const doc = await client.getDocument(instanceId);
2839
- if (!doc) throw new Error(`Workflow instance ${instanceId} not found`);
2840
- if (!intersectsTags(doc.tags, tags))
2841
- throw new Error(`Workflow instance ${instanceId} not visible to this engine (tag mismatch)`);
2842
- return doc;
2843
- }
2844
- function intersectsTags(docTags, engineTags) {
2845
- return !docTags || docTags.length === 0 ? !1 : engineTags.some((t) => docTags.includes(t));
2846
- }
2847
2943
  function toEffectsContextEntries(ctx) {
2848
2944
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
2849
2945
  }
@@ -2853,10 +2949,11 @@ function assertActionAllowed(evaluation, task, action) {
2853
2949
  throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
2854
2950
  }
2855
2951
  async function applyAction(args) {
2856
- const { client, instance, task, action, params, actor, grants, clock } = args, options = {
2952
+ const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = {
2857
2953
  actor,
2858
2954
  ...grants !== void 0 ? { grants } : {},
2859
- ...clock !== void 0 ? { clock } : {}
2955
+ ...clock !== void 0 ? { clock } : {},
2956
+ ...clientForGdr !== void 0 ? { clientForGdr } : {}
2860
2957
  };
2861
2958
  return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
2862
2959
  client,
@@ -2867,6 +2964,78 @@ async function applyAction(args) {
2867
2964
  options
2868
2965
  })).ranOps;
2869
2966
  }
2967
+ async function deleteDefinition(args) {
2968
+ const { client, tag, definition, version, cascade: cascade2, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, versions = await loadDefinitionVersions(client, definition, tag);
2969
+ if (versions.length === 0)
2970
+ throw new Error(`No deployed definition for workflow ${definition}`);
2971
+ const targets = version === void 0 ? versions : versions.filter((d) => d.version === version);
2972
+ if (targets.length === 0)
2973
+ throw new Error(`Workflow definition ${definition} v${version} not deployed`);
2974
+ const lastVersionGoes = targets.length === versions.length;
2975
+ await assertNoSpawnReferrers(client, tag, definition, targets, lastVersionGoes);
2976
+ const instanceIds = await nonTerminalInstanceIds(client, tag, definition, version);
2977
+ if (instanceIds.length > 0 && cascade2 !== !0)
2978
+ throw new Error(
2979
+ `Cannot delete ${definition}: ${instanceIds.length} non-terminal instance(s) exist (${previewIds(instanceIds)}). Pass cascade to abort them first \u2014 instances are aborted in place, never deleted.`
2980
+ );
2981
+ const abortedInstanceIds = await abortInstances({
2982
+ client,
2983
+ instanceIds,
2984
+ reason,
2985
+ actor,
2986
+ clientForGdr,
2987
+ clock
2988
+ }), tx = client.transaction();
2989
+ for (const target of targets) tx.delete(target._id);
2990
+ await tx.commit();
2991
+ const deletedGuardCount = lastVersionGoes ? await deleteOrphanedDefinitionGuards({
2992
+ client,
2993
+ clientForGdr,
2994
+ workflowResource: args.workflowResource,
2995
+ definition,
2996
+ definitions: versions,
2997
+ tag
2998
+ }) : 0;
2999
+ return {
3000
+ name: definition,
3001
+ deletedVersions: targets.map((d) => d.version),
3002
+ abortedInstanceIds,
3003
+ deletedGuardCount
3004
+ };
3005
+ }
3006
+ async function assertNoSpawnReferrers(client, tag, definition, targets, lastVersionGoes) {
3007
+ const deployed = await client.fetch(
3008
+ `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}]`,
3009
+ { tag }
3010
+ ), targetIds = new Set(targets.map((d) => d._id)), targetVersions = new Set(targets.map((d) => d.version)), referrers = deployed.filter((d) => !targetIds.has(d._id)).filter(
3011
+ (d) => refsOf(d).some(
3012
+ (ref) => ref.name === definition && (typeof ref.version == "number" ? targetVersions.has(ref.version) : lastVersionGoes)
3013
+ )
3014
+ );
3015
+ if (referrers.length > 0) {
3016
+ const names = referrers.map((d) => `${d.name} v${d.version}`).join(", ");
3017
+ throw new Error(
3018
+ `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`
3019
+ );
3020
+ }
3021
+ }
3022
+ async function nonTerminalInstanceIds(client, tag, definition, version) {
3023
+ const versionFilter = version === void 0 ? "" : " && pinnedVersion == $version";
3024
+ return (await client.fetch(
3025
+ `*[_type == "${WORKFLOW_INSTANCE_TYPE}" && definition == $definition && ${tagScopeFilter()} && !defined(completedAt)${versionFilter}] | order(_id asc){_id}`,
3026
+ { definition, tag, ...version !== void 0 ? { version } : {} }
3027
+ )).map((doc) => doc._id);
3028
+ }
3029
+ async function abortInstances(args) {
3030
+ const { client, instanceIds, reason, actor, clientForGdr, clock } = args, aborted = [];
3031
+ for (const instanceId of instanceIds)
3032
+ await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock }) && aborted.push(instanceId);
3033
+ return aborted;
3034
+ }
3035
+ function previewIds(ids) {
3036
+ const head = ids.slice(0, 3).join(", ");
3037
+ return ids.length > 3 ? `${head}, \u2026` : head;
3038
+ }
2870
3039
  const workflow = {
2871
3040
  /**
2872
3041
  * Deploy a set of workflow definitions as one call. Each lands as a
@@ -2885,20 +3054,20 @@ const workflow = {
2885
3054
  * Cycles in the dependency graph error before any write happens.
2886
3055
  */
2887
3056
  deployDefinitions: async (args) => {
2888
- const { client, tags, workflowResource, definitions } = args;
2889
- validateTags(tags);
3057
+ const { client, tag, workflowResource, definitions } = args;
3058
+ validateTag(tag);
2890
3059
  for (const def of definitions)
2891
3060
  validateDefinition(def);
2892
- const ordered = await sortByDependencies(client, definitions, tags, workflowResource), results = [], prefix = canonicalTag(tags), tx = client.transaction();
3061
+ const ordered = await sortByDependencies(client, definitions, tag, workflowResource), results = [], tx = client.transaction();
2893
3062
  let hasWrites = !1;
2894
3063
  for (const def of ordered) {
2895
- const id = definitionDocId(workflowResource, prefix, def.name, def.version), expected = {
3064
+ const id = definitionDocId(workflowResource, tag, def.name, def.version), expected = {
2896
3065
  ...def,
2897
3066
  _id: id,
2898
3067
  _type: schema.WORKFLOW_DEFINITION_TYPE,
2899
- tags
3068
+ tag
2900
3069
  }, existing = await client.getDocument(id);
2901
- if (existing && intersectsTags(existing.tags, tags) && isDefinitionUnchanged(existing, expected)) {
3070
+ if (existing && existing.tag === tag && isDefinitionUnchanged(existing, expected)) {
2902
3071
  results.push({ definition: def.name, version: def.version, status: "unchanged" });
2903
3072
  continue;
2904
3073
  }
@@ -2917,6 +3086,14 @@ const workflow = {
2917
3086
  }
2918
3087
  return hasWrites && await tx.commit(), { results };
2919
3088
  },
3089
+ /**
3090
+ * Remove a deployed workflow definition (all versions, or one via
3091
+ * `version`). Refuses while non-terminal instances exist unless
3092
+ * `cascade` aborts them first — instances are never deleted, only
3093
+ * aborted in place; see {@link deleteDefinitionInternal} for the
3094
+ * full contract (spawn-referrer check, guard-doc housekeeping).
3095
+ */
3096
+ deleteDefinition: async (args) => deleteDefinition(args),
2920
3097
  /**
2921
3098
  * Spawn a new workflow instance from a deployed definition.
2922
3099
  *
@@ -2928,7 +3105,7 @@ const workflow = {
2928
3105
  startInstance: async (args) => {
2929
3106
  const {
2930
3107
  client,
2931
- tags,
3108
+ tag,
2932
3109
  workflowResource,
2933
3110
  definition: definitionName,
2934
3111
  version,
@@ -2937,7 +3114,7 @@ const workflow = {
2937
3114
  effectsContext,
2938
3115
  instanceId,
2939
3116
  perspective
2940
- } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tags), id = instanceId ?? `${canonicalTag(tags)}.wf-instance.${randomKey()}`;
3117
+ } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, definition = await loadDefinition(client, definitionName, version, tag), id = instanceId ?? `${tag}.wf-instance.${randomKey()}`;
2941
3118
  if (definition.stages.find((s) => s.name === definition.initialStage) === void 0)
2942
3119
  throw new Error(
2943
3120
  `Initial stage "${definition.initialStage}" missing in ${definitionName} v${definition.version}`
@@ -2949,7 +3126,7 @@ const workflow = {
2949
3126
  client,
2950
3127
  now,
2951
3128
  selfId: id,
2952
- tags,
3129
+ tag,
2953
3130
  workflowResource,
2954
3131
  ...perspective !== void 0 ? { perspective } : {}
2955
3132
  },
@@ -2957,7 +3134,7 @@ const workflow = {
2957
3134
  }), effectivePerspective = perspective ?? derivePerspectiveFromState(resolvedState), base = buildInstanceBase({
2958
3135
  id,
2959
3136
  now,
2960
- tags,
3137
+ tag,
2961
3138
  workflowResource,
2962
3139
  definitionName: definition.name,
2963
3140
  pinnedVersion: definition.version,
@@ -2969,7 +3146,7 @@ const workflow = {
2969
3146
  initialStage: definition.initialStage,
2970
3147
  actor
2971
3148
  });
2972
- return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tags);
3149
+ return await client.create(base), await primeInitialStage(client, id, actor, clientForGdr, clock), await cascade(client, id, actor, clientForGdr, clock), reload(client, id, tag);
2973
3150
  },
2974
3151
  /**
2975
3152
  * Fire an action against a task. If the task is pending, it is
@@ -2984,19 +3161,19 @@ const workflow = {
2984
3161
  fireAction: async (args) => {
2985
3162
  const {
2986
3163
  client,
2987
- tags,
3164
+ tag,
2988
3165
  instanceId,
2989
3166
  task,
2990
3167
  action,
2991
3168
  params,
2992
3169
  idempotent,
2993
3170
  resourceClients
2994
- } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tags);
3171
+ } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
2995
3172
  if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
2996
3173
  return { instance: before, cascaded: 0, fired: !1 };
2997
3174
  const evaluation = await evaluateInstance({
2998
3175
  client,
2999
- tags,
3176
+ tag,
3000
3177
  instanceId,
3001
3178
  access,
3002
3179
  clock,
@@ -3011,10 +3188,11 @@ const workflow = {
3011
3188
  actor,
3012
3189
  ...access.grants !== void 0 ? { grants: access.grants } : {},
3013
3190
  clock,
3191
+ clientForGdr,
3014
3192
  ...params !== void 0 ? { params } : {}
3015
3193
  }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
3016
3194
  return {
3017
- instance: await reload(client, instanceId, tags),
3195
+ instance: await reload(client, instanceId, tag),
3018
3196
  cascaded,
3019
3197
  fired: !0,
3020
3198
  ...ranOps !== void 0 ? { ranOps } : {}
@@ -3028,7 +3206,7 @@ const workflow = {
3028
3206
  * reference them. Cascades after.
3029
3207
  */
3030
3208
  completeEffect: async (args) => {
3031
- const { client, tags, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3209
+ const { client, tag, instanceId, effectKey, status, outputs, detail, error, durationMs } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3032
3210
  await completeEffect({
3033
3211
  client,
3034
3212
  instanceId,
@@ -3038,11 +3216,11 @@ const workflow = {
3038
3216
  ...detail !== void 0 ? { detail } : {},
3039
3217
  ...error !== void 0 ? { error } : {},
3040
3218
  ...durationMs !== void 0 ? { durationMs } : {},
3041
- options: { ...actor !== void 0 ? { actor } : {}, clock }
3219
+ options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
3042
3220
  });
3043
3221
  const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
3044
3222
  return {
3045
- instance: await reload(client, instanceId, tags),
3223
+ instance: await reload(client, instanceId, tag),
3046
3224
  cascaded,
3047
3225
  fired: !0
3048
3226
  };
@@ -3057,11 +3235,11 @@ const workflow = {
3057
3235
  * affected instances and the engine re-evaluates.
3058
3236
  */
3059
3237
  tick: async (args) => {
3060
- const { client, tags, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, current = await reload(client, instanceId, tags), guards = await verdictGuardsForInstance(client, current._id);
3238
+ const { client, tag, instanceId } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, current = await reload(client, instanceId, tag), guards = await verdictGuardsForInstance(client, current._id);
3061
3239
  await assertInstanceWriteAllowed({ instance: current, guards, identity: actor.id });
3062
3240
  const cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
3063
3241
  return {
3064
- instance: await reload(client, instanceId, tags),
3242
+ instance: await reload(client, instanceId, tag),
3065
3243
  cascaded,
3066
3244
  fired: cascaded > 0
3067
3245
  };
@@ -3072,56 +3250,50 @@ const workflow = {
3072
3250
  * upstream; this verb performs the mechanical move.
3073
3251
  */
3074
3252
  setStage: async (args) => {
3075
- const { client, tags, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3076
- await reload(client, instanceId, tags);
3253
+ const { client, tag, instanceId, targetStage, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3254
+ await reload(client, instanceId, tag);
3077
3255
  const result = await setStage({
3078
3256
  client,
3079
3257
  instanceId,
3080
3258
  targetStage,
3081
3259
  ...reason !== void 0 ? { reason } : {},
3082
- options: { ...actor !== void 0 ? { actor } : {}, clock }
3260
+ options: { ...actor !== void 0 ? { actor } : {}, clock, clientForGdr }
3083
3261
  }), cascaded = result.fired ? await cascade(client, instanceId, actor, clientForGdr, clock) : 0;
3084
3262
  return {
3085
- instance: await reload(client, instanceId, tags),
3263
+ instance: await reload(client, instanceId, tag),
3086
3264
  cascaded,
3087
3265
  fired: result.fired
3088
3266
  };
3089
3267
  },
3090
3268
  /**
3091
3269
  * Admin override — hard-stop an in-flight instance where it stands.
3092
- * No stage move, no transition effects, pending effects cancelled; see
3093
- * the engine-level {@link engineAbortInstance} for the full contract.
3094
- * Ancestors are propagated (not cascaded — the instance is terminal)
3095
- * so a parent gate waiting on this child re-evaluates.
3270
+ * No stage move, no transition effects, pending effects cancelled;
3271
+ * see {@link abortAndPropagate} for the abort + ancestor-propagation
3272
+ * contract (propagated, not cascaded — the instance is terminal).
3096
3273
  */
3097
3274
  abortInstance: async (args) => {
3098
- const { client, tags, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3099
- await reload(client, instanceId, tags);
3100
- const result = await abortInstance({
3101
- client,
3102
- instanceId,
3103
- ...reason !== void 0 ? { reason } : {},
3104
- options: { ...actor !== void 0 ? { actor } : {}, clock }
3105
- });
3106
- return result.fired && await propagateToAncestors(client, instanceId, actor, clientForGdr, clock), {
3107
- instance: await reload(client, instanceId, tags),
3275
+ const { client, tag, instanceId, reason } = args, { actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock;
3276
+ await reload(client, instanceId, tag);
3277
+ const fired = await abortAndPropagate({ client, instanceId, reason, actor, clientForGdr, clock });
3278
+ return {
3279
+ instance: await reload(client, instanceId, tag),
3108
3280
  cascaded: 0,
3109
- fired: result.fired
3281
+ fired
3110
3282
  };
3111
3283
  },
3112
3284
  /**
3113
- * Fetch a workflow instance by id, scoped to the engine's tags.
3285
+ * Fetch a workflow instance by id, scoped to the engine's tag.
3114
3286
  * Throws when the instance doesn't exist or isn't visible to this
3115
3287
  * engine.
3116
3288
  */
3117
3289
  getInstance: async (args) => {
3118
- const { client, tags, instanceId } = args;
3119
- return validateTags(tags), reload(client, instanceId, tags);
3290
+ const { client, tag, instanceId } = args;
3291
+ return validateTag(tag), reload(client, instanceId, tag);
3120
3292
  },
3121
3293
  guardsForInstance: async (args) => {
3122
- const { client, tags, instanceId, resourceClients } = args;
3123
- validateTags(tags);
3124
- const instance = await reload(client, instanceId, tags);
3294
+ const { client, tag, instanceId, resourceClients } = args;
3295
+ validateTag(tag);
3296
+ const instance = await reload(client, instanceId, tag);
3125
3297
  return guardsForInstance({
3126
3298
  client,
3127
3299
  clientForGdr: buildClientForGdr(client, resourceClients),
@@ -3129,9 +3301,9 @@ const workflow = {
3129
3301
  });
3130
3302
  },
3131
3303
  guardsForDefinition: async (args) => {
3132
- const { client, tags, workflowResource, definition, resourceClients } = args;
3133
- validateTags(tags);
3134
- const definitions = await loadDefinitionVersions(client, definition, tags);
3304
+ const { client, tag, workflowResource, definition, resourceClients } = args;
3305
+ validateTag(tag);
3306
+ const definitions = await loadDefinitionVersions(client, definition, tag);
3135
3307
  if (definitions.length === 0)
3136
3308
  throw new Error(`No deployed definition for workflow ${definition}`);
3137
3309
  return guardsForDefinition({
@@ -3143,22 +3315,21 @@ const workflow = {
3143
3315
  });
3144
3316
  },
3145
3317
  /**
3146
- * Run a caller-supplied GROQ query with the engine's tags bound as
3147
- * `$engineTags`. This does NOT rewrite the query — arbitrary GROQ
3318
+ * Run a caller-supplied GROQ query with the engine's tag bound as
3319
+ * `$tag`. This does NOT rewrite the query — arbitrary GROQ
3148
3320
  * can't be safely tag-scoped after the fact — so the CALLER MUST
3149
- * filter on `$engineTags` (e.g.
3150
- * `count(tags[@ in $engineTags]) > 0`). To guard against accidental
3151
- * cross-tenant reads, a query that never references `$engineTags` is
3321
+ * filter on `$tag` (e.g. `tag == $tag`). To guard against accidental
3322
+ * cross-partition reads, a query that never references `$tag` is
3152
3323
  * rejected before it reaches the lake. Caller is responsible for type
3153
3324
  * narrowing the result.
3154
3325
  */
3155
3326
  query: async (args) => {
3156
- const { client, tags, groq, params } = args;
3157
- if (validateTags(tags), !groq.includes("$engineTags"))
3327
+ const { client, tag, groq, params } = args;
3328
+ if (validateTag(tag), !/\$tag\b/.test(groq))
3158
3329
  throw new Error(
3159
- `workflow.query: query must filter on $engineTags to stay tag-scoped (e.g. "count(tags[@ in $engineTags]) > 0"); got: ${groq}`
3330
+ `workflow.query: query must filter on $tag to stay tag-scoped (e.g. "tag == $tag"); got: ${groq}`
3160
3331
  );
3161
- return client.fetch(groq, { ...params, engineTags: tags });
3332
+ return client.fetch(groq, { ...params, tag });
3162
3333
  },
3163
3334
  /**
3164
3335
  * Snapshot-aware GROQ — runs against the same in-memory view that
@@ -3177,9 +3348,9 @@ const workflow = {
3177
3348
  * without re-implementing hydration. Pure read — never writes.
3178
3349
  */
3179
3350
  queryInScope: async (args) => {
3180
- const { client, tags, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
3181
- validateTags(tags);
3182
- const instance = await reload(client, instanceId, tags), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams({ instance, now: clock(), snapshot }), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
3351
+ const { client, tag, instanceId, groq, params, resourceClients } = args, clock = args.clock ?? wallClock;
3352
+ validateTag(tag);
3353
+ const instance = await reload(client, instanceId, tag), clientForGdr = buildClientForGdr(client, resourceClients), snapshot = await hydrateSnapshot({ client, clientForGdr, instance }), reserved = buildParams({ instance, now: clock(), snapshot }), tree = groqJs.parse(groq, { params: { ...reserved, ...params } });
3183
3354
  return await (await groqJs.evaluate(tree, {
3184
3355
  dataset: snapshot.docs,
3185
3356
  params: { ...reserved, ...params }
@@ -3189,13 +3360,13 @@ const workflow = {
3189
3360
  * List every pending effect on the instance. Returns the same entries
3190
3361
  * the runtime would see — claimed and unclaimed alike.
3191
3362
  */
3192
- listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tags)).pendingEffects,
3363
+ listPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tag)).pendingEffects,
3193
3364
  /**
3194
3365
  * Filter pending effects on the instance by criteria. `claimed`
3195
3366
  * filters on claim presence; `names` restricts to specific effect
3196
3367
  * names. Both filters compose (AND).
3197
3368
  */
3198
- findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tags)).pendingEffects.filter((e) => !(args.claimed === !0 && e.claim === void 0 || args.claimed === !1 && e.claim !== void 0 || args.names !== void 0 && !args.names.includes(e.name))),
3369
+ findPendingEffects: async (args) => (await reload(args.client, args.instanceId, args.tag)).pendingEffects.filter((e) => !(args.claimed === !0 && e.claim === void 0 || args.claimed === !1 && e.claim !== void 0 || args.names !== void 0 && !args.names.includes(e.name))),
3199
3370
  /**
3200
3371
  * Project the instance from a given actor's perspective. Returns a
3201
3372
  * `WorkflowEvaluation` with per-action verdicts (`allowed` + a
@@ -3205,6 +3376,30 @@ const workflow = {
3205
3376
  * `fireAction` to gate writes via the same logic.
3206
3377
  */
3207
3378
  evaluate: async (args) => evaluateInstance(args),
3379
+ /**
3380
+ * Diagnose why an instance is or isn't progressing. Projects the
3381
+ * instance (the same read as `evaluate`) and classifies it — terminal,
3382
+ * `progressing`, `waiting` (an action is available — healthy), or `stuck`
3383
+ * with a structured cause — returning that verdict as a
3384
+ * {@link DiagnoseResult} alongside the evaluation it came from, so a
3385
+ * consumer can render the supporting evidence without a second projection.
3386
+ * Pure read.
3387
+ */
3388
+ diagnose: async (args) => {
3389
+ const evaluation = await evaluateInstance(args);
3390
+ return { evaluation, diagnosis: diagnoseInstance(diagnoseInputFromEvaluation(evaluation)) };
3391
+ },
3392
+ /**
3393
+ * List the actions an actor could fire on an instance's current stage,
3394
+ * each flagged `allowed` (with a structured `disabledReason` when not).
3395
+ * Projects the instance from the actor's perspective and flattens its
3396
+ * tasks' actions. Returns the evaluation alongside the actions so a
3397
+ * consumer can read the instance/stage context. Pure read.
3398
+ */
3399
+ availableActions: async (args) => {
3400
+ const evaluation = await evaluateInstance(args);
3401
+ return { evaluation, actions: availableActions(evaluation.currentStage.tasks) };
3402
+ },
3208
3403
  /**
3209
3404
  * Materialised spawned children of a parent instance.
3210
3405
  *
@@ -3212,18 +3407,18 @@ const workflow = {
3212
3407
  * record. (The per-task `spawnedInstances` field on the active stage
3213
3408
  * only exists while that stage is current; history survives.) Strips
3214
3409
  * the GDR URI on each `instanceRef` to a bare `_id`, fetches the
3215
- * instances, drops any that aren't visible to this engine's tags, and
3410
+ * instances, drops any that aren't visible to this engine's tag, and
3216
3411
  * returns them sorted by `startedAt` ascending.
3217
3412
  *
3218
3413
  * Pass `task` to restrict to a single spawning task on the parent.
3219
3414
  */
3220
3415
  children: async (args) => {
3221
- const { client, tags, instanceId, task } = args;
3222
- validateTags(tags);
3223
- const parent = await reload(client, instanceId, tags), isSpawned = (h) => h._type === "spawned", ids = parent.history.filter(isSpawned).filter((h) => task === void 0 || h.task === task).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
3416
+ const { client, tag, instanceId, task } = args;
3417
+ validateTag(tag);
3418
+ const parent = await reload(client, instanceId, tag), isSpawned = (h) => h._type === "spawned", ids = parent.history.filter(isSpawned).filter((h) => task === void 0 || h.task === task).flatMap((h) => bareIdFromSpawnRef(h.instanceRef.id));
3224
3419
  return ids.length === 0 ? [] : client.fetch(
3225
3420
  `*[_id in $ids && ${tagScopeFilter()}] | order(startedAt asc)`,
3226
- { ids, engineTags: tags }
3421
+ { ids, tag }
3227
3422
  );
3228
3423
  },
3229
3424
  /**
@@ -3238,11 +3433,11 @@ const workflow = {
3238
3433
  }
3239
3434
  };
3240
3435
  async function verifyDeployedDefinitionsInternal(args) {
3241
- const { client, tags, effectHandlers, missingHandler, logger } = args;
3242
- validateTags(tags);
3436
+ const { client, tag, effectHandlers, missingHandler, logger } = args;
3437
+ validateTag(tag);
3243
3438
  const log = logger("verifyDeployedDefinitions"), definitions = await client.fetch(
3244
3439
  `*[_type == "${schema.WORKFLOW_DEFINITION_TYPE}" && ${tagScopeFilter()}] | order(name asc, version asc)`,
3245
- { engineTags: tags }
3440
+ { tag }
3246
3441
  ), seen = [], missingByName = /* @__PURE__ */ new Map();
3247
3442
  for (const def of definitions)
3248
3443
  seen.push({ name: def.name, version: def.version, _id: def._id }), walkEffectNames(def, (name, location) => {
@@ -3291,7 +3486,7 @@ function walkEffectNames(def, visit) {
3291
3486
  async function drainEffectsInternal(args) {
3292
3487
  const {
3293
3488
  client,
3294
- tags,
3489
+ tag,
3295
3490
  workflowResource,
3296
3491
  resourceClients,
3297
3492
  instanceId,
@@ -3302,10 +3497,10 @@ async function drainEffectsInternal(args) {
3302
3497
  actor: drainerActor,
3303
3498
  ...args.access?.grants !== void 0 ? { grants: args.access.grants } : {}
3304
3499
  };
3305
- validateTags(tags);
3500
+ validateTag(tag);
3306
3501
  const log = logger("drainEffects"), drained = [], failed = [], skipped = [], skippedKeys = /* @__PURE__ */ new Set();
3307
3502
  for (; ; ) {
3308
- const before = await reload(client, instanceId, tags), candidate = before.pendingEffects.find(
3503
+ const before = await reload(client, instanceId, tag), candidate = before.pendingEffects.find(
3309
3504
  (e) => e.claim === void 0 && !skippedKeys.has(e._key)
3310
3505
  );
3311
3506
  if (candidate === void 0) break;
@@ -3322,7 +3517,7 @@ async function drainEffectsInternal(args) {
3322
3517
  });
3323
3518
  await reportEffectOutcome({
3324
3519
  client,
3325
- tags,
3520
+ tag,
3326
3521
  workflowResource,
3327
3522
  ...resourceClients !== void 0 ? { resourceClients } : {},
3328
3523
  instanceId,
@@ -3392,7 +3587,7 @@ async function applyMissingHandler(policy, info, log) {
3392
3587
  }
3393
3588
  }
3394
3589
  function createInstanceSession(args) {
3395
- const { client, tags, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
3590
+ const { client, tag, clock } = args, clientForGdr = buildClientForGdr(client, args.resourceClients);
3396
3591
  let instance = asHeldInstance(args.instance), overlay = /* @__PURE__ */ new Map(), heldGuards = [], committing = !1, buffered;
3397
3592
  const selfUri = () => gdrFromResource(instance.workflowResource, instance._id), applyUpdate = (docs) => {
3398
3593
  const next = /* @__PURE__ */ new Map();
@@ -3453,7 +3648,7 @@ function createInstanceSession(args) {
3453
3648
  return commit(async (actor, held) => {
3454
3649
  await assertInstanceWriteAllowed({ instance, guards: heldGuards, identity: actor.id });
3455
3650
  const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3456
- return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: cascaded > 0 };
3651
+ return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: cascaded > 0 };
3457
3652
  });
3458
3653
  },
3459
3654
  fireAction({ task, action, params }) {
@@ -3465,11 +3660,12 @@ function createInstanceSession(args) {
3465
3660
  task,
3466
3661
  action,
3467
3662
  actor,
3663
+ clientForGdr,
3468
3664
  ...grants !== void 0 ? { grants } : {},
3469
3665
  ...clock !== void 0 ? { clock } : {},
3470
3666
  ...params !== void 0 ? { params } : {}
3471
3667
  }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3472
- return instance = await reload(client, instance._id, tags), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
3668
+ return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
3473
3669
  });
3474
3670
  }
3475
3671
  };
@@ -3497,11 +3693,11 @@ const defaultLoggerFactory = (name) => ({
3497
3693
  }
3498
3694
  };
3499
3695
  function createEngine(args) {
3500
- const { client, workflowResource, tags, resourceClients, clock } = args;
3501
- validateTags(tags);
3696
+ const { client, workflowResource, resourceClients, clock, tag } = args;
3697
+ validateTag(tag);
3502
3698
  const effectHandlers = args.effectHandlers ?? {}, missingHandler = args.missingHandler ?? "fail", logger = args.loggerFactory ?? defaultLoggerFactory, bind = (rest) => ({
3503
3699
  client,
3504
- tags,
3700
+ tag,
3505
3701
  workflowResource,
3506
3702
  ...resourceClients !== void 0 ? { resourceClients } : {},
3507
3703
  ...clock !== void 0 ? { clock } : {},
@@ -3509,7 +3705,7 @@ function createEngine(args) {
3509
3705
  });
3510
3706
  return {
3511
3707
  client,
3512
- tags,
3708
+ tag,
3513
3709
  workflowResource,
3514
3710
  effectHandlers,
3515
3711
  missingHandler,
@@ -3520,13 +3716,16 @@ function createEngine(args) {
3520
3716
  completeEffect: (rest) => workflow.completeEffect(bind(rest)),
3521
3717
  tick: (rest) => workflow.tick(bind(rest)),
3522
3718
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
3719
+ diagnose: (rest) => workflow.diagnose(bind(rest)),
3720
+ availableActions: (rest) => workflow.availableActions(bind(rest)),
3523
3721
  setStage: (rest) => workflow.setStage(bind(rest)),
3524
3722
  abortInstance: (rest) => workflow.abortInstance(bind(rest)),
3525
- getInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }),
3526
- subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tags, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
3723
+ deleteDefinition: (rest) => workflow.deleteDefinition(bind(rest)),
3724
+ getInstance: ({ instanceId }) => workflow.getInstance({ client, tag, workflowResource, instanceId }),
3725
+ subscriptionDocumentsForInstance: ({ instanceId }) => workflow.getInstance({ client, tag, workflowResource, instanceId }).then(subscriptionDocumentsForInstance),
3527
3726
  instance: (instanceDoc, opts) => createInstanceSession({
3528
3727
  client,
3529
- tags,
3728
+ tag,
3530
3729
  instance: instanceDoc,
3531
3730
  ...resourceClients !== void 0 ? { resourceClients } : {},
3532
3731
  ...clock !== void 0 ? { clock } : {},
@@ -3535,45 +3734,45 @@ function createEngine(args) {
3535
3734
  }),
3536
3735
  guardsForInstance: ({ instanceId }) => workflow.guardsForInstance({
3537
3736
  client,
3538
- tags,
3737
+ tag,
3539
3738
  workflowResource,
3540
3739
  instanceId,
3541
3740
  ...resourceClients !== void 0 ? { resourceClients } : {}
3542
3741
  }),
3543
3742
  guardsForDefinition: ({ definition }) => workflow.guardsForDefinition({
3544
3743
  client,
3545
- tags,
3744
+ tag,
3546
3745
  workflowResource,
3547
3746
  definition,
3548
3747
  ...resourceClients !== void 0 ? { resourceClients } : {}
3549
3748
  }),
3550
3749
  children: ({ instanceId, task }) => workflow.children({
3551
3750
  client,
3552
- tags,
3751
+ tag,
3553
3752
  workflowResource,
3554
3753
  instanceId,
3555
3754
  ...task !== void 0 ? { task } : {}
3556
3755
  }),
3557
3756
  query: ({ groq, params }) => workflow.query({
3558
3757
  client,
3559
- tags,
3758
+ tag,
3560
3759
  workflowResource,
3561
3760
  groq,
3562
3761
  ...params !== void 0 ? { params } : {}
3563
3762
  }),
3564
3763
  queryInScope: ({ instanceId, groq, params }) => workflow.queryInScope({
3565
3764
  client,
3566
- tags,
3765
+ tag,
3567
3766
  workflowResource,
3568
3767
  instanceId,
3569
3768
  groq,
3570
3769
  ...params !== void 0 ? { params } : {},
3571
3770
  ...clock !== void 0 ? { clock } : {}
3572
3771
  }),
3573
- listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tags, workflowResource, instanceId }),
3772
+ listPendingEffects: ({ instanceId }) => workflow.listPendingEffects({ client, tag, workflowResource, instanceId }),
3574
3773
  findPendingEffects: ({ instanceId, claimed, names }) => workflow.findPendingEffects({
3575
3774
  client,
3576
- tags,
3775
+ tag,
3577
3776
  workflowResource,
3578
3777
  instanceId,
3579
3778
  ...claimed !== void 0 ? { claimed } : {},
@@ -3581,7 +3780,7 @@ function createEngine(args) {
3581
3780
  }),
3582
3781
  drainEffects: ({ instanceId, access }) => drainEffectsInternal({
3583
3782
  client,
3584
- tags,
3783
+ tag,
3585
3784
  workflowResource,
3586
3785
  ...resourceClients !== void 0 ? { resourceClients } : {},
3587
3786
  instanceId,
@@ -3592,13 +3791,43 @@ function createEngine(args) {
3592
3791
  }),
3593
3792
  verifyDeployedDefinitions: () => verifyDeployedDefinitionsInternal({
3594
3793
  client,
3595
- tags,
3794
+ tag,
3596
3795
  effectHandlers,
3597
3796
  missingHandler,
3598
3797
  logger
3599
3798
  })
3600
3799
  };
3601
3800
  }
3801
+ function docIdFor(def, target) {
3802
+ return definitionDocId(target.workflowResource, target.tag, def.name, def.version);
3803
+ }
3804
+ function diffStatus(existing, expected) {
3805
+ return existing === void 0 ? "create" : isDefinitionUnchanged(existing, expected) ? "unchanged" : "update";
3806
+ }
3807
+ function diffEntry(def, existingRaw, target) {
3808
+ const docId = docIdFor(def, target), expected = {
3809
+ ...def,
3810
+ _id: docId,
3811
+ _type: schema.WORKFLOW_DEFINITION_TYPE,
3812
+ tag: target.tag
3813
+ }, status = diffStatus(existingRaw, expected), existing = existingRaw ? stripSystemFields(existingRaw) : void 0;
3814
+ return {
3815
+ name: def.name,
3816
+ version: def.version,
3817
+ status,
3818
+ docId,
3819
+ expected,
3820
+ ...existing !== void 0 ? { existing } : {}
3821
+ };
3822
+ }
3823
+ async function computeDiffEntries(client, defs, target) {
3824
+ const entries = [];
3825
+ for (const def of defs) {
3826
+ const existingRaw = await client.getDocument(docIdFor(def, target)) ?? void 0;
3827
+ entries.push(diffEntry(def, existingRaw, target));
3828
+ }
3829
+ return entries;
3830
+ }
3602
3831
  const HISTORY_DISPLAY = {
3603
3832
  stageEntered: {
3604
3833
  title: "Stage entered",
@@ -3791,15 +4020,22 @@ exports.OP_DISPLAY = OP_DISPLAY;
3791
4020
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
3792
4021
  exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
3793
4022
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;
4023
+ exports.abortReason = abortReason;
4024
+ exports.actionVerdict = actionVerdict;
4025
+ exports.assigneesOf = assigneesOf;
4026
+ exports.availableActions = availableActions;
3794
4027
  exports.buildSnapshot = buildSnapshot;
3795
- exports.canonicalTag = canonicalTag;
3796
4028
  exports.compileGuard = compileGuard;
4029
+ exports.computeDiffEntries = computeDiffEntries;
3797
4030
  exports.contentReleaseName = contentReleaseName;
3798
4031
  exports.createEngine = createEngine;
3799
4032
  exports.datasetResourceParts = datasetResourceParts;
3800
4033
  exports.defaultLoggerFactory = defaultLoggerFactory;
3801
4034
  exports.denyingGuards = denyingGuards;
3802
4035
  exports.deployStageGuards = deployStageGuards;
4036
+ exports.diagnoseInputFromEvaluation = diagnoseInputFromEvaluation;
4037
+ exports.diagnoseInstance = diagnoseInstance;
4038
+ exports.diffEntry = diffEntry;
3803
4039
  exports.displayDescription = displayDescription;
3804
4040
  exports.displayTitle = displayTitle;
3805
4041
  exports.effectsContextMap = effectsContextMap;
@@ -3818,6 +4054,7 @@ exports.isDefinitionUnchanged = isDefinitionUnchanged;
3818
4054
  exports.isGdr = isGdr;
3819
4055
  exports.isTerminalStage = isTerminalStage;
3820
4056
  exports.lakeGuardId = lakeGuardId;
4057
+ exports.openStage = openStage;
3821
4058
  exports.parseGdr = parseGdr;
3822
4059
  exports.readsRaw = readsRaw;
3823
4060
  exports.refCanvas = refCanvas;
@@ -3833,7 +4070,7 @@ exports.subscriptionDocument = subscriptionDocument;
3833
4070
  exports.subscriptionDocumentsForInstance = subscriptionDocumentsForInstance;
3834
4071
  exports.tagScopeFilter = tagScopeFilter;
3835
4072
  exports.validateDefinition = validateDefinition;
3836
- exports.validateTags = validateTags;
4073
+ exports.validateTag = validateTag;
3837
4074
  exports.verdictGuardsForInstance = verdictGuardsForInstance;
3838
4075
  exports.wallClock = wallClock;
3839
4076
  exports.workflow = workflow;