@sanity/workflow-engine 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,9 @@
1
1
  import { parse, evaluate } from "groq-js";
2
2
  import { isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
3
+ import { isStartableDefinition } from "./_chunks-es/schema.js";
3
4
  import * as v from "valibot";
4
- function getPath(value, path) {
5
- let current = value;
6
- for (const part of path.split(".")) {
7
- if (current == null || typeof current != "object") return;
8
- current = current[part];
9
- }
10
- return current;
5
+ function findOpenStageEntry(host) {
6
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
11
7
  }
12
8
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
13
9
  function parseGdr(uri) {
@@ -111,12 +107,100 @@ function gdrRef(res, documentId, type) {
111
107
  function isGdr(value) {
112
108
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
113
109
  }
110
+ function buildParams(args) {
111
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
112
+ return {
113
+ self: selfGdr(instance),
114
+ state: renderedState(instance.state ?? [], snapshot),
115
+ parent: instance.ancestors.at(-1)?.id ?? null,
116
+ ancestors: instance.ancestors.map((a) => a.id),
117
+ stage: instance.currentStage,
118
+ now,
119
+ /** `$effects` — parent context handoff by entry name, completed-effect
120
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
121
+ effects: effectsContextMap(instance),
122
+ /** `$tasks` — the current stage's task rows, statuses included. */
123
+ tasks: currentTasks2,
124
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
125
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
126
+ ...extra
127
+ };
128
+ }
129
+ function renderedState(entries, snapshot) {
130
+ const out = {};
131
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
132
+ return out;
133
+ }
134
+ function renderedValue(entry, snapshot) {
135
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
136
+ }
137
+ function scopedStateOverlay(instance, snapshot, taskName) {
138
+ const stageEntry = findOpenStageEntry(instance);
139
+ if (stageEntry === void 0) return {};
140
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
141
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
142
+ }
143
+ function assignedFor(instance, taskName, actor) {
144
+ if (actor === void 0) return !1;
145
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
146
+ return entry === void 0 ? !1 : entry.value.some(
147
+ (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
148
+ );
149
+ }
150
+ function effectsContextMap(instance) {
151
+ const out = {};
152
+ for (const entry of instance.effectsContext)
153
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
154
+ return out;
155
+ }
156
+ function parseJsonContextEntry(entry) {
157
+ try {
158
+ return JSON.parse(entry.value);
159
+ } catch (err) {
160
+ throw new Error(
161
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
162
+ { cause: err }
163
+ );
164
+ }
165
+ }
166
+ function paramsForLake(params) {
167
+ return {
168
+ ...params,
169
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
170
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
171
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
172
+ state: stripStateForLake(params.state)
173
+ };
174
+ }
175
+ function stripStateForLake(value) {
176
+ if (value == null) return value;
177
+ if (typeof value == "string") return bareId(value);
178
+ if (Array.isArray(value)) return value.map(stripStateForLake);
179
+ if (typeof value == "object") {
180
+ const out = {};
181
+ for (const [k, v2] of Object.entries(value))
182
+ out[k] = stripStateForLake(v2);
183
+ return out;
184
+ }
185
+ return value;
186
+ }
187
+ function bareId(id) {
188
+ return isGdrUri(id) ? extractDocumentId(id) : id;
189
+ }
190
+ function getPath(value, path) {
191
+ let current = value;
192
+ for (const part of path.split(".")) {
193
+ if (current == null || typeof current != "object") return;
194
+ current = current[part];
195
+ }
196
+ return current;
197
+ }
114
198
  function resourceOf(p) {
115
199
  return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
116
200
  }
117
- const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
201
+ const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
118
202
  function isGuardReadExpr(expr) {
119
- return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
203
+ return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
120
204
  }
121
205
  function resolveGuardRead(expr, ctx) {
122
206
  if (expr === "$self") return selfGdr(ctx.instance);
@@ -126,8 +210,13 @@ function resolveGuardRead(expr, ctx) {
126
210
  const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
127
211
  return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
128
212
  }
213
+ const effectsRead = EFFECTS_READ.exec(expr);
214
+ if (effectsRead !== null) {
215
+ const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
216
+ return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
217
+ }
129
218
  throw new Error(
130
- `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
219
+ `Guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
131
220
  );
132
221
  }
133
222
  function resolveIdRefTarget(expr, ctx) {
@@ -209,7 +298,7 @@ function createDefinitionValidator() {
209
298
  entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
210
299
  }, checkGuardRead: (expr, where) => {
211
300
  isGuardReadExpr(expr) || errors.push(
212
- ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", or "$state.<name>[.path]" (guards store resolved values; the lake cannot see $state)`
301
+ ` \xB7 ${where}: guard read "${expr}" is not a supported deploy-time value \u2014 use "$self", "$now", "$state.<name>[.path]", or "$effects['<name>'][.path]" (guards store resolved values; the lake cannot see $state or $effects)`
213
302
  );
214
303
  } };
215
304
  }
@@ -388,89 +477,6 @@ async function assertInstanceWriteAllowed(args) {
388
477
  guards: denied
389
478
  });
390
479
  }
391
- function findOpenStageEntry(host) {
392
- return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
393
- }
394
- function buildParams(args) {
395
- const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
396
- return {
397
- self: selfGdr(instance),
398
- state: renderedState(instance.state ?? [], snapshot),
399
- parent: instance.ancestors.at(-1)?.id ?? null,
400
- ancestors: instance.ancestors.map((a) => a.id),
401
- stage: instance.currentStage,
402
- now,
403
- /** `$effects` — parent context handoff by entry name, completed-effect
404
- * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
405
- effects: effectsContextMap(instance),
406
- /** `$tasks` — the current stage's task rows, statuses included. */
407
- tasks: currentTasks2,
408
- allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
409
- anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
410
- ...extra
411
- };
412
- }
413
- function renderedState(entries, snapshot) {
414
- const out = {};
415
- for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
416
- return out;
417
- }
418
- function renderedValue(entry, snapshot) {
419
- return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
420
- }
421
- function scopedStateOverlay(instance, snapshot, taskName) {
422
- const stageEntry = findOpenStageEntry(instance);
423
- if (stageEntry === void 0) return {};
424
- const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
425
- return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
426
- }
427
- function assignedFor(instance, taskName, actor) {
428
- if (actor === void 0) return !1;
429
- const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
430
- return entry === void 0 ? !1 : entry.value.some(
431
- (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
432
- );
433
- }
434
- function effectsContextMap(instance) {
435
- const out = {};
436
- for (const entry of instance.effectsContext)
437
- out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
438
- return out;
439
- }
440
- function parseJsonContextEntry(entry) {
441
- try {
442
- return JSON.parse(entry.value);
443
- } catch (err) {
444
- throw new Error(
445
- `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
446
- { cause: err }
447
- );
448
- }
449
- }
450
- function paramsForLake(params) {
451
- return {
452
- ...params,
453
- self: typeof params.self == "string" ? bareId(params.self) : params.self,
454
- parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
455
- ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
456
- state: stripStateForLake(params.state)
457
- };
458
- }
459
- function stripStateForLake(value) {
460
- if (value == null) return value;
461
- if (typeof value == "string") return bareId(value);
462
- if (Array.isArray(value)) return value.map(stripStateForLake);
463
- if (typeof value == "object") {
464
- const out = {};
465
- for (const [k, v2] of Object.entries(value))
466
- out[k] = stripStateForLake(v2);
467
- return out;
468
- }
469
- return value;
470
- }
471
- function bareId(id) {
472
- return isGdrUri(id) ? extractDocumentId(id) : id;
473
- }
474
480
  function abortReason(instance) {
475
481
  return instance.history.find(
476
482
  (h) => h._type === "aborted"
@@ -593,6 +599,79 @@ function remediationsFor(diagnosis) {
593
599
  available: RUNNABLE_VERBS.has(seed.verb)
594
600
  }));
595
601
  }
602
+ class ActionParamsInvalidError extends Error {
603
+ action;
604
+ task;
605
+ issues;
606
+ constructor(args) {
607
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
608
+ `);
609
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
610
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
611
+ }
612
+ }
613
+ class RequiredStateNotProvidedError extends Error {
614
+ definition;
615
+ missing;
616
+ constructor(args) {
617
+ const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
618
+ `), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
619
+ super(
620
+ `Required init state${where} was not provided:
621
+ ${lines}
622
+ Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
623
+ ), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
624
+ }
625
+ }
626
+ class WorkflowStateDivergedError extends Error {
627
+ instanceId;
628
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
629
+ guardError;
630
+ /** The rollback failure, when rollback was attempted and itself failed. */
631
+ rollbackError;
632
+ constructor(args) {
633
+ super(
634
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
635
+ { cause: args.guardError }
636
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
637
+ }
638
+ }
639
+ class PartialGuardDeployError extends Error {
640
+ stageName;
641
+ deployed;
642
+ constructor(args) {
643
+ super(
644
+ `Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
645
+ { cause: args.cause }
646
+ ), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
647
+ }
648
+ }
649
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
650
+ class ConcurrentFireActionError extends Error {
651
+ instanceId;
652
+ task;
653
+ action;
654
+ attempts;
655
+ constructor(args) {
656
+ super(
657
+ `Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
658
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
659
+ }
660
+ }
661
+ function isRevisionConflict(error) {
662
+ if (typeof error != "object" || error === null) return !1;
663
+ const { statusCode, message } = error;
664
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
665
+ }
666
+ class CascadeLimitError extends Error {
667
+ instanceId;
668
+ limit;
669
+ constructor(args) {
670
+ super(
671
+ `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
672
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
673
+ }
674
+ }
596
675
  function buildSnapshot(args) {
597
676
  const docs = [], knownIds = /* @__PURE__ */ new Set();
598
677
  for (const { doc, resource } of args.docs) {
@@ -858,9 +937,16 @@ async function committedInstance(args) {
858
937
  }
859
938
  async function deployStageGuards(args) {
860
939
  const live = await committedInstance(args);
861
- if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
862
- for (const { client, doc } of resolvedStageGuards(args))
940
+ if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
941
+ let deployed = 0;
942
+ for (const { client, doc } of resolvedStageGuards(args)) {
943
+ try {
863
944
  await upsertGuard(client, doc);
945
+ } catch (cause) {
946
+ throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
947
+ }
948
+ deployed += 1;
949
+ }
864
950
  }
865
951
  async function retractStageGuards(args) {
866
952
  const live = await committedInstance(args);
@@ -1049,6 +1135,7 @@ function derivePerspectiveFromState(entries) {
1049
1135
  async function resolveDeclaredState(args) {
1050
1136
  const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
1051
1137
  if (entryDefs === void 0 || entryDefs.length === 0) return [];
1138
+ assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
1052
1139
  const out = [];
1053
1140
  for (const entry of entryDefs) {
1054
1141
  const scopedCtx = { ...ctx, resolvedState: out };
@@ -1056,6 +1143,17 @@ async function resolveDeclaredState(args) {
1056
1143
  }
1057
1144
  return out;
1058
1145
  }
1146
+ function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
1147
+ const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
1148
+ const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
1149
+ return match === void 0 || match.value === void 0 || match.value === null;
1150
+ }).map((entry) => ({ name: entry.name, type: entry.type }));
1151
+ if (missing.length !== 0)
1152
+ throw new RequiredStateNotProvidedError({
1153
+ ...definitionName !== void 0 ? { definition: definitionName } : {},
1154
+ missing
1155
+ });
1156
+ }
1059
1157
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
1060
1158
  "doc.refs",
1061
1159
  "checklist",
@@ -1280,56 +1378,6 @@ async function resolveTaskStateEntries(args) {
1280
1378
  randomKey
1281
1379
  });
1282
1380
  }
1283
- class ActionParamsInvalidError extends Error {
1284
- action;
1285
- task;
1286
- issues;
1287
- constructor(args) {
1288
- const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1289
- `);
1290
- super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1291
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1292
- }
1293
- }
1294
- class WorkflowStateDivergedError extends Error {
1295
- instanceId;
1296
- /** The guard-deploy failure that triggered the (attempted) rollback. */
1297
- guardError;
1298
- /** The rollback failure, when rollback was attempted and itself failed. */
1299
- rollbackError;
1300
- constructor(args) {
1301
- super(
1302
- `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1303
- { cause: args.guardError }
1304
- ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1305
- }
1306
- }
1307
- const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1308
- class ConcurrentFireActionError extends Error {
1309
- instanceId;
1310
- task;
1311
- action;
1312
- attempts;
1313
- constructor(args) {
1314
- super(
1315
- `Action "${args.action}" on task "${args.task}" (instance ${args.instanceId}) lost the optimistic-locking race ${args.attempts} attempts running \u2014 a concurrent writer kept committing first. Re-read and retry, or investigate a write storm on this instance.`
1316
- ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1317
- }
1318
- }
1319
- function isRevisionConflict(error) {
1320
- if (typeof error != "object" || error === null) return !1;
1321
- const { statusCode, message } = error;
1322
- return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1323
- }
1324
- class CascadeLimitError extends Error {
1325
- instanceId;
1326
- limit;
1327
- constructor(args) {
1328
- super(
1329
- `Cascade did not stabilise after ${args.limit} auto-transitions on ${args.instanceId} \u2014 likely a runaway loop (transition guards that stay mutually satisfied). Check that each transition's filter eventually goes false.`
1330
- ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1331
- }
1332
- }
1333
1381
  async function deployOrRollback(args) {
1334
1382
  try {
1335
1383
  await args.deploy();
@@ -1351,7 +1399,11 @@ async function deployOrRollback(args) {
1351
1399
  reason: "rollback of the committed move failed"
1352
1400
  });
1353
1401
  }
1354
- throw guardError;
1402
+ throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
1403
+ instanceId: args.instanceId,
1404
+ guardError,
1405
+ reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
1406
+ }) : guardError;
1355
1407
  }
1356
1408
  }
1357
1409
  function applyTaskStatusChange(args) {
@@ -1440,7 +1492,8 @@ function materializeInstance(base, mutation) {
1440
1492
  ...base,
1441
1493
  currentStage: mutation.currentStage,
1442
1494
  state: mutation.state,
1443
- stages: mutation.stages
1495
+ stages: mutation.stages,
1496
+ effectsContext: mutation.effectsContext
1444
1497
  };
1445
1498
  }
1446
1499
  async function persist(ctx, mutation) {
@@ -1455,7 +1508,21 @@ async function persist(ctx, mutation) {
1455
1508
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1456
1509
  const actorForPriming = void 0;
1457
1510
  for (const body of pendingCreates)
1458
- await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1511
+ try {
1512
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
1513
+ ctx.client,
1514
+ body._id,
1515
+ actorForPriming,
1516
+ ctx.clientForGdr,
1517
+ ctx.clock
1518
+ ), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1519
+ } catch (cause) {
1520
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
1521
+ instanceId: ctx.instance._id,
1522
+ guardError: cause,
1523
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
1524
+ });
1525
+ }
1459
1526
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1460
1527
  if (!reloaded)
1461
1528
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1720,6 +1787,7 @@ async function prepareChildInstance(args) {
1720
1787
  selfId: childDocId,
1721
1788
  tag: childTag,
1722
1789
  workflowResource,
1790
+ definitionName: definition.name,
1723
1791
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1724
1792
  },
1725
1793
  randomKey
@@ -1766,6 +1834,28 @@ async function resolveBindings(args) {
1766
1834
  resolved[key] = await runGroq(groq, args.params, args.snapshot);
1767
1835
  return { ...resolved, ...args.staticInput };
1768
1836
  }
1837
+ async function persistThenDeploy(ctx, mutation, deploy) {
1838
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1839
+ await deployOrRollback({
1840
+ client: ctx.client,
1841
+ instanceId: ctx.instance._id,
1842
+ committedRev: committed._rev,
1843
+ restore: instanceStateFields(ctx.instance),
1844
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1845
+ reversible: !spawned,
1846
+ deploy
1847
+ });
1848
+ }
1849
+ async function refreshStageGuards(ctx, mutation, stageName) {
1850
+ await deployStageGuards({
1851
+ client: ctx.client,
1852
+ clientForGdr: ctx.clientForGdr,
1853
+ instance: materializeInstance(ctx.instance, mutation),
1854
+ definition: ctx.definition,
1855
+ stageName,
1856
+ now: ctx.now
1857
+ });
1858
+ }
1769
1859
  async function completeEffect(args) {
1770
1860
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1771
1861
  ...options?.clock ? { clock: options.clock } : {},
@@ -1811,9 +1901,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1811
1901
  const mutation = startMutation(ctx.instance);
1812
1902
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1813
1903
  const ranAt = ctx.now;
1814
- return mutation.effectHistory.push(
1904
+ mutation.effectHistory.push(
1815
1905
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1816
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1906
+ );
1907
+ const wroteEffectsContext = status === "done" && outputs !== void 0;
1908
+ return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1817
1909
  _key: randomKey(),
1818
1910
  _type: "effectCompleted",
1819
1911
  at: ranAt,
@@ -1823,7 +1915,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1823
1915
  ...outputs !== void 0 ? { outputs } : {},
1824
1916
  ...detail !== void 0 ? { detail } : {},
1825
1917
  ...actor !== void 0 ? { actor } : {}
1826
- }), await persist(ctx, mutation), { effectKey, status };
1918
+ }), await persistThenDeploy(
1919
+ ctx,
1920
+ mutation,
1921
+ () => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
1922
+ ), { effectKey, status };
1827
1923
  }
1828
1924
  function buildQueuedEffect(effect, origin, params, actor, now) {
1829
1925
  const key = randomKey(), pending = {
@@ -2283,28 +2379,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
2283
2379
  now: ctx.now
2284
2380
  });
2285
2381
  }
2286
- async function persistThenDeploy(ctx, mutation, deploy) {
2287
- const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
2288
- await deployOrRollback({
2289
- client: ctx.client,
2290
- instanceId: ctx.instance._id,
2291
- committedRev: committed._rev,
2292
- restore: instanceStateFields(ctx.instance),
2293
- unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
2294
- reversible: !spawned,
2295
- deploy
2296
- });
2297
- }
2298
- async function refreshStageGuards(ctx, mutation, stageName) {
2299
- await deployStageGuards({
2300
- client: ctx.client,
2301
- clientForGdr: ctx.clientForGdr,
2302
- instance: materializeInstance(ctx.instance, mutation),
2303
- definition: ctx.definition,
2304
- stageName,
2305
- now: ctx.now
2306
- });
2307
- }
2308
2382
  async function commitTransition(ctx, actor) {
2309
2383
  if (isTerminal(ctx))
2310
2384
  return { fired: !1 };
@@ -3261,6 +3335,7 @@ const workflow = {
3261
3335
  selfId: id,
3262
3336
  tag,
3263
3337
  workflowResource,
3338
+ definitionName: definition.name,
3264
3339
  ...perspective !== void 0 ? { perspective } : {}
3265
3340
  },
3266
3341
  randomKey
@@ -4151,6 +4226,8 @@ export {
4151
4226
  HISTORY_DISPLAY,
4152
4227
  MutationGuardDeniedError,
4153
4228
  OP_DISPLAY,
4229
+ PartialGuardDeployError,
4230
+ RequiredStateNotProvidedError,
4154
4231
  STATE_SLOT_DISPLAY,
4155
4232
  WORKFLOW_DEFINITION_TYPE,
4156
4233
  WORKFLOW_INSTANCE_TYPE,
@@ -4186,6 +4263,7 @@ export {
4186
4263
  instanceGuardQuery,
4187
4264
  isDefinitionUnchanged,
4188
4265
  isGdr,
4266
+ isStartableDefinition,
4189
4267
  isTerminalStage,
4190
4268
  lakeGuardId,
4191
4269
  parseGdr,