@sanity/workflow-engine 0.7.0 → 0.9.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
@@ -17,13 +17,8 @@ function _interopNamespaceCompat(e) {
17
17
  }), n.default = e, Object.freeze(n);
18
18
  }
19
19
  var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
20
- function getPath(value, path) {
21
- let current = value;
22
- for (const part of path.split(".")) {
23
- if (current == null || typeof current != "object") return;
24
- current = current[part];
25
- }
26
- return current;
20
+ function findOpenStageEntry(host) {
21
+ return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
27
22
  }
28
23
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set(["dataset", "canvas", "media-library", "dashboard"]);
29
24
  function parseGdr(uri) {
@@ -127,12 +122,100 @@ function gdrRef(res, documentId, type) {
127
122
  function isGdr(value) {
128
123
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
129
124
  }
125
+ function buildParams(args) {
126
+ const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
127
+ return {
128
+ self: selfGdr(instance),
129
+ state: renderedState(instance.state ?? [], snapshot),
130
+ parent: instance.ancestors.at(-1)?.id ?? null,
131
+ ancestors: instance.ancestors.map((a) => a.id),
132
+ stage: instance.currentStage,
133
+ now,
134
+ /** `$effects` — parent context handoff by entry name, completed-effect
135
+ * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
136
+ effects: effectsContextMap(instance),
137
+ /** `$tasks` — the current stage's task rows, statuses included. */
138
+ tasks: currentTasks2,
139
+ allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
140
+ anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
141
+ ...extra
142
+ };
143
+ }
144
+ function renderedState(entries, snapshot) {
145
+ const out = {};
146
+ for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
147
+ return out;
148
+ }
149
+ function renderedValue(entry, snapshot) {
150
+ return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
151
+ }
152
+ function scopedStateOverlay(instance, snapshot, taskName) {
153
+ const stageEntry = findOpenStageEntry(instance);
154
+ if (stageEntry === void 0) return {};
155
+ const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
156
+ return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
157
+ }
158
+ function assignedFor(instance, taskName, actor, roleAliases) {
159
+ if (actor === void 0) return !1;
160
+ const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
161
+ return entry === void 0 ? !1 : entry.value.some(
162
+ (a) => a.type === "user" ? a.id === actor.id : schema.actorFulfillsRole(actor.roles, a.role, roleAliases)
163
+ );
164
+ }
165
+ function effectsContextMap(instance) {
166
+ const out = {};
167
+ for (const entry of instance.effectsContext)
168
+ out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
169
+ return out;
170
+ }
171
+ function parseJsonContextEntry(entry) {
172
+ try {
173
+ return JSON.parse(entry.value);
174
+ } catch (err) {
175
+ throw new Error(
176
+ `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
177
+ { cause: err }
178
+ );
179
+ }
180
+ }
181
+ function paramsForLake(params) {
182
+ return {
183
+ ...params,
184
+ self: typeof params.self == "string" ? bareId(params.self) : params.self,
185
+ parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
186
+ ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
187
+ state: stripStateForLake(params.state)
188
+ };
189
+ }
190
+ function stripStateForLake(value) {
191
+ if (value == null) return value;
192
+ if (typeof value == "string") return bareId(value);
193
+ if (Array.isArray(value)) return value.map(stripStateForLake);
194
+ if (typeof value == "object") {
195
+ const out = {};
196
+ for (const [k, v2] of Object.entries(value))
197
+ out[k] = stripStateForLake(v2);
198
+ return out;
199
+ }
200
+ return value;
201
+ }
202
+ function bareId(id) {
203
+ return isGdrUri(id) ? extractDocumentId(id) : id;
204
+ }
205
+ function getPath(value, path) {
206
+ let current = value;
207
+ for (const part of path.split(".")) {
208
+ if (current == null || typeof current != "object") return;
209
+ current = current[part];
210
+ }
211
+ return current;
212
+ }
130
213
  function resourceOf(p) {
131
214
  return p.scheme === "dataset" ? { type: "dataset", id: `${p.projectId}.${p.dataset}` } : { type: p.scheme, id: p.resourceId ?? "" };
132
215
  }
133
- const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/;
216
+ const STATE_READ = /^\$state\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
134
217
  function isGuardReadExpr(expr) {
135
- return expr === "$self" || expr === "$now" || STATE_READ.test(expr);
218
+ return expr === "$self" || expr === "$now" || STATE_READ.test(expr) || EFFECTS_READ.test(expr);
136
219
  }
137
220
  function resolveGuardRead(expr, ctx) {
138
221
  if (expr === "$self") return selfGdr(ctx.instance);
@@ -142,8 +225,13 @@ function resolveGuardRead(expr, ctx) {
142
225
  const value = (ctx.instance.state ?? []).find((s) => s.name === stateRead[1])?.value;
143
226
  return stateRead[2] !== void 0 ? getPath(value, stateRead[2]) : value;
144
227
  }
228
+ const effectsRead = EFFECTS_READ.exec(expr);
229
+ if (effectsRead !== null) {
230
+ const outputs = effectsContextMap(ctx.instance)[effectsRead[1]];
231
+ return effectsRead[2] !== void 0 ? getPath(outputs, effectsRead[2]) : outputs;
232
+ }
145
233
  throw new Error(
146
- `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)`
234
+ `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)`
147
235
  );
148
236
  }
149
237
  function resolveIdRefTarget(expr, ctx) {
@@ -225,7 +313,7 @@ function createDefinitionValidator() {
225
313
  entry.source.type === "query" && tryParse(entry.source.query, `${label}.query`);
226
314
  }, checkGuardRead: (expr, where) => {
227
315
  isGuardReadExpr(expr) || errors.push(
228
- ` \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)`
316
+ ` \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)`
229
317
  );
230
318
  } };
231
319
  }
@@ -404,89 +492,6 @@ async function assertInstanceWriteAllowed(args) {
404
492
  guards: denied
405
493
  });
406
494
  }
407
- function findOpenStageEntry(host) {
408
- return host.stages.find((s) => s.name === host.currentStage && s.exitedAt === void 0);
409
- }
410
- function buildParams(args) {
411
- const { instance, now, snapshot, extra } = args, currentTasks2 = findOpenStageEntry(instance)?.tasks ?? [];
412
- return {
413
- self: selfGdr(instance),
414
- state: renderedState(instance.state ?? [], snapshot),
415
- parent: instance.ancestors.at(-1)?.id ?? null,
416
- ancestors: instance.ancestors.map((a) => a.id),
417
- stage: instance.currentStage,
418
- now,
419
- /** `$effects` — parent context handoff by entry name, completed-effect
420
- * outputs namespaced under the effect's name (`$effects['x.y'].out`). */
421
- effects: effectsContextMap(instance),
422
- /** `$tasks` — the current stage's task rows, statuses included. */
423
- tasks: currentTasks2,
424
- allTasksDone: currentTasks2.every((t) => t.status === "done" || t.status === "skipped"),
425
- anyTaskFailed: currentTasks2.some((t) => t.status === "failed"),
426
- ...extra
427
- };
428
- }
429
- function renderedState(entries, snapshot) {
430
- const out = {};
431
- for (const entry of entries) out[entry.name] = renderedValue(entry, snapshot);
432
- return out;
433
- }
434
- function renderedValue(entry, snapshot) {
435
- return entry._type !== "doc.ref" || entry.value === null || snapshot === void 0 ? entry.value : snapshot.docs.find((d) => d._id === entry.value.id) ?? entry.value;
436
- }
437
- function scopedStateOverlay(instance, snapshot, taskName) {
438
- const stageEntry = findOpenStageEntry(instance);
439
- if (stageEntry === void 0) return {};
440
- const stageState = renderedState(stageEntry.state ?? [], snapshot), task = taskName ? stageEntry.tasks.find((t) => t.name === taskName) : void 0;
441
- return { ...stageState, ...renderedState(task?.state ?? [], snapshot) };
442
- }
443
- function assignedFor(instance, taskName, actor) {
444
- if (actor === void 0) return !1;
445
- const entry = findOpenStageEntry(instance)?.tasks.find((t) => t.name === taskName)?.state?.find((s) => s._type === "assignees");
446
- return entry === void 0 ? !1 : entry.value.some(
447
- (a) => a.type === "user" ? a.id === actor.id : (actor.roles ?? []).includes(a.role)
448
- );
449
- }
450
- function effectsContextMap(instance) {
451
- const out = {};
452
- for (const entry of instance.effectsContext)
453
- out[entry.name] = entry._type === "effectsContext.json" ? parseJsonContextEntry(entry) : entry.value;
454
- return out;
455
- }
456
- function parseJsonContextEntry(entry) {
457
- try {
458
- return JSON.parse(entry.value);
459
- } catch (err) {
460
- throw new Error(
461
- `effectsContext entry "${entry.name}" holds unparseable JSON: ${err instanceof Error ? err.message : String(err)}`,
462
- { cause: err }
463
- );
464
- }
465
- }
466
- function paramsForLake(params) {
467
- return {
468
- ...params,
469
- self: typeof params.self == "string" ? bareId(params.self) : params.self,
470
- parent: typeof params.parent == "string" ? bareId(params.parent) : params.parent,
471
- ancestors: Array.isArray(params.ancestors) ? params.ancestors.map((a) => typeof a == "string" ? bareId(a) : a) : params.ancestors,
472
- state: stripStateForLake(params.state)
473
- };
474
- }
475
- function stripStateForLake(value) {
476
- if (value == null) return value;
477
- if (typeof value == "string") return bareId(value);
478
- if (Array.isArray(value)) return value.map(stripStateForLake);
479
- if (typeof value == "object") {
480
- const out = {};
481
- for (const [k, v2] of Object.entries(value))
482
- out[k] = stripStateForLake(v2);
483
- return out;
484
- }
485
- return value;
486
- }
487
- function bareId(id) {
488
- return isGdrUri(id) ? extractDocumentId(id) : id;
489
- }
490
495
  function abortReason(instance) {
491
496
  return instance.history.find(
492
497
  (h) => h._type === "aborted"
@@ -609,6 +614,90 @@ function remediationsFor(diagnosis) {
609
614
  available: RUNNABLE_VERBS.has(seed.verb)
610
615
  }));
611
616
  }
617
+ class ActionParamsInvalidError extends Error {
618
+ action;
619
+ task;
620
+ issues;
621
+ constructor(args) {
622
+ const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
623
+ `);
624
+ super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
625
+ ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
626
+ }
627
+ }
628
+ class RequiredStateNotProvidedError extends Error {
629
+ definition;
630
+ missing;
631
+ constructor(args) {
632
+ const lines = args.missing.map((m) => ` - ${m.name} (${m.type})`).join(`
633
+ `), where = args.definition !== void 0 ? ` for workflow "${args.definition}"` : "";
634
+ super(
635
+ `Required init state${where} was not provided:
636
+ ${lines}
637
+ Provide each via \`initialState\` when starting standalone, or via the parent's \`subworkflows.with\` when spawned.`
638
+ ), this.name = "RequiredStateNotProvidedError", args.definition !== void 0 && (this.definition = args.definition), this.missing = args.missing;
639
+ }
640
+ }
641
+ class WorkflowStateDivergedError extends Error {
642
+ instanceId;
643
+ /** The guard-deploy failure that triggered the (attempted) rollback. */
644
+ guardError;
645
+ /** The rollback failure, when rollback was attempted and itself failed. */
646
+ rollbackError;
647
+ constructor(args) {
648
+ super(
649
+ `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
650
+ { cause: args.guardError }
651
+ ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
652
+ }
653
+ }
654
+ class PartialGuardDeployError extends Error {
655
+ stageName;
656
+ deployed;
657
+ constructor(args) {
658
+ super(
659
+ `Partial guard deploy on stage "${args.stageName}": ${args.deployed} guard(s) deployed before a later one failed.`,
660
+ { cause: args.cause }
661
+ ), this.name = "PartialGuardDeployError", this.stageName = args.stageName, this.deployed = args.deployed;
662
+ }
663
+ }
664
+ const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
665
+ class ConcurrentFireActionError extends Error {
666
+ instanceId;
667
+ task;
668
+ action;
669
+ attempts;
670
+ constructor(args) {
671
+ super(
672
+ `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.`
673
+ ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
674
+ }
675
+ }
676
+ function isRevisionConflict(error) {
677
+ if (typeof error != "object" || error === null) return !1;
678
+ const { statusCode, message } = error;
679
+ return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
680
+ }
681
+ class ConcurrentEditStateError extends Error {
682
+ instanceId;
683
+ target;
684
+ attempts;
685
+ constructor(args) {
686
+ const where = args.target.task !== void 0 ? `${args.target.task}.${args.target.state}` : args.target.state;
687
+ super(
688
+ `Edit of "${args.target.scope}:${where}" (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.`
689
+ ), this.name = "ConcurrentEditStateError", this.instanceId = args.instanceId, this.target = args.target, this.attempts = args.attempts;
690
+ }
691
+ }
692
+ class CascadeLimitError extends Error {
693
+ instanceId;
694
+ limit;
695
+ constructor(args) {
696
+ super(
697
+ `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.`
698
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
699
+ }
700
+ }
612
701
  function buildSnapshot(args) {
613
702
  const docs = [], knownIds = /* @__PURE__ */ new Set();
614
703
  for (const { doc, resource } of args.docs) {
@@ -874,9 +963,16 @@ async function committedInstance(args) {
874
963
  }
875
964
  async function deployStageGuards(args) {
876
965
  const live = await committedInstance(args);
877
- if (!(live === void 0 || live.currentStage !== args.stageName) && live.abortedAt === void 0)
878
- for (const { client, doc } of resolvedStageGuards(args))
966
+ if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
967
+ let deployed = 0;
968
+ for (const { client, doc } of resolvedStageGuards(args)) {
969
+ try {
879
970
  await upsertGuard(client, doc);
971
+ } catch (cause) {
972
+ throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
973
+ }
974
+ deployed += 1;
975
+ }
880
976
  }
881
977
  async function retractStageGuards(args) {
882
978
  const live = await committedInstance(args);
@@ -1065,6 +1161,7 @@ function derivePerspectiveFromState(entries) {
1065
1161
  async function resolveDeclaredState(args) {
1066
1162
  const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
1067
1163
  if (entryDefs === void 0 || entryDefs.length === 0) return [];
1164
+ assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
1068
1165
  const out = [];
1069
1166
  for (const entry of entryDefs) {
1070
1167
  const scopedCtx = { ...ctx, resolvedState: out };
@@ -1072,6 +1169,17 @@ async function resolveDeclaredState(args) {
1072
1169
  }
1073
1170
  return out;
1074
1171
  }
1172
+ function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
1173
+ const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
1174
+ const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
1175
+ return match === void 0 || match.value === void 0 || match.value === null;
1176
+ }).map((entry) => ({ name: entry.name, type: entry.type }));
1177
+ if (missing.length !== 0)
1178
+ throw new RequiredStateNotProvidedError({
1179
+ ...definitionName !== void 0 ? { definition: definitionName } : {},
1180
+ missing
1181
+ });
1182
+ }
1075
1183
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
1076
1184
  "doc.refs",
1077
1185
  "checklist",
@@ -1200,6 +1308,12 @@ function coerceToGdr(raw, workflowResource) {
1200
1308
  function gdrToBareId(uri) {
1201
1309
  return uri.includes(":") ? extractDocumentId(uri) : uri;
1202
1310
  }
1311
+ function loadCallContext(client, instanceId, options) {
1312
+ return loadContext(client, instanceId, {
1313
+ ...options?.clock ? { clock: options.clock } : {},
1314
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1315
+ });
1316
+ }
1203
1317
  async function loadContext(client, instanceId, options) {
1204
1318
  const instance = await client.getDocument(instanceId);
1205
1319
  if (!instance)
@@ -1220,7 +1334,7 @@ async function ctxConditionParams(ctx, opts) {
1220
1334
  ...base,
1221
1335
  state,
1222
1336
  actor: opts?.actor,
1223
- assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
1337
+ assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor, ctx.definition.roleAliases) : !1,
1224
1338
  ...opts?.vars
1225
1339
  };
1226
1340
  return { ...await evaluatePredicates({
@@ -1265,86 +1379,36 @@ async function resolveStageStateEntries(args) {
1265
1379
  ctx: {
1266
1380
  client,
1267
1381
  now,
1268
- selfId: instance._id,
1269
- tag: instance.tag,
1270
- stageName: stage.name,
1271
- workflowResource: instance.workflowResource,
1272
- // Allow stage-scope entries' `source: { type: "stateRead", scope:
1273
- // "workflow", ... }` to see the already-resolved workflow-scope state.
1274
- workflowState: instance.state ?? [],
1275
- ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1276
- },
1277
- randomKey
1278
- });
1279
- }
1280
- async function resolveTaskStateEntries(args) {
1281
- const { client, instance, stage, task, now } = args;
1282
- return resolveDeclaredState({
1283
- entryDefs: task.state,
1284
- initialState: [],
1285
- ctx: {
1286
- client,
1287
- now,
1288
- selfId: instance._id,
1289
- tag: instance.tag,
1290
- stageName: stage.name,
1291
- workflowResource: instance.workflowResource,
1292
- taskName: task.name,
1293
- workflowState: instance.state ?? [],
1294
- ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1295
- },
1296
- randomKey
1297
- });
1298
- }
1299
- class ActionParamsInvalidError extends Error {
1300
- action;
1301
- task;
1302
- issues;
1303
- constructor(args) {
1304
- const lines = args.issues.map((i) => ` - ${i.param}: ${i.reason}`).join(`
1305
- `);
1306
- super(`Action "${args.action}" on task "${args.task}" rejected: invalid params
1307
- ${lines}`), this.name = "ActionParamsInvalidError", this.action = args.action, this.task = args.task, this.issues = args.issues;
1308
- }
1309
- }
1310
- class WorkflowStateDivergedError extends Error {
1311
- instanceId;
1312
- /** The guard-deploy failure that triggered the (attempted) rollback. */
1313
- guardError;
1314
- /** The rollback failure, when rollback was attempted and itself failed. */
1315
- rollbackError;
1316
- constructor(args) {
1317
- super(
1318
- `Workflow "${args.instanceId}" state committed but its guards could not be reconciled: ${args.reason}.`,
1319
- { cause: args.guardError }
1320
- ), this.name = "WorkflowStateDivergedError", this.instanceId = args.instanceId, this.guardError = args.guardError, args.rollbackError !== void 0 && (this.rollbackError = args.rollbackError);
1321
- }
1322
- }
1323
- const CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS = 3;
1324
- class ConcurrentFireActionError extends Error {
1325
- instanceId;
1326
- task;
1327
- action;
1328
- attempts;
1329
- constructor(args) {
1330
- super(
1331
- `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.`
1332
- ), this.name = "ConcurrentFireActionError", this.instanceId = args.instanceId, this.task = args.task, this.action = args.action, this.attempts = args.attempts;
1333
- }
1334
- }
1335
- function isRevisionConflict(error) {
1336
- if (typeof error != "object" || error === null) return !1;
1337
- const { statusCode, message } = error;
1338
- return statusCode === 409 ? !0 : typeof message == "string" && message.includes("ifRevisionId check failed");
1382
+ selfId: instance._id,
1383
+ tag: instance.tag,
1384
+ stageName: stage.name,
1385
+ workflowResource: instance.workflowResource,
1386
+ // Allow stage-scope entries' `source: { type: "stateRead", scope:
1387
+ // "workflow", ... }` to see the already-resolved workflow-scope state.
1388
+ workflowState: instance.state ?? [],
1389
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1390
+ },
1391
+ randomKey
1392
+ });
1339
1393
  }
1340
- class CascadeLimitError extends Error {
1341
- instanceId;
1342
- limit;
1343
- constructor(args) {
1344
- super(
1345
- `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.`
1346
- ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
1347
- }
1394
+ async function resolveTaskStateEntries(args) {
1395
+ const { client, instance, stage, task, now } = args;
1396
+ return resolveDeclaredState({
1397
+ entryDefs: task.state,
1398
+ initialState: [],
1399
+ ctx: {
1400
+ client,
1401
+ now,
1402
+ selfId: instance._id,
1403
+ tag: instance.tag,
1404
+ stageName: stage.name,
1405
+ workflowResource: instance.workflowResource,
1406
+ taskName: task.name,
1407
+ workflowState: instance.state ?? [],
1408
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1409
+ },
1410
+ randomKey
1411
+ });
1348
1412
  }
1349
1413
  async function deployOrRollback(args) {
1350
1414
  try {
@@ -1367,7 +1431,11 @@ async function deployOrRollback(args) {
1367
1431
  reason: "rollback of the committed move failed"
1368
1432
  });
1369
1433
  }
1370
- throw guardError;
1434
+ throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
1435
+ instanceId: args.instanceId,
1436
+ guardError,
1437
+ reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
1438
+ }) : guardError;
1371
1439
  }
1372
1440
  }
1373
1441
  function applyTaskStatusChange(args) {
@@ -1456,7 +1524,8 @@ function materializeInstance(base, mutation) {
1456
1524
  ...base,
1457
1525
  currentStage: mutation.currentStage,
1458
1526
  state: mutation.state,
1459
- stages: mutation.stages
1527
+ stages: mutation.stages,
1528
+ effectsContext: mutation.effectsContext
1460
1529
  };
1461
1530
  }
1462
1531
  async function persist(ctx, mutation) {
@@ -1471,7 +1540,21 @@ async function persist(ctx, mutation) {
1471
1540
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1472
1541
  const actorForPriming = void 0;
1473
1542
  for (const body of pendingCreates)
1474
- 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);
1543
+ try {
1544
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
1545
+ ctx.client,
1546
+ body._id,
1547
+ actorForPriming,
1548
+ ctx.clientForGdr,
1549
+ ctx.clock
1550
+ ), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1551
+ } catch (cause) {
1552
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
1553
+ instanceId: ctx.instance._id,
1554
+ guardError: cause,
1555
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
1556
+ });
1557
+ }
1475
1558
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1476
1559
  if (!reloaded)
1477
1560
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1736,6 +1819,7 @@ async function prepareChildInstance(args) {
1736
1819
  selfId: childDocId,
1737
1820
  tag: childTag,
1738
1821
  workflowResource,
1822
+ definitionName: definition.name,
1739
1823
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1740
1824
  },
1741
1825
  randomKey
@@ -1782,6 +1866,28 @@ async function resolveBindings(args) {
1782
1866
  resolved[key] = await runGroq(groq, args.params, args.snapshot);
1783
1867
  return { ...resolved, ...args.staticInput };
1784
1868
  }
1869
+ async function persistThenDeploy(ctx, mutation, deploy) {
1870
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1871
+ await deployOrRollback({
1872
+ client: ctx.client,
1873
+ instanceId: ctx.instance._id,
1874
+ committedRev: committed._rev,
1875
+ restore: instanceStateFields(ctx.instance),
1876
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1877
+ reversible: !spawned,
1878
+ deploy
1879
+ });
1880
+ }
1881
+ async function refreshStageGuards(ctx, mutation, stageName) {
1882
+ await deployStageGuards({
1883
+ client: ctx.client,
1884
+ clientForGdr: ctx.clientForGdr,
1885
+ instance: materializeInstance(ctx.instance, mutation),
1886
+ definition: ctx.definition,
1887
+ stageName,
1888
+ now: ctx.now
1889
+ });
1890
+ }
1785
1891
  async function completeEffect(args) {
1786
1892
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1787
1893
  ...options?.clock ? { clock: options.clock } : {},
@@ -1827,9 +1933,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1827
1933
  const mutation = startMutation(ctx.instance);
1828
1934
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1829
1935
  const ranAt = ctx.now;
1830
- return mutation.effectHistory.push(
1936
+ mutation.effectHistory.push(
1831
1937
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1832
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1938
+ );
1939
+ const wroteEffectsContext = status === "done" && outputs !== void 0;
1940
+ return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1833
1941
  _key: randomKey(),
1834
1942
  _type: "effectCompleted",
1835
1943
  at: ranAt,
@@ -1839,7 +1947,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1839
1947
  ...outputs !== void 0 ? { outputs } : {},
1840
1948
  ...detail !== void 0 ? { detail } : {},
1841
1949
  ...actor !== void 0 ? { actor } : {}
1842
- }), await persist(ctx, mutation), { effectKey, status };
1950
+ }), await persistThenDeploy(
1951
+ ctx,
1952
+ mutation,
1953
+ () => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
1954
+ ), { effectKey, status };
1843
1955
  }
1844
1956
  function buildQueuedEffect(effect, origin, params, actor, now) {
1845
1957
  const key = randomKey(), pending = {
@@ -1953,22 +2065,27 @@ function runOps(args) {
1953
2065
  const summaries = [];
1954
2066
  for (const op of ops) {
1955
2067
  const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
1956
- summaries.push(summary), mutation.history.push({
1957
- _key: randomKey(),
1958
- _type: "opApplied",
1959
- at: now,
1960
- stage,
1961
- ...origin.task !== void 0 ? { task: origin.task } : {},
1962
- ...origin.action !== void 0 ? { action: origin.action } : {},
1963
- ...origin.transition !== void 0 ? { transition: origin.transition } : {},
1964
- opType: summary.opType,
1965
- ...summary.target !== void 0 ? { target: summary.target } : {},
1966
- ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
1967
- ...actor !== void 0 ? { actor } : {}
1968
- });
2068
+ summaries.push(summary), mutation.history.push(opAppliedEntry({ origin, summary, stage, actor, now }));
1969
2069
  }
1970
2070
  return summaries;
1971
2071
  }
2072
+ function opAppliedEntry(args) {
2073
+ const { origin, summary, stage, actor, now } = args;
2074
+ return {
2075
+ _key: randomKey(),
2076
+ _type: "opApplied",
2077
+ at: now,
2078
+ stage,
2079
+ ...origin.task !== void 0 ? { task: origin.task } : {},
2080
+ ...origin.action !== void 0 ? { action: origin.action } : {},
2081
+ ...origin.transition !== void 0 ? { transition: origin.transition } : {},
2082
+ ...origin.edit === !0 ? { edit: !0 } : {},
2083
+ opType: summary.opType,
2084
+ ...summary.target !== void 0 ? { target: summary.target } : {},
2085
+ ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
2086
+ ...actor !== void 0 ? { actor } : {}
2087
+ };
2088
+ }
1972
2089
  function applyOp(op, ctx) {
1973
2090
  switch (op.type) {
1974
2091
  case "state.set":
@@ -2299,28 +2416,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
2299
2416
  now: ctx.now
2300
2417
  });
2301
2418
  }
2302
- async function persistThenDeploy(ctx, mutation, deploy) {
2303
- const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
2304
- await deployOrRollback({
2305
- client: ctx.client,
2306
- instanceId: ctx.instance._id,
2307
- committedRev: committed._rev,
2308
- restore: instanceStateFields(ctx.instance),
2309
- unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
2310
- reversible: !spawned,
2311
- deploy
2312
- });
2313
- }
2314
- async function refreshStageGuards(ctx, mutation, stageName) {
2315
- await deployStageGuards({
2316
- client: ctx.client,
2317
- clientForGdr: ctx.clientForGdr,
2318
- instance: materializeInstance(ctx.instance, mutation),
2319
- definition: ctx.definition,
2320
- stageName,
2321
- now: ctx.now
2322
- });
2323
- }
2324
2419
  async function commitTransition(ctx, actor) {
2325
2420
  if (isTerminal(ctx))
2326
2421
  return { fired: !1 };
@@ -2624,6 +2719,85 @@ async function fetchGrantsCached(requestFn, resourcePath) {
2624
2719
  return;
2625
2720
  }
2626
2721
  }
2722
+ function effectiveEditable(baseline, override) {
2723
+ if (baseline === void 0) return;
2724
+ if (override === void 0) return baseline;
2725
+ if (baseline === !0 && override === !0) return !0;
2726
+ const parts = [baseline, override].map((e) => e === !0 ? void 0 : e);
2727
+ return schema.andConditions(parts) ?? !0;
2728
+ }
2729
+ function slotSites(definition, stage) {
2730
+ const sites = [];
2731
+ for (const entry of definition.state ?? []) sites.push({ scope: "workflow", entry });
2732
+ for (const entry of stage.state ?? []) sites.push({ scope: "stage", entry });
2733
+ for (const task of stage.tasks ?? [])
2734
+ for (const entry of task.state ?? []) sites.push({ scope: "task", task: task.name, entry });
2735
+ return sites;
2736
+ }
2737
+ function toResolvedSlot(site, stage) {
2738
+ return {
2739
+ scope: site.scope,
2740
+ ...site.task !== void 0 ? { task: site.task } : {},
2741
+ name: site.entry.name,
2742
+ type: site.entry.type,
2743
+ ...site.entry.title !== void 0 ? { title: site.entry.title } : {},
2744
+ ref: { scope: site.scope, state: site.entry.name },
2745
+ effective: effectiveEditable(site.entry.editable, stage.editable?.[site.entry.name])
2746
+ };
2747
+ }
2748
+ function editableSlotsInStage(definition, stage) {
2749
+ return slotSites(definition, stage).filter((site) => site.entry.editable !== void 0).map((site) => toResolvedSlot(site, stage));
2750
+ }
2751
+ function editTargetMatches(slot, target) {
2752
+ return slot.name !== target.state ? !1 : target.task !== void 0 ? slot.scope === "task" && slot.task === target.task : target.scope !== void 0 ? slot.scope === target.scope : !0;
2753
+ }
2754
+ const SCOPE_PRECEDENCE = { task: 0, stage: 1, workflow: 2 };
2755
+ function resolveEditTarget(args) {
2756
+ const { definition, stage, target } = args, found = slotSites(definition, stage).filter(
2757
+ (site) => editTargetMatches(
2758
+ {
2759
+ scope: site.scope,
2760
+ name: site.entry.name,
2761
+ ...site.task !== void 0 ? { task: site.task } : {}
2762
+ },
2763
+ target
2764
+ )
2765
+ ).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
2766
+ return found ? toResolvedSlot(found, stage) : void 0;
2767
+ }
2768
+ function slotWindowOpen(instance, slot) {
2769
+ if (instance.completedAt !== void 0) return { open: !1, detail: "instance completed" };
2770
+ if (instance.abortedAt !== void 0) return { open: !1, detail: "instance aborted" };
2771
+ if (slot.scope !== "task") return { open: !0 };
2772
+ const status = findOpenStageEntry(instance)?.tasks.find((t) => t.name === slot.task)?.status;
2773
+ return status === "active" ? { open: !0 } : { open: !1, detail: `task "${slot.task}" is ${status ?? "not active"}` };
2774
+ }
2775
+ function readSlotValue(instance, slot) {
2776
+ return slotStateHost(instance, slot)?.find((e) => e.name === slot.name)?.value;
2777
+ }
2778
+ function slotStateHost(instance, slot) {
2779
+ if (slot.scope === "workflow") return instance.state;
2780
+ const stageEntry = findOpenStageEntry(instance);
2781
+ return slot.scope === "stage" ? stageEntry?.state : stageEntry?.tasks.find((t) => t.name === slot.task)?.state;
2782
+ }
2783
+ function slotProvenance(instance, ref) {
2784
+ let latest;
2785
+ for (const entry of instance.history)
2786
+ entry._type === "opApplied" && (entry.target?.scope !== ref.scope || entry.target.state !== ref.state || (latest = { at: entry.at, ...entry.actor !== void 0 ? { actor: entry.actor } : {} }));
2787
+ return latest === void 0 ? {} : {
2788
+ ...latest.actor !== void 0 ? { setBy: latest.actor } : {},
2789
+ setAt: latest.at
2790
+ };
2791
+ }
2792
+ function editDisabledReason(args) {
2793
+ const { effective, instance, window, guardDenial, predicateSatisfied } = args;
2794
+ if (effective === void 0) return { kind: "not-editable" };
2795
+ if (!window.open)
2796
+ return instance.completedAt !== void 0 ? { kind: "instance-completed", completedAt: instance.completedAt } : { kind: "edit-window-closed", detail: window.detail ?? "closed" };
2797
+ if (guardDenial !== void 0) return guardDenial;
2798
+ if (!predicateSatisfied)
2799
+ return { kind: "editor-not-permitted", predicate: effective === !0 ? "true" : effective };
2800
+ }
2627
2801
  async function evaluateInstance(args) {
2628
2802
  const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2629
2803
  validateTag(tag);
@@ -2657,6 +2831,7 @@ async function evaluateFromSnapshot(args) {
2657
2831
  actor,
2658
2832
  snapshot,
2659
2833
  scope,
2834
+ roleAliases: definition.roleAliases,
2660
2835
  stageHasExits: !isTerminalStage(stage),
2661
2836
  guardDenial
2662
2837
  })
@@ -2678,16 +2853,65 @@ async function evaluateFromSnapshot(args) {
2678
2853
  stage,
2679
2854
  tasks: taskEvaluations,
2680
2855
  transitions: transitionEvaluations
2681
- }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2856
+ }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableSlots = await evaluateEditableSlots({
2857
+ instance,
2858
+ definition,
2859
+ stage,
2860
+ actor,
2861
+ snapshot,
2862
+ scope,
2863
+ guardDenial: editGuardDenial
2864
+ });
2682
2865
  return {
2683
2866
  instance,
2684
2867
  definition,
2685
2868
  actor,
2686
2869
  currentStage,
2687
2870
  pendingOnYou,
2688
- canInteract
2871
+ canInteract,
2872
+ editableSlots
2689
2873
  };
2690
2874
  }
2875
+ async function evaluateEditableSlots(args) {
2876
+ const { instance, definition, stage, actor, snapshot, scope, guardDenial } = args, slots = [];
2877
+ for (const slot of editableSlotsInStage(definition, stage)) {
2878
+ const window = slotWindowOpen(instance, slot), predicateSatisfied = await editPredicateSatisfied({
2879
+ slot,
2880
+ window,
2881
+ guardDenial,
2882
+ instance,
2883
+ snapshot,
2884
+ scope,
2885
+ actor,
2886
+ roleAliases: definition.roleAliases
2887
+ }), reason = editDisabledReason({
2888
+ effective: slot.effective,
2889
+ instance,
2890
+ window,
2891
+ guardDenial,
2892
+ predicateSatisfied
2893
+ }), value = readSlotValue(instance, slot);
2894
+ slots.push({
2895
+ scope: slot.scope,
2896
+ ...slot.task !== void 0 ? { task: slot.task } : {},
2897
+ name: slot.name,
2898
+ type: slot.type,
2899
+ ...slot.title !== void 0 ? { title: slot.title } : {},
2900
+ value,
2901
+ editable: reason === void 0,
2902
+ ...reason !== void 0 ? { disabledReason: reason } : {},
2903
+ ...slotProvenance(instance, slot.ref)
2904
+ });
2905
+ }
2906
+ return slots;
2907
+ }
2908
+ async function editPredicateSatisfied(args) {
2909
+ const { slot, window, guardDenial, instance, snapshot, scope, actor, roleAliases } = args;
2910
+ if (!window.open || guardDenial !== void 0) return !1;
2911
+ if (slot.effective === !0) return !0;
2912
+ const params = slot.scope === "task" && slot.task !== void 0 ? taskScopeFor({ scope, instance, snapshot, actor, taskName: slot.task, roleAliases }) : scope;
2913
+ return evaluateCondition({ condition: slot.effective, snapshot, params });
2914
+ }
2691
2915
  async function renderScope(args) {
2692
2916
  const { instance, definition, actor, grants, snapshot, now } = args, params = {
2693
2917
  ...buildParams({ instance, now, snapshot }),
@@ -2713,15 +2937,36 @@ async function advisoryCan(instance, actor, grants) {
2713
2937
  });
2714
2938
  return can;
2715
2939
  }
2716
- async function evaluateTask(args) {
2717
- const { task, statusEntry, instance, actor, snapshot, scope, stageHasExits, guardDenial } = args, status = statusEntry?.status ?? "pending", assigned = assignedFor(instance, task.name, actor), taskScope = {
2940
+ function taskScopeFor(args) {
2941
+ const { scope, instance, snapshot, actor, taskName, roleAliases } = args;
2942
+ return {
2718
2943
  ...scope,
2719
2944
  state: {
2720
2945
  ...scope.state,
2721
- ...scopedStateOverlay(instance, snapshot, task.name)
2946
+ ...scopedStateOverlay(instance, snapshot, taskName)
2722
2947
  },
2723
- assigned
2724
- }, unmetRequirements = await evaluateRequirements({
2948
+ assigned: assignedFor(instance, taskName, actor, roleAliases)
2949
+ };
2950
+ }
2951
+ async function evaluateTask(args) {
2952
+ const {
2953
+ task,
2954
+ statusEntry,
2955
+ instance,
2956
+ actor,
2957
+ snapshot,
2958
+ scope,
2959
+ roleAliases,
2960
+ stageHasExits,
2961
+ guardDenial
2962
+ } = args, status = statusEntry?.status ?? "pending", taskScope = taskScopeFor({
2963
+ scope,
2964
+ instance,
2965
+ snapshot,
2966
+ actor,
2967
+ taskName: task.name,
2968
+ roleAliases
2969
+ }), assigned = taskScope.assigned === !0, unmetRequirements = await evaluateRequirements({
2725
2970
  requirements: task.requirements,
2726
2971
  snapshot,
2727
2972
  params: taskScope
@@ -2948,10 +3193,7 @@ async function commitAbort(ctx, reason, actor) {
2948
3193
  async function fireAction(args) {
2949
3194
  const { client, instanceId, task, action, params, options } = args;
2950
3195
  for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2951
- const ctx = await loadContext(client, instanceId, {
2952
- ...options?.clock ? { clock: options.clock } : {},
2953
- ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
2954
- });
3196
+ const ctx = await loadCallContext(client, instanceId, options);
2955
3197
  try {
2956
3198
  return await commitAction(ctx, task, action, params, options);
2957
3199
  } catch (error) {
@@ -3046,6 +3288,114 @@ function formatDisabledReason(task, action, reason) {
3046
3288
  const detail = disabledReasonDetail[reason.kind](reason);
3047
3289
  return `Action "${task}:${action}" is not allowed: ${detail}`;
3048
3290
  }
3291
+ class EditStateDeniedError extends Error {
3292
+ reason;
3293
+ target;
3294
+ constructor(args) {
3295
+ super(formatEditDisabledReason(args.target, args.reason)), this.name = "EditStateDeniedError", this.reason = args.reason, this.target = args.target;
3296
+ }
3297
+ }
3298
+ const editDisabledReasonDetail = {
3299
+ "not-editable": () => "slot is not declared editable",
3300
+ "instance-completed": (r) => `instance completed at ${r.completedAt}`,
3301
+ "edit-window-closed": (r) => `edit window closed (${r.detail})`,
3302
+ "editor-not-permitted": (r) => `editor not permitted (${r.predicate})`,
3303
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
3304
+ };
3305
+ function formatEditDisabledReason(target, reason) {
3306
+ const detail = editDisabledReasonDetail[reason.kind](
3307
+ reason
3308
+ ), where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
3309
+ return `State "${target.scope}:${where}" is not editable: ${detail}`;
3310
+ }
3311
+ async function editState(args) {
3312
+ const { client, instanceId, target, mode = "set", value, options } = args;
3313
+ for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
3314
+ const ctx = await loadCallContext(client, instanceId, options);
3315
+ try {
3316
+ return await commitEdit(ctx, target, mode, value, options);
3317
+ } catch (error) {
3318
+ if (!isRevisionConflict(error)) throw error;
3319
+ }
3320
+ }
3321
+ throw new ConcurrentEditStateError({
3322
+ instanceId,
3323
+ target: labelTarget(target),
3324
+ attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
3325
+ });
3326
+ }
3327
+ async function commitEdit(ctx, target, mode, value, options) {
3328
+ const actor = options?.actor, stage = findStage(ctx.definition, ctx.instance.currentStage), slot = resolveEditTarget({ definition: ctx.definition, stage, target });
3329
+ if (slot === void 0)
3330
+ throw new Error(
3331
+ `No state slot "${describeTarget(target)}" resolves in current stage "${stage.name}" of ${ctx.definition.name}`
3332
+ );
3333
+ await assertSlotEditable(ctx, slot, options);
3334
+ const op = buildEditOp(slot, mode, value), mutation = startMutation(ctx.instance), ranOps = runOps({
3335
+ ops: [op],
3336
+ mutation,
3337
+ stage: stage.name,
3338
+ origin: {
3339
+ edit: !0,
3340
+ ...slot.scope === "task" && slot.task !== void 0 ? { task: slot.task } : {}
3341
+ },
3342
+ params: {},
3343
+ actor,
3344
+ self: selfGdr(ctx.instance),
3345
+ now: ctx.now
3346
+ });
3347
+ return await persistThenDeploy(
3348
+ ctx,
3349
+ mutation,
3350
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
3351
+ ), {
3352
+ edited: !0,
3353
+ target: slotTarget(slot),
3354
+ ...ranOps.length > 0 ? { ranOps } : {}
3355
+ };
3356
+ }
3357
+ async function assertSlotEditable(ctx, slot, options) {
3358
+ const actor = options?.actor, window = slotWindowOpen(ctx.instance, slot), can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan(ctx.instance, actor, options.grants) : void 0, predicateSatisfied = window.open && slot.effective !== !0 && slot.effective !== void 0 ? await ctxEvaluateCondition(ctx, slot.effective, {
3359
+ ...slot.task !== void 0 ? { taskName: slot.task } : {},
3360
+ ...actor !== void 0 ? { actor } : {},
3361
+ ...can !== void 0 ? { vars: { can } } : {}
3362
+ }) : slot.effective === !0, reason = editDisabledReason({
3363
+ effective: slot.effective,
3364
+ instance: ctx.instance,
3365
+ window,
3366
+ guardDenial: void 0,
3367
+ predicateSatisfied
3368
+ });
3369
+ if (reason !== void 0) throw new EditStateDeniedError({ target: slotTarget(slot), reason });
3370
+ }
3371
+ function buildEditOp(slot, mode, value) {
3372
+ if (mode === "unset") return { type: "state.unset", target: slot.ref };
3373
+ if (value === void 0)
3374
+ throw new Error(`editState mode "${mode}" requires a value for slot "${slot.name}"`);
3375
+ return {
3376
+ type: mode === "append" ? "state.append" : "state.set",
3377
+ target: slot.ref,
3378
+ value: { type: "literal", value }
3379
+ };
3380
+ }
3381
+ function slotTarget(slot) {
3382
+ return {
3383
+ scope: slot.scope,
3384
+ state: slot.name,
3385
+ ...slot.task !== void 0 ? { task: slot.task } : {}
3386
+ };
3387
+ }
3388
+ function labelTarget(target) {
3389
+ return {
3390
+ scope: target.scope ?? (target.task !== void 0 ? "task" : "workflow"),
3391
+ state: target.state,
3392
+ ...target.task !== void 0 ? { task: target.task } : {}
3393
+ };
3394
+ }
3395
+ function describeTarget(target) {
3396
+ const where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
3397
+ return target.scope !== void 0 ? `${target.scope}:${where}` : where;
3398
+ }
3049
3399
  function bareIdFromSpawnRef(uri) {
3050
3400
  if (!uri.includes(":")) return [uri];
3051
3401
  try {
@@ -3092,18 +3442,60 @@ function buildClientForGdr(defaultClient, resolver) {
3092
3442
  function toEffectsContextEntries(ctx) {
3093
3443
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
3094
3444
  }
3445
+ async function dispatchGatedWrite(args) {
3446
+ const { client, tag, instanceId, resourceClients, access, apply } = args, evaluation = await evaluateInstance({
3447
+ client,
3448
+ tag,
3449
+ instanceId,
3450
+ access,
3451
+ clock: args.clock,
3452
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3453
+ }), ranOps = await apply(args.before, evaluation), cascaded = await cascade(client, instanceId, args.actor, args.clientForGdr, args.clock);
3454
+ return {
3455
+ instance: await reload(client, instanceId, tag),
3456
+ cascaded,
3457
+ fired: !0,
3458
+ ...ranOps !== void 0 ? { ranOps } : {}
3459
+ };
3460
+ }
3095
3461
  function assertActionAllowed(evaluation, task, action) {
3096
3462
  const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
3097
3463
  if (actionEval?.allowed === !1 && actionEval.disabledReason)
3098
3464
  throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
3099
3465
  }
3100
- async function applyAction(args) {
3101
- const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = {
3102
- actor,
3103
- ...grants !== void 0 ? { grants } : {},
3104
- ...clock !== void 0 ? { clock } : {},
3105
- ...clientForGdr !== void 0 ? { clientForGdr } : {}
3466
+ function assertEditAllowed(evaluation, target) {
3467
+ const slot = evaluation.editableSlots.filter((s) => editTargetMatches(s, target)).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
3468
+ if (slot !== void 0 && !slot.editable && slot.disabledReason !== void 0)
3469
+ throw new EditStateDeniedError({
3470
+ target: {
3471
+ scope: slot.scope,
3472
+ state: slot.name,
3473
+ ...slot.task !== void 0 ? { task: slot.task } : {}
3474
+ },
3475
+ reason: slot.disabledReason
3476
+ });
3477
+ }
3478
+ async function applyEdit(args) {
3479
+ const { client, instance, target, mode, value, actor, grants, clock, clientForGdr } = args;
3480
+ return (await editState({
3481
+ client,
3482
+ instanceId: instance._id,
3483
+ target,
3484
+ ...mode !== void 0 ? { mode } : {},
3485
+ ...value !== void 0 ? { value } : {},
3486
+ options: buildEngineCallOptions({ actor, grants, clock, clientForGdr })
3487
+ })).ranOps;
3488
+ }
3489
+ function buildEngineCallOptions(args) {
3490
+ return {
3491
+ actor: args.actor,
3492
+ ...args.grants !== void 0 ? { grants: args.grants } : {},
3493
+ ...args.clock !== void 0 ? { clock: args.clock } : {},
3494
+ ...args.clientForGdr !== void 0 ? { clientForGdr: args.clientForGdr } : {}
3106
3495
  };
3496
+ }
3497
+ async function applyAction(args) {
3498
+ const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = buildEngineCallOptions({ actor, grants, clock, clientForGdr });
3107
3499
  return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
3108
3500
  client,
3109
3501
  instanceId: instance._id,
@@ -3277,6 +3669,7 @@ const workflow = {
3277
3669
  selfId: id,
3278
3670
  tag,
3279
3671
  workflowResource,
3672
+ definitionName: definition.name,
3280
3673
  ...perspective !== void 0 ? { perspective } : {}
3281
3674
  },
3282
3675
  randomKey
@@ -3318,34 +3711,68 @@ const workflow = {
3318
3711
  idempotent,
3319
3712
  resourceClients
3320
3713
  } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
3321
- if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
3322
- return { instance: before, cascaded: 0, fired: !1 };
3323
- const evaluation = await evaluateInstance({
3714
+ return findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0 ? { instance: before, cascaded: 0, fired: !1 } : dispatchGatedWrite({
3324
3715
  client,
3325
3716
  tag,
3326
3717
  instanceId,
3327
3718
  access,
3719
+ actor,
3720
+ clientForGdr,
3328
3721
  clock,
3329
- ...resourceClients !== void 0 ? { resourceClients } : {}
3722
+ before,
3723
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3724
+ apply: (instance, evaluation) => (assertActionAllowed(evaluation, task, action), applyAction({
3725
+ client,
3726
+ instance,
3727
+ task,
3728
+ action,
3729
+ actor,
3730
+ ...access.grants !== void 0 ? { grants: access.grants } : {},
3731
+ clock,
3732
+ clientForGdr,
3733
+ ...params !== void 0 ? { params } : {}
3734
+ }))
3330
3735
  });
3331
- assertActionAllowed(evaluation, task, action);
3332
- const ranOps = await applyAction({
3736
+ },
3737
+ /**
3738
+ * Edit a declared-editable state slot directly — reassign, reschedule,
3739
+ * claim-by-hand, append to a running log — through the generic edit seam,
3740
+ * instead of a bespoke action per field. Soft-gates on the slot's declared
3741
+ * editability (the same projection a UI renders), applies the edit as a
3742
+ * `state.*` op (so provenance + history are stamped by the op path),
3743
+ * refreshes the stage's guards, then cascades — an edit to a value a
3744
+ * transition reads can and should move the instance. Advisory like every
3745
+ * engine gate; the lake ACL + guards are the only hard locks.
3746
+ *
3747
+ * Each call is a discrete COMMIT (a history entry, a guard refresh, a
3748
+ * cascade, an `ifRevisionId` write), not a draft patch — so an inline-field
3749
+ * UI must bind it to a deliberate boundary (blur / Enter / Save / debounce),
3750
+ * never an `onChange` per keystroke.
3751
+ */
3752
+ editState: async (args) => {
3753
+ const { client, tag, instanceId, target, mode, value, resourceClients } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
3754
+ return dispatchGatedWrite({
3333
3755
  client,
3334
- instance: before,
3335
- task,
3336
- action,
3756
+ tag,
3757
+ instanceId,
3758
+ access,
3337
3759
  actor,
3338
- ...access.grants !== void 0 ? { grants: access.grants } : {},
3339
- clock,
3340
3760
  clientForGdr,
3341
- ...params !== void 0 ? { params } : {}
3342
- }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
3343
- return {
3344
- instance: await reload(client, instanceId, tag),
3345
- cascaded,
3346
- fired: !0,
3347
- ...ranOps !== void 0 ? { ranOps } : {}
3348
- };
3761
+ clock,
3762
+ before,
3763
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3764
+ apply: (instance, evaluation) => (assertEditAllowed(evaluation, target), applyEdit({
3765
+ client,
3766
+ instance,
3767
+ target,
3768
+ ...mode !== void 0 ? { mode } : {},
3769
+ ...value !== void 0 ? { value } : {},
3770
+ actor,
3771
+ ...access.grants !== void 0 ? { grants: access.grants } : {},
3772
+ clock,
3773
+ clientForGdr
3774
+ }))
3775
+ });
3349
3776
  },
3350
3777
  /**
3351
3778
  * Report a queued effect's outcome. Drains it from `pendingEffects`,
@@ -3763,6 +4190,9 @@ function createInstanceSession(args) {
3763
4190
  ...clock !== void 0 ? { now: clock() } : {},
3764
4191
  ...grants !== void 0 ? { grants } : {}
3765
4192
  });
4193
+ }, settleAfterApply = async (actor, held, ranOps) => {
4194
+ const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
4195
+ return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
3766
4196
  }, commit = async (run) => {
3767
4197
  committing = !0;
3768
4198
  const held = new Map(overlay);
@@ -3813,8 +4243,25 @@ function createInstanceSession(args) {
3813
4243
  ...grants !== void 0 ? { grants } : {},
3814
4244
  ...clock !== void 0 ? { clock } : {},
3815
4245
  ...params !== void 0 ? { params } : {}
3816
- }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3817
- return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
4246
+ });
4247
+ return settleAfterApply(actor, held, ranOps);
4248
+ });
4249
+ },
4250
+ editState({ target, mode, value }) {
4251
+ return commit(async (actor, held) => {
4252
+ assertEditAllowed(await evaluateWith(held, heldGuards), target);
4253
+ const { grants } = await access(), ranOps = await applyEdit({
4254
+ client,
4255
+ instance,
4256
+ target,
4257
+ ...mode !== void 0 ? { mode } : {},
4258
+ ...value !== void 0 ? { value } : {},
4259
+ actor,
4260
+ clientForGdr,
4261
+ ...grants !== void 0 ? { grants } : {},
4262
+ ...clock !== void 0 ? { clock } : {}
4263
+ });
4264
+ return settleAfterApply(actor, held, ranOps);
3818
4265
  });
3819
4266
  }
3820
4267
  };
@@ -3862,6 +4309,7 @@ function createEngine(args) {
3862
4309
  deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
3863
4310
  startInstance: (rest) => workflow.startInstance(bind(rest)),
3864
4311
  fireAction: (rest) => workflow.fireAction(bind(rest)),
4312
+ editState: (rest) => workflow.editState(bind(rest)),
3865
4313
  completeEffect: (rest) => workflow.completeEffect(bind(rest)),
3866
4314
  tick: (rest) => workflow.tick(bind(rest)),
3867
4315
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
@@ -4153,20 +4601,25 @@ function displayDescription(typeKey) {
4153
4601
  return DISPLAY[typeKey]?.description;
4154
4602
  }
4155
4603
  exports.WORKFLOW_DEFINITION_TYPE = schema.WORKFLOW_DEFINITION_TYPE;
4604
+ exports.isStartableDefinition = schema.isStartableDefinition;
4156
4605
  exports.AUTHORING_DISPLAY = AUTHORING_DISPLAY;
4157
4606
  exports.ActionDisabledError = ActionDisabledError;
4158
4607
  exports.ActionParamsInvalidError = ActionParamsInvalidError;
4159
4608
  exports.CascadeLimitError = CascadeLimitError;
4609
+ exports.ConcurrentEditStateError = ConcurrentEditStateError;
4160
4610
  exports.ConcurrentFireActionError = ConcurrentFireActionError;
4161
4611
  exports.DEFAULT_CONTENT_PERSPECTIVE = DEFAULT_CONTENT_PERSPECTIVE;
4162
4612
  exports.DISPLAY = DISPLAY;
4163
4613
  exports.EFFECTS_CONTEXT_DISPLAY = EFFECTS_CONTEXT_DISPLAY;
4614
+ exports.EditStateDeniedError = EditStateDeniedError;
4164
4615
  exports.GUARD_DOC_TYPE = GUARD_DOC_TYPE;
4165
4616
  exports.GUARD_LIFTED_PREDICATE = GUARD_LIFTED_PREDICATE;
4166
4617
  exports.GUARD_OWNER = GUARD_OWNER;
4167
4618
  exports.HISTORY_DISPLAY = HISTORY_DISPLAY;
4168
4619
  exports.MutationGuardDeniedError = MutationGuardDeniedError;
4169
4620
  exports.OP_DISPLAY = OP_DISPLAY;
4621
+ exports.PartialGuardDeployError = PartialGuardDeployError;
4622
+ exports.RequiredStateNotProvidedError = RequiredStateNotProvidedError;
4170
4623
  exports.STATE_SLOT_DISPLAY = STATE_SLOT_DISPLAY;
4171
4624
  exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
4172
4625
  exports.WorkflowStateDivergedError = WorkflowStateDivergedError;