@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.js CHANGED
@@ -1,13 +1,9 @@
1
1
  import { parse, evaluate } from "groq-js";
2
- import { isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, DOCUMENT_VALUE_PERMISSIONS } from "./_chunks-es/schema.js";
2
+ import { actorFulfillsRole, isTerminalTaskStatus, WORKFLOW_DEFINITION_TYPE, andConditions, 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, roleAliases) {
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 : actorFulfillsRole(actor.roles, a.role, roleAliases)
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,90 @@ 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 ConcurrentEditStateError extends Error {
667
+ instanceId;
668
+ target;
669
+ attempts;
670
+ constructor(args) {
671
+ const where = args.target.task !== void 0 ? `${args.target.task}.${args.target.state}` : args.target.state;
672
+ super(
673
+ `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.`
674
+ ), this.name = "ConcurrentEditStateError", this.instanceId = args.instanceId, this.target = args.target, this.attempts = args.attempts;
675
+ }
676
+ }
677
+ class CascadeLimitError extends Error {
678
+ instanceId;
679
+ limit;
680
+ constructor(args) {
681
+ super(
682
+ `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.`
683
+ ), this.name = "CascadeLimitError", this.instanceId = args.instanceId, this.limit = args.limit;
684
+ }
685
+ }
596
686
  function buildSnapshot(args) {
597
687
  const docs = [], knownIds = /* @__PURE__ */ new Set();
598
688
  for (const { doc, resource } of args.docs) {
@@ -858,9 +948,16 @@ async function committedInstance(args) {
858
948
  }
859
949
  async function deployStageGuards(args) {
860
950
  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))
951
+ if (live === void 0 || live.currentStage !== args.stageName || live.abortedAt !== void 0) return;
952
+ let deployed = 0;
953
+ for (const { client, doc } of resolvedStageGuards(args)) {
954
+ try {
863
955
  await upsertGuard(client, doc);
956
+ } catch (cause) {
957
+ throw deployed > 0 ? new PartialGuardDeployError({ stageName: args.stageName, deployed, cause }) : cause;
958
+ }
959
+ deployed += 1;
960
+ }
864
961
  }
865
962
  async function retractStageGuards(args) {
866
963
  const live = await committedInstance(args);
@@ -1049,6 +1146,7 @@ function derivePerspectiveFromState(entries) {
1049
1146
  async function resolveDeclaredState(args) {
1050
1147
  const { entryDefs, initialState, ctx, randomKey: randomKey2 } = args;
1051
1148
  if (entryDefs === void 0 || entryDefs.length === 0) return [];
1149
+ assertRequiredInitProvided(entryDefs, initialState, ctx.definitionName);
1052
1150
  const out = [];
1053
1151
  for (const entry of entryDefs) {
1054
1152
  const scopedCtx = { ...ctx, resolvedState: out };
@@ -1056,6 +1154,17 @@ async function resolveDeclaredState(args) {
1056
1154
  }
1057
1155
  return out;
1058
1156
  }
1157
+ function assertRequiredInitProvided(entryDefs, initialState, definitionName) {
1158
+ const missing = entryDefs.filter((entry) => entry.required === !0 && entry.source.type === "init").filter((entry) => {
1159
+ const match = initialState.find((s) => s.name === entry.name && s.type === entry.type);
1160
+ return match === void 0 || match.value === void 0 || match.value === null;
1161
+ }).map((entry) => ({ name: entry.name, type: entry.type }));
1162
+ if (missing.length !== 0)
1163
+ throw new RequiredStateNotProvidedError({
1164
+ ...definitionName !== void 0 ? { definition: definitionName } : {},
1165
+ missing
1166
+ });
1167
+ }
1059
1168
  const ARRAY_SLOT_TYPES = /* @__PURE__ */ new Set([
1060
1169
  "doc.refs",
1061
1170
  "checklist",
@@ -1184,6 +1293,12 @@ function coerceToGdr(raw, workflowResource) {
1184
1293
  function gdrToBareId(uri) {
1185
1294
  return uri.includes(":") ? extractDocumentId(uri) : uri;
1186
1295
  }
1296
+ function loadCallContext(client, instanceId, options) {
1297
+ return loadContext(client, instanceId, {
1298
+ ...options?.clock ? { clock: options.clock } : {},
1299
+ ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
1300
+ });
1301
+ }
1187
1302
  async function loadContext(client, instanceId, options) {
1188
1303
  const instance = await client.getDocument(instanceId);
1189
1304
  if (!instance)
@@ -1204,7 +1319,7 @@ async function ctxConditionParams(ctx, opts) {
1204
1319
  ...base,
1205
1320
  state,
1206
1321
  actor: opts?.actor,
1207
- assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor) : !1,
1322
+ assigned: opts?.taskName !== void 0 ? assignedFor(ctx.instance, opts.taskName, opts?.actor, ctx.definition.roleAliases) : !1,
1208
1323
  ...opts?.vars
1209
1324
  };
1210
1325
  return { ...await evaluatePredicates({
@@ -1249,86 +1364,36 @@ async function resolveStageStateEntries(args) {
1249
1364
  ctx: {
1250
1365
  client,
1251
1366
  now,
1252
- selfId: instance._id,
1253
- tag: instance.tag,
1254
- stageName: stage.name,
1255
- workflowResource: instance.workflowResource,
1256
- // Allow stage-scope entries' `source: { type: "stateRead", scope:
1257
- // "workflow", ... }` to see the already-resolved workflow-scope state.
1258
- workflowState: instance.state ?? [],
1259
- ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1260
- },
1261
- randomKey
1262
- });
1263
- }
1264
- async function resolveTaskStateEntries(args) {
1265
- const { client, instance, stage, task, now } = args;
1266
- return resolveDeclaredState({
1267
- entryDefs: task.state,
1268
- initialState: [],
1269
- ctx: {
1270
- client,
1271
- now,
1272
- selfId: instance._id,
1273
- tag: instance.tag,
1274
- stageName: stage.name,
1275
- workflowResource: instance.workflowResource,
1276
- taskName: task.name,
1277
- workflowState: instance.state ?? [],
1278
- ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1279
- },
1280
- randomKey
1281
- });
1282
- }
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");
1367
+ selfId: instance._id,
1368
+ tag: instance.tag,
1369
+ stageName: stage.name,
1370
+ workflowResource: instance.workflowResource,
1371
+ // Allow stage-scope entries' `source: { type: "stateRead", scope:
1372
+ // "workflow", ... }` to see the already-resolved workflow-scope state.
1373
+ workflowState: instance.state ?? [],
1374
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1375
+ },
1376
+ randomKey
1377
+ });
1323
1378
  }
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
- }
1379
+ async function resolveTaskStateEntries(args) {
1380
+ const { client, instance, stage, task, now } = args;
1381
+ return resolveDeclaredState({
1382
+ entryDefs: task.state,
1383
+ initialState: [],
1384
+ ctx: {
1385
+ client,
1386
+ now,
1387
+ selfId: instance._id,
1388
+ tag: instance.tag,
1389
+ stageName: stage.name,
1390
+ workflowResource: instance.workflowResource,
1391
+ taskName: task.name,
1392
+ workflowState: instance.state ?? [],
1393
+ ...instance.perspective !== void 0 ? { perspective: instance.perspective } : {}
1394
+ },
1395
+ randomKey
1396
+ });
1332
1397
  }
1333
1398
  async function deployOrRollback(args) {
1334
1399
  try {
@@ -1351,7 +1416,11 @@ async function deployOrRollback(args) {
1351
1416
  reason: "rollback of the committed move failed"
1352
1417
  });
1353
1418
  }
1354
- throw guardError;
1419
+ throw guardError instanceof PartialGuardDeployError ? new WorkflowStateDivergedError({
1420
+ instanceId: args.instanceId,
1421
+ guardError,
1422
+ reason: "a multi-guard deploy partially applied; the rolled-back move left orphaned guard locks"
1423
+ }) : guardError;
1355
1424
  }
1356
1425
  }
1357
1426
  function applyTaskStatusChange(args) {
@@ -1440,7 +1509,8 @@ function materializeInstance(base, mutation) {
1440
1509
  ...base,
1441
1510
  currentStage: mutation.currentStage,
1442
1511
  state: mutation.state,
1443
- stages: mutation.stages
1512
+ stages: mutation.stages,
1513
+ effectsContext: mutation.effectsContext
1444
1514
  };
1445
1515
  }
1446
1516
  async function persist(ctx, mutation) {
@@ -1455,7 +1525,21 @@ async function persist(ctx, mutation) {
1455
1525
  tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)), await tx.commit(), mutation.pendingCreates = [];
1456
1526
  const actorForPriming = void 0;
1457
1527
  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);
1528
+ try {
1529
+ await primeInitialStage(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock), await cascadeAutoTransitions(
1530
+ ctx.client,
1531
+ body._id,
1532
+ actorForPriming,
1533
+ ctx.clientForGdr,
1534
+ ctx.clock
1535
+ ), await propagateToAncestors(ctx.client, body._id, actorForPriming, ctx.clientForGdr, ctx.clock);
1536
+ } catch (cause) {
1537
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
1538
+ instanceId: ctx.instance._id,
1539
+ guardError: cause,
1540
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
1541
+ });
1542
+ }
1459
1543
  const reloaded = await ctx.client.getDocument(ctx.instance._id);
1460
1544
  if (!reloaded)
1461
1545
  throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
@@ -1720,6 +1804,7 @@ async function prepareChildInstance(args) {
1720
1804
  selfId: childDocId,
1721
1805
  tag: childTag,
1722
1806
  workflowResource,
1807
+ definitionName: definition.name,
1723
1808
  ...inheritedPerspective !== void 0 ? { perspective: inheritedPerspective } : {}
1724
1809
  },
1725
1810
  randomKey
@@ -1766,6 +1851,28 @@ async function resolveBindings(args) {
1766
1851
  resolved[key] = await runGroq(groq, args.params, args.snapshot);
1767
1852
  return { ...resolved, ...args.staticInput };
1768
1853
  }
1854
+ async function persistThenDeploy(ctx, mutation, deploy) {
1855
+ const spawned = mutation.pendingCreates.length > 0, committed = await persist(ctx, mutation);
1856
+ await deployOrRollback({
1857
+ client: ctx.client,
1858
+ instanceId: ctx.instance._id,
1859
+ committedRev: committed._rev,
1860
+ restore: instanceStateFields(ctx.instance),
1861
+ unset: ctx.instance.completedAt === void 0 ? ["completedAt"] : [],
1862
+ reversible: !spawned,
1863
+ deploy
1864
+ });
1865
+ }
1866
+ async function refreshStageGuards(ctx, mutation, stageName) {
1867
+ await deployStageGuards({
1868
+ client: ctx.client,
1869
+ clientForGdr: ctx.clientForGdr,
1870
+ instance: materializeInstance(ctx.instance, mutation),
1871
+ definition: ctx.definition,
1872
+ stageName,
1873
+ now: ctx.now
1874
+ });
1875
+ }
1769
1876
  async function completeEffect(args) {
1770
1877
  const { client, instanceId, effectKey, status, outputs, detail, error, durationMs, options } = args, ctx = await loadContext(client, instanceId, {
1771
1878
  ...options?.clock ? { clock: options.clock } : {},
@@ -1811,9 +1918,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1811
1918
  const mutation = startMutation(ctx.instance);
1812
1919
  mutation.pendingEffects = mutation.pendingEffects.filter((e) => e._key !== effectKey);
1813
1920
  const ranAt = ctx.now;
1814
- return mutation.effectHistory.push(
1921
+ mutation.effectHistory.push(
1815
1922
  buildEffectHistoryEntry(pending, { status, ranAt, actor, detail, error, durationMs, outputs })
1816
- ), status === "done" && outputs !== void 0 && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1923
+ );
1924
+ const wroteEffectsContext = status === "done" && outputs !== void 0;
1925
+ return wroteEffectsContext && upsertEffectsContext(mutation, pending.name, outputs), mutation.history.push({
1817
1926
  _key: randomKey(),
1818
1927
  _type: "effectCompleted",
1819
1928
  at: ranAt,
@@ -1823,7 +1932,11 @@ async function commitCompleteEffect(ctx, effectKey, status, outputs, detail, err
1823
1932
  ...outputs !== void 0 ? { outputs } : {},
1824
1933
  ...detail !== void 0 ? { detail } : {},
1825
1934
  ...actor !== void 0 ? { actor } : {}
1826
- }), await persist(ctx, mutation), { effectKey, status };
1935
+ }), await persistThenDeploy(
1936
+ ctx,
1937
+ mutation,
1938
+ () => wroteEffectsContext ? refreshStageGuards(ctx, mutation, ctx.instance.currentStage) : Promise.resolve()
1939
+ ), { effectKey, status };
1827
1940
  }
1828
1941
  function buildQueuedEffect(effect, origin, params, actor, now) {
1829
1942
  const key = randomKey(), pending = {
@@ -1937,22 +2050,27 @@ function runOps(args) {
1937
2050
  const summaries = [];
1938
2051
  for (const op of ops) {
1939
2052
  const summary = applyOp(op, { mutation, stage, taskName: origin.task, params, actor, self, now });
1940
- summaries.push(summary), mutation.history.push({
1941
- _key: randomKey(),
1942
- _type: "opApplied",
1943
- at: now,
1944
- stage,
1945
- ...origin.task !== void 0 ? { task: origin.task } : {},
1946
- ...origin.action !== void 0 ? { action: origin.action } : {},
1947
- ...origin.transition !== void 0 ? { transition: origin.transition } : {},
1948
- opType: summary.opType,
1949
- ...summary.target !== void 0 ? { target: summary.target } : {},
1950
- ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
1951
- ...actor !== void 0 ? { actor } : {}
1952
- });
2053
+ summaries.push(summary), mutation.history.push(opAppliedEntry({ origin, summary, stage, actor, now }));
1953
2054
  }
1954
2055
  return summaries;
1955
2056
  }
2057
+ function opAppliedEntry(args) {
2058
+ const { origin, summary, stage, actor, now } = args;
2059
+ return {
2060
+ _key: randomKey(),
2061
+ _type: "opApplied",
2062
+ at: now,
2063
+ stage,
2064
+ ...origin.task !== void 0 ? { task: origin.task } : {},
2065
+ ...origin.action !== void 0 ? { action: origin.action } : {},
2066
+ ...origin.transition !== void 0 ? { transition: origin.transition } : {},
2067
+ ...origin.edit === !0 ? { edit: !0 } : {},
2068
+ opType: summary.opType,
2069
+ ...summary.target !== void 0 ? { target: summary.target } : {},
2070
+ ...summary.resolved !== void 0 ? { resolved: summary.resolved } : {},
2071
+ ...actor !== void 0 ? { actor } : {}
2072
+ };
2073
+ }
1956
2074
  function applyOp(op, ctx) {
1957
2075
  switch (op.type) {
1958
2076
  case "state.set":
@@ -2283,28 +2401,6 @@ async function persistStageMove(ctx, mutation, exitedStage, enteredStage) {
2283
2401
  now: ctx.now
2284
2402
  });
2285
2403
  }
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
2404
  async function commitTransition(ctx, actor) {
2309
2405
  if (isTerminal(ctx))
2310
2406
  return { fired: !1 };
@@ -2608,6 +2704,85 @@ async function fetchGrantsCached(requestFn, resourcePath) {
2608
2704
  return;
2609
2705
  }
2610
2706
  }
2707
+ function effectiveEditable(baseline, override) {
2708
+ if (baseline === void 0) return;
2709
+ if (override === void 0) return baseline;
2710
+ if (baseline === !0 && override === !0) return !0;
2711
+ const parts = [baseline, override].map((e) => e === !0 ? void 0 : e);
2712
+ return andConditions(parts) ?? !0;
2713
+ }
2714
+ function slotSites(definition, stage) {
2715
+ const sites = [];
2716
+ for (const entry of definition.state ?? []) sites.push({ scope: "workflow", entry });
2717
+ for (const entry of stage.state ?? []) sites.push({ scope: "stage", entry });
2718
+ for (const task of stage.tasks ?? [])
2719
+ for (const entry of task.state ?? []) sites.push({ scope: "task", task: task.name, entry });
2720
+ return sites;
2721
+ }
2722
+ function toResolvedSlot(site, stage) {
2723
+ return {
2724
+ scope: site.scope,
2725
+ ...site.task !== void 0 ? { task: site.task } : {},
2726
+ name: site.entry.name,
2727
+ type: site.entry.type,
2728
+ ...site.entry.title !== void 0 ? { title: site.entry.title } : {},
2729
+ ref: { scope: site.scope, state: site.entry.name },
2730
+ effective: effectiveEditable(site.entry.editable, stage.editable?.[site.entry.name])
2731
+ };
2732
+ }
2733
+ function editableSlotsInStage(definition, stage) {
2734
+ return slotSites(definition, stage).filter((site) => site.entry.editable !== void 0).map((site) => toResolvedSlot(site, stage));
2735
+ }
2736
+ function editTargetMatches(slot, target) {
2737
+ 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;
2738
+ }
2739
+ const SCOPE_PRECEDENCE = { task: 0, stage: 1, workflow: 2 };
2740
+ function resolveEditTarget(args) {
2741
+ const { definition, stage, target } = args, found = slotSites(definition, stage).filter(
2742
+ (site) => editTargetMatches(
2743
+ {
2744
+ scope: site.scope,
2745
+ name: site.entry.name,
2746
+ ...site.task !== void 0 ? { task: site.task } : {}
2747
+ },
2748
+ target
2749
+ )
2750
+ ).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
2751
+ return found ? toResolvedSlot(found, stage) : void 0;
2752
+ }
2753
+ function slotWindowOpen(instance, slot) {
2754
+ if (instance.completedAt !== void 0) return { open: !1, detail: "instance completed" };
2755
+ if (instance.abortedAt !== void 0) return { open: !1, detail: "instance aborted" };
2756
+ if (slot.scope !== "task") return { open: !0 };
2757
+ const status = findOpenStageEntry(instance)?.tasks.find((t) => t.name === slot.task)?.status;
2758
+ return status === "active" ? { open: !0 } : { open: !1, detail: `task "${slot.task}" is ${status ?? "not active"}` };
2759
+ }
2760
+ function readSlotValue(instance, slot) {
2761
+ return slotStateHost(instance, slot)?.find((e) => e.name === slot.name)?.value;
2762
+ }
2763
+ function slotStateHost(instance, slot) {
2764
+ if (slot.scope === "workflow") return instance.state;
2765
+ const stageEntry = findOpenStageEntry(instance);
2766
+ return slot.scope === "stage" ? stageEntry?.state : stageEntry?.tasks.find((t) => t.name === slot.task)?.state;
2767
+ }
2768
+ function slotProvenance(instance, ref) {
2769
+ let latest;
2770
+ for (const entry of instance.history)
2771
+ entry._type === "opApplied" && (entry.target?.scope !== ref.scope || entry.target.state !== ref.state || (latest = { at: entry.at, ...entry.actor !== void 0 ? { actor: entry.actor } : {} }));
2772
+ return latest === void 0 ? {} : {
2773
+ ...latest.actor !== void 0 ? { setBy: latest.actor } : {},
2774
+ setAt: latest.at
2775
+ };
2776
+ }
2777
+ function editDisabledReason(args) {
2778
+ const { effective, instance, window, guardDenial, predicateSatisfied } = args;
2779
+ if (effective === void 0) return { kind: "not-editable" };
2780
+ if (!window.open)
2781
+ return instance.completedAt !== void 0 ? { kind: "instance-completed", completedAt: instance.completedAt } : { kind: "edit-window-closed", detail: window.detail ?? "closed" };
2782
+ if (guardDenial !== void 0) return guardDenial;
2783
+ if (!predicateSatisfied)
2784
+ return { kind: "editor-not-permitted", predicate: effective === !0 ? "true" : effective };
2785
+ }
2611
2786
  async function evaluateInstance(args) {
2612
2787
  const { client, tag, instanceId, resourceClients } = args, now = (args.clock ?? wallClock)();
2613
2788
  validateTag(tag);
@@ -2641,6 +2816,7 @@ async function evaluateFromSnapshot(args) {
2641
2816
  actor,
2642
2817
  snapshot,
2643
2818
  scope,
2819
+ roleAliases: definition.roleAliases,
2644
2820
  stageHasExits: !isTerminalStage(stage),
2645
2821
  guardDenial
2646
2822
  })
@@ -2662,16 +2838,65 @@ async function evaluateFromSnapshot(args) {
2662
2838
  stage,
2663
2839
  tasks: taskEvaluations,
2664
2840
  transitions: transitionEvaluations
2665
- }, pendingOnYou = taskEvaluations.filter((t) => t.pendingOnActor), canInteract = taskEvaluations.some((t) => t.actions.some((a) => a.allowed));
2841
+ }, 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({
2842
+ instance,
2843
+ definition,
2844
+ stage,
2845
+ actor,
2846
+ snapshot,
2847
+ scope,
2848
+ guardDenial: editGuardDenial
2849
+ });
2666
2850
  return {
2667
2851
  instance,
2668
2852
  definition,
2669
2853
  actor,
2670
2854
  currentStage,
2671
2855
  pendingOnYou,
2672
- canInteract
2856
+ canInteract,
2857
+ editableSlots
2673
2858
  };
2674
2859
  }
2860
+ async function evaluateEditableSlots(args) {
2861
+ const { instance, definition, stage, actor, snapshot, scope, guardDenial } = args, slots = [];
2862
+ for (const slot of editableSlotsInStage(definition, stage)) {
2863
+ const window = slotWindowOpen(instance, slot), predicateSatisfied = await editPredicateSatisfied({
2864
+ slot,
2865
+ window,
2866
+ guardDenial,
2867
+ instance,
2868
+ snapshot,
2869
+ scope,
2870
+ actor,
2871
+ roleAliases: definition.roleAliases
2872
+ }), reason = editDisabledReason({
2873
+ effective: slot.effective,
2874
+ instance,
2875
+ window,
2876
+ guardDenial,
2877
+ predicateSatisfied
2878
+ }), value = readSlotValue(instance, slot);
2879
+ slots.push({
2880
+ scope: slot.scope,
2881
+ ...slot.task !== void 0 ? { task: slot.task } : {},
2882
+ name: slot.name,
2883
+ type: slot.type,
2884
+ ...slot.title !== void 0 ? { title: slot.title } : {},
2885
+ value,
2886
+ editable: reason === void 0,
2887
+ ...reason !== void 0 ? { disabledReason: reason } : {},
2888
+ ...slotProvenance(instance, slot.ref)
2889
+ });
2890
+ }
2891
+ return slots;
2892
+ }
2893
+ async function editPredicateSatisfied(args) {
2894
+ const { slot, window, guardDenial, instance, snapshot, scope, actor, roleAliases } = args;
2895
+ if (!window.open || guardDenial !== void 0) return !1;
2896
+ if (slot.effective === !0) return !0;
2897
+ const params = slot.scope === "task" && slot.task !== void 0 ? taskScopeFor({ scope, instance, snapshot, actor, taskName: slot.task, roleAliases }) : scope;
2898
+ return evaluateCondition({ condition: slot.effective, snapshot, params });
2899
+ }
2675
2900
  async function renderScope(args) {
2676
2901
  const { instance, definition, actor, grants, snapshot, now } = args, params = {
2677
2902
  ...buildParams({ instance, now, snapshot }),
@@ -2697,15 +2922,36 @@ async function advisoryCan(instance, actor, grants) {
2697
2922
  });
2698
2923
  return can;
2699
2924
  }
2700
- async function evaluateTask(args) {
2701
- const { task, statusEntry, instance, actor, snapshot, scope, stageHasExits, guardDenial } = args, status = statusEntry?.status ?? "pending", assigned = assignedFor(instance, task.name, actor), taskScope = {
2925
+ function taskScopeFor(args) {
2926
+ const { scope, instance, snapshot, actor, taskName, roleAliases } = args;
2927
+ return {
2702
2928
  ...scope,
2703
2929
  state: {
2704
2930
  ...scope.state,
2705
- ...scopedStateOverlay(instance, snapshot, task.name)
2931
+ ...scopedStateOverlay(instance, snapshot, taskName)
2706
2932
  },
2707
- assigned
2708
- }, unmetRequirements = await evaluateRequirements({
2933
+ assigned: assignedFor(instance, taskName, actor, roleAliases)
2934
+ };
2935
+ }
2936
+ async function evaluateTask(args) {
2937
+ const {
2938
+ task,
2939
+ statusEntry,
2940
+ instance,
2941
+ actor,
2942
+ snapshot,
2943
+ scope,
2944
+ roleAliases,
2945
+ stageHasExits,
2946
+ guardDenial
2947
+ } = args, status = statusEntry?.status ?? "pending", taskScope = taskScopeFor({
2948
+ scope,
2949
+ instance,
2950
+ snapshot,
2951
+ actor,
2952
+ taskName: task.name,
2953
+ roleAliases
2954
+ }), assigned = taskScope.assigned === !0, unmetRequirements = await evaluateRequirements({
2709
2955
  requirements: task.requirements,
2710
2956
  snapshot,
2711
2957
  params: taskScope
@@ -2932,10 +3178,7 @@ async function commitAbort(ctx, reason, actor) {
2932
3178
  async function fireAction(args) {
2933
3179
  const { client, instanceId, task, action, params, options } = args;
2934
3180
  for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
2935
- const ctx = await loadContext(client, instanceId, {
2936
- ...options?.clock ? { clock: options.clock } : {},
2937
- ...options?.clientForGdr ? { clientForGdr: options.clientForGdr } : {}
2938
- });
3181
+ const ctx = await loadCallContext(client, instanceId, options);
2939
3182
  try {
2940
3183
  return await commitAction(ctx, task, action, params, options);
2941
3184
  } catch (error) {
@@ -3030,6 +3273,114 @@ function formatDisabledReason(task, action, reason) {
3030
3273
  const detail = disabledReasonDetail[reason.kind](reason);
3031
3274
  return `Action "${task}:${action}" is not allowed: ${detail}`;
3032
3275
  }
3276
+ class EditStateDeniedError extends Error {
3277
+ reason;
3278
+ target;
3279
+ constructor(args) {
3280
+ super(formatEditDisabledReason(args.target, args.reason)), this.name = "EditStateDeniedError", this.reason = args.reason, this.target = args.target;
3281
+ }
3282
+ }
3283
+ const editDisabledReasonDetail = {
3284
+ "not-editable": () => "slot is not declared editable",
3285
+ "instance-completed": (r) => `instance completed at ${r.completedAt}`,
3286
+ "edit-window-closed": (r) => `edit window closed (${r.detail})`,
3287
+ "editor-not-permitted": (r) => `editor not permitted (${r.predicate})`,
3288
+ "mutation-guard-denied": (r) => `blocked by mutation guard(s) [${r.guardIds.join(", ")}]${r.detail ? ` (${r.detail})` : ""}`
3289
+ };
3290
+ function formatEditDisabledReason(target, reason) {
3291
+ const detail = editDisabledReasonDetail[reason.kind](
3292
+ reason
3293
+ ), where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
3294
+ return `State "${target.scope}:${where}" is not editable: ${detail}`;
3295
+ }
3296
+ async function editState(args) {
3297
+ const { client, instanceId, target, mode = "set", value, options } = args;
3298
+ for (let attempt = 1; attempt <= CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS; attempt++) {
3299
+ const ctx = await loadCallContext(client, instanceId, options);
3300
+ try {
3301
+ return await commitEdit(ctx, target, mode, value, options);
3302
+ } catch (error) {
3303
+ if (!isRevisionConflict(error)) throw error;
3304
+ }
3305
+ }
3306
+ throw new ConcurrentEditStateError({
3307
+ instanceId,
3308
+ target: labelTarget(target),
3309
+ attempts: CONCURRENT_FIRE_ACTION_MAX_ATTEMPTS
3310
+ });
3311
+ }
3312
+ async function commitEdit(ctx, target, mode, value, options) {
3313
+ const actor = options?.actor, stage = findStage(ctx.definition, ctx.instance.currentStage), slot = resolveEditTarget({ definition: ctx.definition, stage, target });
3314
+ if (slot === void 0)
3315
+ throw new Error(
3316
+ `No state slot "${describeTarget(target)}" resolves in current stage "${stage.name}" of ${ctx.definition.name}`
3317
+ );
3318
+ await assertSlotEditable(ctx, slot, options);
3319
+ const op = buildEditOp(slot, mode, value), mutation = startMutation(ctx.instance), ranOps = runOps({
3320
+ ops: [op],
3321
+ mutation,
3322
+ stage: stage.name,
3323
+ origin: {
3324
+ edit: !0,
3325
+ ...slot.scope === "task" && slot.task !== void 0 ? { task: slot.task } : {}
3326
+ },
3327
+ params: {},
3328
+ actor,
3329
+ self: selfGdr(ctx.instance),
3330
+ now: ctx.now
3331
+ });
3332
+ return await persistThenDeploy(
3333
+ ctx,
3334
+ mutation,
3335
+ () => ranOps.some(isStateOp) ? refreshStageGuards(ctx, mutation, stage.name) : Promise.resolve()
3336
+ ), {
3337
+ edited: !0,
3338
+ target: slotTarget(slot),
3339
+ ...ranOps.length > 0 ? { ranOps } : {}
3340
+ };
3341
+ }
3342
+ async function assertSlotEditable(ctx, slot, options) {
3343
+ 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, {
3344
+ ...slot.task !== void 0 ? { taskName: slot.task } : {},
3345
+ ...actor !== void 0 ? { actor } : {},
3346
+ ...can !== void 0 ? { vars: { can } } : {}
3347
+ }) : slot.effective === !0, reason = editDisabledReason({
3348
+ effective: slot.effective,
3349
+ instance: ctx.instance,
3350
+ window,
3351
+ guardDenial: void 0,
3352
+ predicateSatisfied
3353
+ });
3354
+ if (reason !== void 0) throw new EditStateDeniedError({ target: slotTarget(slot), reason });
3355
+ }
3356
+ function buildEditOp(slot, mode, value) {
3357
+ if (mode === "unset") return { type: "state.unset", target: slot.ref };
3358
+ if (value === void 0)
3359
+ throw new Error(`editState mode "${mode}" requires a value for slot "${slot.name}"`);
3360
+ return {
3361
+ type: mode === "append" ? "state.append" : "state.set",
3362
+ target: slot.ref,
3363
+ value: { type: "literal", value }
3364
+ };
3365
+ }
3366
+ function slotTarget(slot) {
3367
+ return {
3368
+ scope: slot.scope,
3369
+ state: slot.name,
3370
+ ...slot.task !== void 0 ? { task: slot.task } : {}
3371
+ };
3372
+ }
3373
+ function labelTarget(target) {
3374
+ return {
3375
+ scope: target.scope ?? (target.task !== void 0 ? "task" : "workflow"),
3376
+ state: target.state,
3377
+ ...target.task !== void 0 ? { task: target.task } : {}
3378
+ };
3379
+ }
3380
+ function describeTarget(target) {
3381
+ const where = target.task !== void 0 ? `${target.task}.${target.state}` : target.state;
3382
+ return target.scope !== void 0 ? `${target.scope}:${where}` : where;
3383
+ }
3033
3384
  function bareIdFromSpawnRef(uri) {
3034
3385
  if (!uri.includes(":")) return [uri];
3035
3386
  try {
@@ -3076,18 +3427,60 @@ function buildClientForGdr(defaultClient, resolver) {
3076
3427
  function toEffectsContextEntries(ctx) {
3077
3428
  return Object.entries(ctx).map(([id, value]) => effectsContextEntry(id, value));
3078
3429
  }
3430
+ async function dispatchGatedWrite(args) {
3431
+ const { client, tag, instanceId, resourceClients, access, apply } = args, evaluation = await evaluateInstance({
3432
+ client,
3433
+ tag,
3434
+ instanceId,
3435
+ access,
3436
+ clock: args.clock,
3437
+ ...resourceClients !== void 0 ? { resourceClients } : {}
3438
+ }), ranOps = await apply(args.before, evaluation), cascaded = await cascade(client, instanceId, args.actor, args.clientForGdr, args.clock);
3439
+ return {
3440
+ instance: await reload(client, instanceId, tag),
3441
+ cascaded,
3442
+ fired: !0,
3443
+ ...ranOps !== void 0 ? { ranOps } : {}
3444
+ };
3445
+ }
3079
3446
  function assertActionAllowed(evaluation, task, action) {
3080
3447
  const actionEval = evaluation.currentStage.tasks.find((t) => t.task.name === task)?.actions.find((a) => a.action.name === action);
3081
3448
  if (actionEval?.allowed === !1 && actionEval.disabledReason)
3082
3449
  throw new ActionDisabledError({ task, action, reason: actionEval.disabledReason });
3083
3450
  }
3084
- async function applyAction(args) {
3085
- const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = {
3086
- actor,
3087
- ...grants !== void 0 ? { grants } : {},
3088
- ...clock !== void 0 ? { clock } : {},
3089
- ...clientForGdr !== void 0 ? { clientForGdr } : {}
3451
+ function assertEditAllowed(evaluation, target) {
3452
+ const slot = evaluation.editableSlots.filter((s) => editTargetMatches(s, target)).sort((a, b) => SCOPE_PRECEDENCE[a.scope] - SCOPE_PRECEDENCE[b.scope])[0];
3453
+ if (slot !== void 0 && !slot.editable && slot.disabledReason !== void 0)
3454
+ throw new EditStateDeniedError({
3455
+ target: {
3456
+ scope: slot.scope,
3457
+ state: slot.name,
3458
+ ...slot.task !== void 0 ? { task: slot.task } : {}
3459
+ },
3460
+ reason: slot.disabledReason
3461
+ });
3462
+ }
3463
+ async function applyEdit(args) {
3464
+ const { client, instance, target, mode, value, actor, grants, clock, clientForGdr } = args;
3465
+ return (await editState({
3466
+ client,
3467
+ instanceId: instance._id,
3468
+ target,
3469
+ ...mode !== void 0 ? { mode } : {},
3470
+ ...value !== void 0 ? { value } : {},
3471
+ options: buildEngineCallOptions({ actor, grants, clock, clientForGdr })
3472
+ })).ranOps;
3473
+ }
3474
+ function buildEngineCallOptions(args) {
3475
+ return {
3476
+ actor: args.actor,
3477
+ ...args.grants !== void 0 ? { grants: args.grants } : {},
3478
+ ...args.clock !== void 0 ? { clock: args.clock } : {},
3479
+ ...args.clientForGdr !== void 0 ? { clientForGdr: args.clientForGdr } : {}
3090
3480
  };
3481
+ }
3482
+ async function applyAction(args) {
3483
+ const { client, instance, task, action, params, actor, grants, clock, clientForGdr } = args, options = buildEngineCallOptions({ actor, grants, clock, clientForGdr });
3091
3484
  return findOpenStageEntry(instance)?.tasks.find((t) => t.name === task)?.status === "pending" && await invokeTask({ client, instanceId: instance._id, task, options }), (await fireAction({
3092
3485
  client,
3093
3486
  instanceId: instance._id,
@@ -3261,6 +3654,7 @@ const workflow = {
3261
3654
  selfId: id,
3262
3655
  tag,
3263
3656
  workflowResource,
3657
+ definitionName: definition.name,
3264
3658
  ...perspective !== void 0 ? { perspective } : {}
3265
3659
  },
3266
3660
  randomKey
@@ -3302,34 +3696,68 @@ const workflow = {
3302
3696
  idempotent,
3303
3697
  resourceClients
3304
3698
  } = args, { access, actor, clientForGdr } = await resolveOperationContext(args), clock = args.clock ?? wallClock, before = await reload(client, instanceId, tag);
3305
- if (findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0)
3306
- return { instance: before, cascaded: 0, fired: !1 };
3307
- const evaluation = await evaluateInstance({
3699
+ return findOpenStageEntry(before)?.tasks.find((t) => t.name === task) === void 0 && idempotent === !0 ? { instance: before, cascaded: 0, fired: !1 } : dispatchGatedWrite({
3308
3700
  client,
3309
3701
  tag,
3310
3702
  instanceId,
3311
3703
  access,
3704
+ actor,
3705
+ clientForGdr,
3312
3706
  clock,
3313
- ...resourceClients !== void 0 ? { resourceClients } : {}
3707
+ before,
3708
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3709
+ apply: (instance, evaluation) => (assertActionAllowed(evaluation, task, action), applyAction({
3710
+ client,
3711
+ instance,
3712
+ task,
3713
+ action,
3714
+ actor,
3715
+ ...access.grants !== void 0 ? { grants: access.grants } : {},
3716
+ clock,
3717
+ clientForGdr,
3718
+ ...params !== void 0 ? { params } : {}
3719
+ }))
3314
3720
  });
3315
- assertActionAllowed(evaluation, task, action);
3316
- const ranOps = await applyAction({
3721
+ },
3722
+ /**
3723
+ * Edit a declared-editable state slot directly — reassign, reschedule,
3724
+ * claim-by-hand, append to a running log — through the generic edit seam,
3725
+ * instead of a bespoke action per field. Soft-gates on the slot's declared
3726
+ * editability (the same projection a UI renders), applies the edit as a
3727
+ * `state.*` op (so provenance + history are stamped by the op path),
3728
+ * refreshes the stage's guards, then cascades — an edit to a value a
3729
+ * transition reads can and should move the instance. Advisory like every
3730
+ * engine gate; the lake ACL + guards are the only hard locks.
3731
+ *
3732
+ * Each call is a discrete COMMIT (a history entry, a guard refresh, a
3733
+ * cascade, an `ifRevisionId` write), not a draft patch — so an inline-field
3734
+ * UI must bind it to a deliberate boundary (blur / Enter / Save / debounce),
3735
+ * never an `onChange` per keystroke.
3736
+ */
3737
+ editState: async (args) => {
3738
+ 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);
3739
+ return dispatchGatedWrite({
3317
3740
  client,
3318
- instance: before,
3319
- task,
3320
- action,
3741
+ tag,
3742
+ instanceId,
3743
+ access,
3321
3744
  actor,
3322
- ...access.grants !== void 0 ? { grants: access.grants } : {},
3323
- clock,
3324
3745
  clientForGdr,
3325
- ...params !== void 0 ? { params } : {}
3326
- }), cascaded = await cascade(client, instanceId, actor, clientForGdr, clock);
3327
- return {
3328
- instance: await reload(client, instanceId, tag),
3329
- cascaded,
3330
- fired: !0,
3331
- ...ranOps !== void 0 ? { ranOps } : {}
3332
- };
3746
+ clock,
3747
+ before,
3748
+ ...resourceClients !== void 0 ? { resourceClients } : {},
3749
+ apply: (instance, evaluation) => (assertEditAllowed(evaluation, target), applyEdit({
3750
+ client,
3751
+ instance,
3752
+ target,
3753
+ ...mode !== void 0 ? { mode } : {},
3754
+ ...value !== void 0 ? { value } : {},
3755
+ actor,
3756
+ ...access.grants !== void 0 ? { grants: access.grants } : {},
3757
+ clock,
3758
+ clientForGdr
3759
+ }))
3760
+ });
3333
3761
  },
3334
3762
  /**
3335
3763
  * Report a queued effect's outcome. Drains it from `pendingEffects`,
@@ -3747,6 +4175,9 @@ function createInstanceSession(args) {
3747
4175
  ...clock !== void 0 ? { now: clock() } : {},
3748
4176
  ...grants !== void 0 ? { grants } : {}
3749
4177
  });
4178
+ }, settleAfterApply = async (actor, held, ranOps) => {
4179
+ const cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
4180
+ return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
3750
4181
  }, commit = async (run) => {
3751
4182
  committing = !0;
3752
4183
  const held = new Map(overlay);
@@ -3797,8 +4228,25 @@ function createInstanceSession(args) {
3797
4228
  ...grants !== void 0 ? { grants } : {},
3798
4229
  ...clock !== void 0 ? { clock } : {},
3799
4230
  ...params !== void 0 ? { params } : {}
3800
- }), cascaded = await cascade(client, instance._id, actor, clientForGdr, clock, held);
3801
- return instance = await reload(client, instance._id, tag), { instance, cascaded, fired: !0, ...ranOps !== void 0 ? { ranOps } : {} };
4231
+ });
4232
+ return settleAfterApply(actor, held, ranOps);
4233
+ });
4234
+ },
4235
+ editState({ target, mode, value }) {
4236
+ return commit(async (actor, held) => {
4237
+ assertEditAllowed(await evaluateWith(held, heldGuards), target);
4238
+ const { grants } = await access(), ranOps = await applyEdit({
4239
+ client,
4240
+ instance,
4241
+ target,
4242
+ ...mode !== void 0 ? { mode } : {},
4243
+ ...value !== void 0 ? { value } : {},
4244
+ actor,
4245
+ clientForGdr,
4246
+ ...grants !== void 0 ? { grants } : {},
4247
+ ...clock !== void 0 ? { clock } : {}
4248
+ });
4249
+ return settleAfterApply(actor, held, ranOps);
3802
4250
  });
3803
4251
  }
3804
4252
  };
@@ -3846,6 +4294,7 @@ function createEngine(args) {
3846
4294
  deployDefinitions: (rest) => workflow.deployDefinitions(bind(rest)),
3847
4295
  startInstance: (rest) => workflow.startInstance(bind(rest)),
3848
4296
  fireAction: (rest) => workflow.fireAction(bind(rest)),
4297
+ editState: (rest) => workflow.editState(bind(rest)),
3849
4298
  completeEffect: (rest) => workflow.completeEffect(bind(rest)),
3850
4299
  tick: (rest) => workflow.tick(bind(rest)),
3851
4300
  evaluateInstance: (rest) => evaluateInstance(bind(rest)),
@@ -4141,16 +4590,20 @@ export {
4141
4590
  ActionDisabledError,
4142
4591
  ActionParamsInvalidError,
4143
4592
  CascadeLimitError,
4593
+ ConcurrentEditStateError,
4144
4594
  ConcurrentFireActionError,
4145
4595
  DEFAULT_CONTENT_PERSPECTIVE,
4146
4596
  DISPLAY,
4147
4597
  EFFECTS_CONTEXT_DISPLAY,
4598
+ EditStateDeniedError,
4148
4599
  GUARD_DOC_TYPE,
4149
4600
  GUARD_LIFTED_PREDICATE,
4150
4601
  GUARD_OWNER,
4151
4602
  HISTORY_DISPLAY,
4152
4603
  MutationGuardDeniedError,
4153
4604
  OP_DISPLAY,
4605
+ PartialGuardDeployError,
4606
+ RequiredStateNotProvidedError,
4154
4607
  STATE_SLOT_DISPLAY,
4155
4608
  WORKFLOW_DEFINITION_TYPE,
4156
4609
  WORKFLOW_INSTANCE_TYPE,
@@ -4186,6 +4639,7 @@ export {
4186
4639
  instanceGuardQuery,
4187
4640
  isDefinitionUnchanged,
4188
4641
  isGdr,
4642
+ isStartableDefinition,
4189
4643
  isTerminalStage,
4190
4644
  lakeGuardId,
4191
4645
  parseGdr,