@sanity/workflow-engine 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -262,6 +262,11 @@ const HIGH_FREQUENCY_SAMPLE_MS = 6e4, WorkflowDefinitionDeployed = defineWorkflo
262
262
  name: "Editorial Workflows Effects Drained",
263
263
  version: 1,
264
264
  description: "A drain pass ran — drainedCount/drainedEffects report the successful dispatches. Unsampled when non-empty (outcome counts must be complete; volume is bounded by real effect work); empty polls are machine-cadence noise, throttled engine-side to at most one per minute"
265
+ }), WorkflowEffectStateReported = defineWorkflowEvent({
266
+ name: "Editorial Workflows Effect State Reported",
267
+ version: 1,
268
+ description: "A running effect handler committed mid-dispatch field state through commitEffectOps. Sampled (high-frequency by design — a dispatch may report many times): an adoption and usage signal, not an exact per-run report count",
269
+ maxSampleRate: HIGH_FREQUENCY_SAMPLE_MS
265
270
  }), WorkflowEffectCompleted = defineWorkflowEvent({
266
271
  name: "Editorial Workflows Effect Completed",
267
272
  version: 1,
@@ -2234,6 +2239,21 @@ class StartNotSettledError extends invariants.WorkflowError {
2234
2239
  }
2235
2240
  }
2236
2241
 
2242
+ class StaleEffectClaimError extends invariants.WorkflowError {
2243
+ instanceId;
2244
+ effectKey;
2245
+ reason;
2246
+ constructor(args) {
2247
+ super("stale-effect-claim", `Mid-dispatch report for effect entry "${args.effectKey}" on ${args.instanceId} rejected: ${staleClaimDetail(args.reason)}. Nothing was written. Stop reporting from this dispatch — a newer dispatch (or none) owns the entry now.`),
2248
+ this.name = "StaleEffectClaimError", this.instanceId = args.instanceId, this.effectKey = args.effectKey,
2249
+ this.reason = args.reason;
2250
+ }
2251
+ }
2252
+
2253
+ function staleClaimDetail(reason) {
2254
+ return reason === "unclaimed" ? "the entry holds no claim" : reason === "claim-superseded" ? "the claim was taken over by another dispatch" : "the claim's lease has expired";
2255
+ }
2256
+
2237
2257
  const CONCURRENT_COMMIT_MAX_ATTEMPTS = 3;
2238
2258
 
2239
2259
  function lostRaceMessage(args) {
@@ -2290,6 +2310,20 @@ class ConcurrentCompleteEffectError extends invariants.WorkflowError {
2290
2310
  }
2291
2311
  }
2292
2312
 
2313
+ class ConcurrentCommitEffectOpsError extends invariants.WorkflowError {
2314
+ instanceId;
2315
+ effectKey;
2316
+ attempts;
2317
+ constructor(args) {
2318
+ super("concurrent-commit-effect-ops", lostRaceMessage({
2319
+ what: `Mid-dispatch report for effect entry "${args.effectKey}"`,
2320
+ instanceId: args.instanceId,
2321
+ attempts: args.attempts
2322
+ })), this.name = "ConcurrentCommitEffectOpsError", this.instanceId = args.instanceId,
2323
+ this.effectKey = args.effectKey, this.attempts = args.attempts;
2324
+ }
2325
+ }
2326
+
2293
2327
  class CascadeLimitError extends invariants.WorkflowError {
2294
2328
  instanceId;
2295
2329
  limit;
@@ -2624,8 +2658,17 @@ async function applyFieldUpdateWhere(op, ctx) {
2624
2658
  fields: entry.of
2625
2659
  }
2626
2660
  });
2627
- if (merge === null || typeof merge != "object" || Array.isArray(merge)) throw new Error(`field.updateWhere value must resolve to an object of fields to merge (target "${op.target.field}")`);
2628
- requireNoReservedMergeKeys(merge, op);
2661
+ if (merge === null || typeof merge != "object" || Array.isArray(merge)) throw new invariants.FieldValueShapeError({
2662
+ entryType: entry._type,
2663
+ entryName: entry.name,
2664
+ mode: "value",
2665
+ issues: [ `field.updateWhere value must resolve to an object of fields to merge (target "${op.target.field}")` ]
2666
+ });
2667
+ requireNoReservedMergeKeys({
2668
+ merge: merge,
2669
+ entry: entry,
2670
+ op: op
2671
+ });
2629
2672
  const matches = await rowMatches({
2630
2673
  where: op.where,
2631
2674
  rows: rows,
@@ -2671,9 +2714,14 @@ function requireMergeableRows(entry, op) {
2671
2714
  if (entry._type !== "array") throw new Error(`${op.type} target ${op.target.scope}:"${op.target.field}" is a ${entry._type} entry — updateWhere merges declared row sub-fields, so it targets \`array\` entries only`);
2672
2715
  }
2673
2716
 
2674
- function requireNoReservedMergeKeys(merge, op) {
2675
- const reserved = [ "_key", "_type" ].filter(key => key in merge);
2676
- if (reserved.length !== 0) throw new Error(`field.updateWhere merge writes the reserved row key${reserved.length === 1 ? "" : "s"} ${reserved.map(k => `"${k}"`).join(", ")} (target "${op.target.field}") — row identity and bookkeeping are engine-stamped, never merged`);
2717
+ function requireNoReservedMergeKeys(args) {
2718
+ const {merge: merge, entry: entry, op: op} = args, reserved = [ "_key", "_type" ].filter(key => key in merge);
2719
+ if (reserved.length !== 0) throw new invariants.FieldValueShapeError({
2720
+ entryType: entry._type,
2721
+ entryName: entry.name,
2722
+ mode: "value",
2723
+ issues: [ `field.updateWhere merge writes the reserved row key${reserved.length === 1 ? "" : "s"} ${reserved.map(k => `"${k}"`).join(", ")} (target "${op.target.field}") — row identity and bookkeeping are engine-stamped, never merged` ]
2724
+ });
2677
2725
  }
2678
2726
 
2679
2727
  function requireArrayValue(entry, op) {
@@ -3441,20 +3489,11 @@ function remediationsFor(diagnosis) {
3441
3489
  }));
3442
3490
  }
3443
3491
 
3444
- async function resolveBindings(args) {
3445
- const resolved = {};
3446
- for (const [key, groq] of Object.entries(args.bindings ?? {})) resolved[key] = await invariants.runGroq({
3447
- groq: groq,
3448
- params: args.params,
3449
- snapshot: args.snapshot
3450
- });
3451
- return {
3452
- ...resolved,
3453
- ...args.staticInput
3454
- };
3492
+ function isClaimExpired(claim, now) {
3493
+ return claim.leaseExpiresAt === void 0 || hasPassed(claim.leaseExpiresAt, now);
3455
3494
  }
3456
3495
 
3457
- const EXECUTION_KINDS = {
3496
+ const EFFECT_RUN_STATUSES = [ "done", "failed", "cancelled" ], EXECUTION_KINDS = {
3458
3497
  interactive: "interactive",
3459
3498
  server: "server",
3460
3499
  cli: "cli",
@@ -4013,7 +4052,7 @@ function coerceToGdr(raw, workflowResource) {
4013
4052
  } : null;
4014
4053
  }
4015
4054
 
4016
- const EFFECT_RUN_STATUSES = [ "done", "failed", "cancelled" ], NonEmpty = invariants.NonEmptyString, UnknownRecord = v__namespace.record(v__namespace.string(), v__namespace.unknown()), PersistedChoiceOptionsSchema = v__namespace.looseObject({
4055
+ const NonEmpty = invariants.NonEmptyString, UnknownRecord = v__namespace.record(v__namespace.string(), v__namespace.unknown()), PersistedChoiceOptionsSchema = v__namespace.looseObject({
4017
4056
  list: v__namespace.array(v__namespace.looseObject({
4018
4057
  title: v__namespace.string(),
4019
4058
  value: v__namespace.union([ v__namespace.string(), v__namespace.number() ])
@@ -4073,7 +4112,7 @@ const OptionalRefTypes = v__namespace.exactOptional(v__namespace.array(v__namesp
4073
4112
  })), v__namespace.looseObject(invariants.tolerantEntries()({
4074
4113
  ...fieldArm("subject", invariants.fieldValueSchemas.subject),
4075
4114
  types: OptionalRefTypes
4076
- })), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("release.ref", invariants.fieldValueSchemas["release.ref"]))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("string", invariants.fieldValueSchemas.string))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("text", invariants.fieldValueSchemas.text))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("number", invariants.fieldValueSchemas.number))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("boolean", invariants.fieldValueSchemas.boolean))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("date", invariants.fieldValueSchemas.date))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("datetime", invariants.fieldValueSchemas.datetime))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("url", invariants.fieldValueSchemas.url))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("actor", invariants.fieldValueSchemas.actor))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("assignee", invariants.fieldValueSchemas.assignee))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("assignees", invariants.fieldValueSchemas.assignees))), v__namespace.looseObject(invariants.tolerantEntries()({
4115
+ })), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("release.ref", invariants.fieldValueSchemas["release.ref"]))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("string", invariants.fieldValueSchemas.string))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("text", invariants.fieldValueSchemas.text))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("number", invariants.fieldValueSchemas.number))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("progress", invariants.fieldValueSchemas.progress))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("boolean", invariants.fieldValueSchemas.boolean))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("date", invariants.fieldValueSchemas.date))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("datetime", invariants.fieldValueSchemas.datetime))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("url", invariants.fieldValueSchemas.url))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("actor", invariants.fieldValueSchemas.actor))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("assignee", invariants.fieldValueSchemas.assignee))), v__namespace.looseObject(invariants.tolerantEntries()(fieldArm("assignees", invariants.fieldValueSchemas.assignees))), v__namespace.looseObject(invariants.tolerantEntries()({
4077
4116
  ...fieldArm("object", v__namespace.union([ v__namespace.null(), UnknownRecord ])),
4078
4117
  fields: v__namespace.array(PersistedFieldShapeSchema)
4079
4118
  })), v__namespace.looseObject(invariants.tolerantEntries()({
@@ -4086,7 +4125,8 @@ const OptionalRefTypes = v__namespace.exactOptional(v__namespace.array(v__namesp
4086
4125
  _type: v__namespace.literal("pendingEffect.claim"),
4087
4126
  claimedAt: invariants.IsoTimestamp,
4088
4127
  claimedBy: invariants.ActorShape,
4089
- leaseExpiresAt: v__namespace.exactOptional(invariants.IsoTimestamp)
4128
+ leaseExpiresAt: v__namespace.exactOptional(invariants.IsoTimestamp),
4129
+ claimToken: v__namespace.exactOptional(NonEmpty)
4090
4130
  }), PendingEffectSchema = invariants.tolerantObject()({
4091
4131
  _key: NonEmpty,
4092
4132
  _type: v__namespace.exactOptional(v__namespace.literal("pendingEffect")),
@@ -4502,6 +4542,10 @@ function collectEntryDocUris(resolvedFieldEntries) {
4502
4542
  return entryDocRefs(resolvedFieldEntries).map(ref => ref.id);
4503
4543
  }
4504
4544
 
4545
+ function isEngineContext(ctx) {
4546
+ return "client" in ctx;
4547
+ }
4548
+
4505
4549
  function loadCallContext({client: client, instanceId: instanceId, options: options}) {
4506
4550
  return loadContext({
4507
4551
  client: client,
@@ -4677,6 +4721,19 @@ async function resolveActivityFieldEntries(args) {
4677
4721
  });
4678
4722
  }
4679
4723
 
4724
+ async function resolveBindings(args) {
4725
+ const resolved = {};
4726
+ for (const [key, groq] of Object.entries(args.bindings ?? {})) resolved[key] = await invariants.runGroq({
4727
+ groq: groq,
4728
+ params: args.params,
4729
+ snapshot: args.snapshot
4730
+ });
4731
+ return {
4732
+ ...resolved,
4733
+ ...args.staticInput
4734
+ };
4735
+ }
4736
+
4680
4737
  const DEFAULT_IDEMPOTENCY_TTL_MS = 1440 * 60 * 1e3;
4681
4738
 
4682
4739
  class RequestAlreadyProcessedError extends Error {
@@ -4851,6 +4908,7 @@ const REQUEST_TAG = {
4851
4908
  fireAction: "workflow.fire-action",
4852
4909
  editField: "workflow.edit-field",
4853
4910
  completeEffect: "workflow.complete-effect",
4911
+ commitEffectOps: "workflow.commit-effect-ops",
4854
4912
  tick: "workflow.tick",
4855
4913
  evaluate: "workflow.evaluate",
4856
4914
  evaluateStart: "workflow.evaluate-start",
@@ -6120,1965 +6178,2134 @@ async function fetchGrants(args) {
6120
6178
  });
6121
6179
  }
6122
6180
 
6123
- const actorCache = /* @__PURE__ */ new WeakMap, grantsCache = /* @__PURE__ */ new WeakMap;
6124
-
6125
- async function resolveAccess(taggedClient, args = {}) {
6126
- const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
6127
- if (requestFn === void 0) throw new invariants.ContractViolationError("workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ uri: '/users/me' })`. Supply a real `@sanity/client` configured with a token (the test bench serves these endpoints per registered token).");
6128
- const grantsPromise = args.grantsFromPath !== void 0 ? cachedGrants({
6129
- client: client,
6130
- requestFn: requestFn,
6131
- resourcePath: args.grantsFromPath
6132
- }) : Promise.resolve(void 0), [actor, grants] = await Promise.all([ cachedActor(client, requestFn), grantsPromise ]);
6133
- if (actor === void 0) throw new invariants.ContractViolationError("workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity — check the token.");
6134
- return {
6135
- actor: actor,
6136
- ...grants !== void 0 ? {
6137
- grants: grants
6138
- } : {}
6139
- };
6140
- }
6141
-
6142
- function cachedActor(client, requestFn) {
6143
- const cached = actorCache.get(client);
6144
- if (cached !== void 0) return cached;
6145
- const pending = fetchActor(requestFn).catch(err => {
6146
- throw actorCache.get(client) === pending && actorCache.delete(client), err;
6147
- });
6148
- return actorCache.set(client, pending), pending;
6149
- }
6150
-
6151
- function grantsForClientPath(taggedClient, resourcePath) {
6152
- const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
6153
- return requestFn === void 0 ? Promise.resolve(void 0) : cachedGrants({
6154
- client: client,
6155
- requestFn: requestFn,
6156
- resourcePath: resourcePath
6181
+ async function advisoryCan({instance: instance, actor: actor, grants: grants}) {
6182
+ if (grants === void 0) return;
6183
+ const can = {};
6184
+ for (const permission of invariants.DOCUMENT_VALUE_PERMISSIONS) can[permission] = await grantsPermissionOn({
6185
+ document: instance,
6186
+ grants: grants,
6187
+ permission: permission,
6188
+ userId: actor.id
6157
6189
  });
6190
+ return can;
6158
6191
  }
6159
6192
 
6160
- function lazyRequest(client) {
6161
- return client.request === void 0 ? void 0 : opts => client.request(opts);
6162
- }
6163
-
6164
- function cachedGrants({client: client, requestFn: requestFn, resourcePath: resourcePath}) {
6165
- let byPath = grantsCache.get(client);
6166
- byPath === void 0 && (byPath = /* @__PURE__ */ new Map, grantsCache.set(client, byPath));
6167
- let cached = byPath.get(resourcePath);
6168
- return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath),
6169
- byPath.set(resourcePath, cached)), cached;
6193
+ function subjectDenialLabels(denied) {
6194
+ return denied.map(d => `${d.permission} on ${d.subject} (${d.resource})`);
6170
6195
  }
6171
6196
 
6172
- async function fetchActor(requestFn) {
6173
- let user;
6174
- try {
6175
- user = await requestFn({
6176
- uri: "/users/me",
6177
- tag: REQUEST_TAG.accessResolveActor
6178
- });
6179
- } catch (err) {
6180
- throw new Error('workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity.', {
6181
- cause: err
6182
- });
6197
+ class ActionDisabledError extends invariants.WorkflowError {
6198
+ reason;
6199
+ activity;
6200
+ action;
6201
+ constructor(args) {
6202
+ super("action-disabled", formatDisabledReason({
6203
+ activity: args.activity,
6204
+ action: args.action,
6205
+ reason: args.reason
6206
+ })), this.name = "ActionDisabledError", this.reason = args.reason, this.activity = args.activity,
6207
+ this.action = args.action;
6183
6208
  }
6184
- if (!user || typeof user.id != "string" || user.id.length === 0) return;
6185
- const roleNames = user.roles?.map(r => r.name).filter(n => !!n) ?? [];
6186
- return {
6187
- kind: "person",
6188
- id: user.id,
6189
- ...roleNames.length > 0 ? {
6190
- roles: roleNames
6191
- } : {}
6192
- };
6193
6209
  }
6194
6210
 
6195
- async function fetchGrantsCached(requestFn, resourcePath) {
6196
- try {
6197
- return await fetchGrants({
6198
- client: {
6199
- request: requestFn
6200
- },
6201
- resourcePath: resourcePath
6202
- });
6203
- } catch (err) {
6204
- console.warn(`workflow: failed to fetch grants from "${resourcePath}"; advisory permission reads that depend on them are skipped — a rendered $can stays undefined (conditions referencing it fail closed) and the subject-write forecast omits this resource. The lake still enforces writes. Original error: ${invariants.errorMessage(err)}`);
6205
- return;
6211
+ class StartNotAllowedError extends invariants.WorkflowError {
6212
+ definition;
6213
+ insight;
6214
+ constructor(args) {
6215
+ super("start-not-allowed", `startInstance refused: start.allowed on definition "${args.definition}" evaluated ` + (args.insight.outcome === "unevaluable" ? `GROQ null ("can't decide" — fail-closed)` : "false") + " for the supplied initialFields. Pre-flight the verdict with evaluateStart."),
6216
+ this.name = "StartNotAllowedError", this.definition = args.definition, this.insight = args.insight;
6206
6217
  }
6207
6218
  }
6208
6219
 
6209
- async function buildFieldInsights({sites: sites, snapshot: snapshot}) {
6210
- const insights = [];
6211
- for (const field of fieldsReadAcross(sites)) {
6212
- const involved = sites.filter(entry => readsField(entry.insight, field));
6213
- insights.push({
6214
- field: field,
6215
- reads: groqConditionDescribe.dedupeReads(involved.flatMap(entry => fieldReads(entry.insight, field))),
6216
- involvedIn: involved.map(entry => entry.site),
6217
- proposals: await verifyProposals({
6218
- field: field,
6219
- involved: involved,
6220
- snapshot: snapshot
6221
- })
6222
- });
6223
- }
6224
- return insights;
6220
+ function actionRendering(action) {
6221
+ const kind = action.disabledReason?.kind;
6222
+ return kind === "filter-failed" ? "absent" : action.triggered === !0 || kind === "cascade-fired" ? "automation" : "button";
6225
6223
  }
6226
6224
 
6227
- function fieldsReadAcross(sites) {
6228
- const names = sites.flatMap(entry => entry.insight.analysis.reads).filter(read => read.variable === "fields").map(read => read.path[0]).filter(head => typeof head == "string");
6229
- return [ ...new Set(names) ];
6230
- }
6225
+ const disabledReasonDetail = {
6226
+ "filter-failed": r => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
6227
+ "cascade-fired": r => `the action is cascade-fired (when: ${JSON.stringify(r.when)}) — the engine fires it on truth; it cannot be invoked via fireAction`,
6228
+ "activity-not-active": r => `activity status is "${r.status}"`,
6229
+ "stage-terminal": r => `stage "${r.stage}" is terminal`,
6230
+ "instance-completed": r => `instance completed at ${r.completedAt}`,
6231
+ "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
6232
+ "requirements-unmet": r => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`,
6233
+ "subject-permission-denied": r => `missing subject permission(s): ${subjectDenialLabels(r.denied).join(", ")}`
6234
+ };
6231
6235
 
6232
- function fieldReads(insight, field) {
6233
- return insight.analysis.reads.filter(read => read.variable === "fields" && read.path[0] === field);
6236
+ function actionDisabledDetail(reason) {
6237
+ return disabledReasonDetail[reason.kind](reason);
6234
6238
  }
6235
6239
 
6236
- function readsField(insight, field) {
6237
- return fieldReads(insight, field).length > 0;
6240
+ function formatDisabledReason({activity: activity, action: action, reason: reason}) {
6241
+ return `Action "${activity}:${action}" is not allowed: ${actionDisabledDetail(reason)}`;
6238
6242
  }
6239
6243
 
6240
- async function verifyProposals({field: field, involved: involved, snapshot: snapshot}) {
6241
- const proposals = [];
6242
- for (const assign of candidateAssignments(involved, field)) {
6243
- const consequences = [];
6244
- for (const entry of involved) {
6245
- const result = await groqConditionDescribe.whatIfCondition({
6246
- condition: entry.condition,
6247
- dataset: snapshot.docs,
6248
- params: entry.params,
6249
- assign: assign
6250
- });
6251
- result.changed && consequences.push({
6252
- site: entry.site,
6253
- before: result.before,
6254
- after: result.after
6255
- });
6256
- }
6257
- consequences.length > 0 && proposals.push({
6258
- assign: assign,
6259
- consequences: consequences
6260
- });
6244
+ class EditFieldDeniedError extends invariants.WorkflowError {
6245
+ reason;
6246
+ target;
6247
+ constructor(args) {
6248
+ super("edit-field-denied", formatEditDisabledReason(args.target, args.reason)),
6249
+ this.name = "EditFieldDeniedError", this.reason = args.reason, this.target = args.target;
6261
6250
  }
6262
- return proposals;
6263
6251
  }
6264
6252
 
6265
- function candidateAssignments(involved, field) {
6266
- const candidates = involved.flatMap(entry => entry.insight.blockedBy).flatMap(atom => atom.requirement !== void 0 ? [ atom.requirement ] : []).filter(requirement => requirement.target.variable === "fields").filter(requirement => requirement.target.path[0] === field).flatMap(assignmentFor).filter(fabricatable);
6267
- return groqConditionDescribe.dedupeBy(candidates, assignment => JSON.stringify([ assignment.target.path, assignment.value ]));
6268
- }
6253
+ const editDisabledReasonDetail = {
6254
+ "not-editable": () => "field is not declared editable",
6255
+ "instance-completed": r => `instance completed at ${r.completedAt}`,
6256
+ "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
6257
+ "edit-window-closed": r => `edit window closed (${r.detail})`,
6258
+ "editor-not-permitted": r => `editor not permitted (${r.predicate})`
6259
+ };
6269
6260
 
6270
- function assignmentFor(requirement) {
6271
- switch (requirement.kind) {
6272
- case "equals":
6273
- return [ {
6274
- target: requirement.target,
6275
- value: requirement.value
6276
- } ];
6261
+ function formatEditDisabledReason(target, reason) {
6262
+ const detail = editDisabledReasonDetail[reason.kind](reason), where = target.activity !== void 0 ? `${target.activity}.${target.field}` : target.field;
6263
+ return `Field "${target.scope}:${where}" is not editable: ${detail}`;
6264
+ }
6277
6265
 
6278
- case "truthy":
6279
- return [ {
6280
- target: requirement.target,
6281
- value: !0
6282
- } ];
6283
-
6284
- case "falsy":
6285
- return [ {
6286
- target: requirement.target,
6287
- value: !1
6288
- } ];
6289
-
6290
- case "differs":
6291
- case "defined":
6292
- case "undefined":
6293
- case "compares":
6294
- return [];
6295
- }
6296
- }
6297
-
6298
- function fabricatable(assignment) {
6299
- return assignment.target.path.every(segment => typeof segment != "number" || segment <= groqConditionDescribe.MAX_COUNTERFACTUAL_INDEX);
6266
+ async function fireAction(args) {
6267
+ const {client: client, instanceId: instanceId, activity: activity, action: action, params: params, requestRecord: requestRecord, options: options} = args;
6268
+ return retryOnRevisionConflict({
6269
+ client: client,
6270
+ instanceId: instanceId,
6271
+ options: options,
6272
+ commit: ctx => commitAction({
6273
+ ctx: ctx,
6274
+ activityName: activity,
6275
+ actionName: action,
6276
+ callerParams: params,
6277
+ requestRecord: requestRecord,
6278
+ options: options
6279
+ }),
6280
+ onExhausted: () => new ConcurrentFireActionError({
6281
+ instanceId: instanceId,
6282
+ activity: activity,
6283
+ action: action,
6284
+ attempts: CONCURRENT_COMMIT_MAX_ATTEMPTS
6285
+ })
6286
+ });
6300
6287
  }
6301
6288
 
6302
- async function subjectResourceGrants(args) {
6303
- const {clientForGdr: clientForGdr, instance: instance} = args, clients = /* @__PURE__ */ new Map;
6304
- for (const {parsed: parsed, resource: resource} of foreignSubjectRefs(instance)) {
6305
- const key = invariants.resourceGdr(resource);
6306
- clients.has(key) || clients.set(key, {
6307
- resource: resource,
6308
- client: clientForGdr(parsed)
6289
+ async function resolveActionCommit({ctx: ctx, activityName: activityName, actionName: actionName, callerParams: callerParams, options: options}) {
6290
+ const actor = options?.actor, {stage: stage, activity: activity} = findActivityInCurrentStage(ctx, activityName), action = (activity.actions ?? []).find(a => a.name === actionName);
6291
+ if (action === void 0) throw new invariants.ContractViolationError(`Action "${actionName}" not declared on activity "${activityName}"`);
6292
+ if (action.when !== void 0) throw new ActionDisabledError({
6293
+ activity: activityName,
6294
+ action: actionName,
6295
+ reason: {
6296
+ kind: "cascade-fired",
6297
+ when: action.when
6298
+ }
6299
+ });
6300
+ if (action.filter !== void 0) {
6301
+ const can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan({
6302
+ instance: ctx.instance,
6303
+ actor: actor,
6304
+ grants: options.grants
6305
+ }) : void 0;
6306
+ if (!await ctxEvaluateCondition({
6307
+ ctx: ctx,
6308
+ condition: action.filter,
6309
+ opts: {
6310
+ activityName: activityName,
6311
+ ...actor !== void 0 ? {
6312
+ actor: actor
6313
+ } : {},
6314
+ ...can !== void 0 ? {
6315
+ vars: {
6316
+ can: can
6317
+ }
6318
+ } : {}
6319
+ }
6320
+ })) throw new ActionDisabledError({
6321
+ activity: activityName,
6322
+ action: actionName,
6323
+ reason: {
6324
+ kind: "filter-failed",
6325
+ filter: action.filter,
6326
+ detail: "commit re-check"
6327
+ }
6309
6328
  });
6310
6329
  }
6311
- const resolved = await Promise.all([ ...clients.entries() ].map(async ([key, entry]) => {
6312
- const path = aclPathForResource(entry.resource);
6313
- if (path === void 0) return;
6314
- const grants = await grantsForClientPath(entry.client, path);
6315
- return grants === void 0 ? void 0 : [ key, grants ];
6316
- }));
6317
- return new Map(resolved.filter(entry => entry !== void 0));
6318
- }
6319
-
6320
- async function evaluateInstance(args) {
6321
- const {client: client, tag: tag, workflowResource: workflowResource, instanceId: instanceId, resourceClients: resourceClients} = args, now = (args.clock ?? wallClock)();
6322
- invariants.validateTag(tag);
6323
- const {actor: actor, grants: grants} = await resolveAccess(client, {
6324
- ...args.grantsFromPath !== void 0 ? {
6325
- grantsFromPath: args.grantsFromPath
6326
- } : {}
6327
- }), instance = await reload({
6328
- client: client,
6329
- instanceId: instanceId,
6330
- tag: tag
6331
- }), definition = invariants.parseDefinitionSnapshot(instance), clientForGdr = buildClientForGdr({
6332
- client: client,
6333
- workflowResource: workflowResource,
6334
- resourceClients: resourceClients
6335
- }), snapshot = await hydrateSnapshot({
6336
- client: client,
6337
- clientForGdr: clientForGdr,
6338
- instance: instance
6339
- }), guards = await verdictGuardsForInstance(client, instance._id), resourceGrants = await subjectResourceGrants({
6340
- clientForGdr: clientForGdr,
6341
- instance: instance
6330
+ const entry = findCurrentActivities(ctx.instance).find(t => t.name === activityName);
6331
+ if (entry === void 0 || entry.status !== "active") throw notActiveAtCommit({
6332
+ activityName: activityName,
6333
+ actionName: actionName,
6334
+ status: entry?.status
6342
6335
  });
6343
- return evaluateFromSnapshot({
6344
- instance: instance,
6345
- definition: definition,
6346
- actor: actor,
6347
- snapshot: snapshot,
6348
- guards: guards,
6349
- now: now,
6350
- resourceGrants: resourceGrants,
6351
- ...grants !== void 0 ? {
6352
- grants: grants
6353
- } : {}
6336
+ const params = validateActionParams({
6337
+ action: action,
6338
+ activityName: activityName,
6339
+ callerParams: callerParams
6354
6340
  });
6355
- }
6356
-
6357
- function memoizedByName(render) {
6358
- const rendered = /* @__PURE__ */ new Map;
6359
- return activityName => {
6360
- const hit = rendered.get(activityName);
6361
- if (hit !== void 0) return hit;
6362
- const scope = render(activityName);
6363
- return rendered.set(activityName, scope), scope;
6341
+ return {
6342
+ stage: stage,
6343
+ activity: activity,
6344
+ action: action,
6345
+ params: params
6364
6346
  };
6365
6347
  }
6366
6348
 
6367
- function currentStageOf(instance, definition) {
6368
- try {
6369
- return findStage(definition, instance.currentStage);
6370
- } catch (err) {
6371
- throw new Error(`Instance "${instance._id}" currentStage "${instance.currentStage}" not in definition`, {
6372
- cause: err
6373
- });
6374
- }
6349
+ function notActiveAtCommit(args) {
6350
+ const {activityName: activityName, actionName: actionName, status: status} = args;
6351
+ return status !== void 0 && invariants.isTerminalActivityStatus(status) ? new ActionDisabledError({
6352
+ activity: activityName,
6353
+ action: actionName,
6354
+ reason: {
6355
+ kind: "activity-not-active",
6356
+ status: status
6357
+ }
6358
+ }) : new Error(`Activity "${activityName}" must be active to fire action "${actionName}"; status is ${status ?? "missing"}`);
6375
6359
  }
6376
6360
 
6377
- async function explainSite(args) {
6378
- const {site: site, condition: condition, params: params, snapshot: snapshot, sites: sites} = args, insight = await groqConditionDescribe.explainCondition({
6379
- condition: condition,
6380
- dataset: snapshot.docs,
6381
- params: params
6361
+ async function applyActionFire({ctx: ctx, mutation: mutation, activity: activity, action: action, params: params, actor: actor, triggered: triggered}) {
6362
+ mutation.history.push({
6363
+ _key: randomKey(),
6364
+ _type: "actionFired",
6365
+ at: ctx.now,
6366
+ stage: mutation.currentStage,
6367
+ activity: activity.name,
6368
+ action: action.name,
6369
+ ...actor !== void 0 ? {
6370
+ actor: actor,
6371
+ driverKind: invariants.driverKind(actor)
6372
+ } : {},
6373
+ ...triggered ? {
6374
+ triggered: !0
6375
+ } : {}
6382
6376
  });
6383
- return sites.push({
6384
- site: site,
6385
- condition: condition,
6377
+ const ranOps = await runOps({
6378
+ ops: action.ops,
6379
+ mutation: mutation,
6380
+ stage: mutation.currentStage,
6381
+ origin: {
6382
+ activity: activity.name,
6383
+ action: action.name
6384
+ },
6386
6385
  params: params,
6387
- insight: insight
6388
- }), insight;
6389
- }
6390
-
6391
- async function evaluateFromSnapshot(args) {
6392
- const {instance: instance, definition: definition, actor: actor, grants: grants, snapshot: snapshot} = args, now = args.now ?? wallClock(), stage = currentStageOf(instance, definition), autonomy = autonomyOf(definition), stageAutonomy2 = stageAutonomyOf(autonomy, stage.name), scopeSource = {
6393
- instance: instance,
6394
- definition: definition,
6395
- snapshot: snapshot,
6396
- now: now
6397
- }, can = await advisoryCan({
6398
- instance: instance,
6399
6386
  actor: actor,
6400
- grants: grants
6401
- }), scope = await renderConditionScope(scopeSource, {
6387
+ self: invariants.selfGdr(ctx.instance),
6388
+ now: ctx.now,
6389
+ snapshot: ctx.snapshot,
6390
+ refSurface: ctx.refSurface
6391
+ });
6392
+ if (await queueEffects({
6393
+ ctx: ctx,
6394
+ mutation: mutation,
6395
+ effects: action.effects,
6396
+ origin: {
6397
+ kind: "action",
6398
+ name: action.name
6399
+ },
6402
6400
  actor: actor,
6403
- vars: {
6404
- can: can
6401
+ opts: {
6402
+ callerParams: params,
6403
+ activityName: activity.name
6405
6404
  }
6406
- }), scopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
6405
+ }), action.spawn !== void 0) {
6406
+ if (!isEngineContext(ctx)) throw new invariants.ContractViolationError(`Action "${action.name}" declares spawn, which requires a lake-capable engine context`);
6407
+ await spawnSubworkflows({
6408
+ ctx: ctx,
6409
+ mutation: mutation,
6410
+ activity: activity,
6411
+ action: action,
6412
+ sub: action.spawn,
6413
+ actor: actor
6414
+ });
6415
+ }
6416
+ return {
6417
+ ranOps: ranOps
6418
+ };
6419
+ }
6420
+
6421
+ async function commitAction({ctx: ctx, activityName: activityName, actionName: actionName, callerParams: callerParams, requestRecord: requestRecord, options: options}) {
6422
+ assertRequestUnprocessed({
6423
+ instance: ctx.instance,
6424
+ record: requestRecord,
6425
+ now: ctx.now
6426
+ });
6427
+ const actor = options?.actor, {stage: stage, activity: activity, action: action, params: params} = await resolveActionCommit({
6428
+ ctx: ctx,
6407
6429
  activityName: activityName,
6408
- actor: actor,
6409
- vars: {
6410
- can: can
6411
- }
6412
- })), cascadeScopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
6413
- activityName: activityName
6414
- })), currentActivityEntries = findOpenStageEntry(instance)?.activities ?? [], guardDenial = await instanceGuardReason({
6415
- instance: instance,
6416
- actor: actor,
6417
- guards: args.guards
6418
- }), subjectDenials = await forecastSubjectDenials({
6419
- instance: instance,
6420
- actor: actor,
6421
- snapshot: snapshot,
6422
- resourceGrants: args.resourceGrants
6423
- }), subjectPermissionReason = subjectDenials.length > 0 ? {
6424
- kind: "subject-permission-denied",
6425
- denied: subjectDenials
6426
- } : void 0, sites = [], activityEvaluations = [];
6427
- for (const activity of stage.activities ?? []) activityEvaluations.push(await evaluateActivity({
6428
- activity: activity,
6429
- statusEntry: currentActivityEntries.find(t => t.name === activity.name),
6430
- instance: instance,
6431
- snapshot: snapshot,
6432
- activityScope: await scopeForActivity(activity.name),
6433
- cascadeActivityScope: () => cascadeScopeForActivity(activity.name),
6434
- stageHasExits: !isTerminalStage(stage),
6435
- guardDenial: guardDenial,
6436
- subjectPermissionReason: subjectPermissionReason,
6437
- sites: sites,
6438
- autonomy: activityAutonomyOf(stageAutonomy2, activity.name)
6439
- }));
6440
- const cascadeParams = await renderConditionScope(scopeSource), transitionEvaluations = [];
6441
- for (const transition of stage.transitions ?? []) {
6442
- const insight = await explainSite({
6443
- site: {
6444
- kind: "transition",
6445
- transition: transition.name
6446
- },
6447
- condition: transition.when,
6448
- params: cascadeParams,
6449
- snapshot: snapshot,
6450
- sites: sites
6451
- });
6452
- transitionEvaluations.push({
6453
- transition: transition,
6454
- whenSatisfied: insight.outcome === "satisfied",
6455
- unevaluable: insight.outcome === "unevaluable",
6456
- insight: insight
6457
- });
6458
- }
6459
- const currentStage = {
6460
- stage: stage,
6461
- activities: activityEvaluations,
6462
- transitions: transitionEvaluations,
6463
- autonomy: stageAutonomy2
6464
- }, pendingOnYou = activityEvaluations.filter(t => t.pendingOnActor), canInteract = activityEvaluations.some(t => t.actions.some(a => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableFields = await evaluateEditableFields({
6465
- instance: instance,
6466
- definition: definition,
6467
- stage: stage,
6468
- snapshot: snapshot,
6469
- scope: scope,
6470
- scopeForActivity: scopeForActivity,
6471
- guardDenial: editGuardDenial,
6472
- sites: sites
6430
+ actionName: actionName,
6431
+ callerParams: callerParams,
6432
+ options: options
6433
+ }), mutation = startMutation(ctx.instance);
6434
+ recordProcessedRequest({
6435
+ mutation: mutation,
6436
+ record: requestRecord,
6437
+ now: ctx.now
6473
6438
  });
6474
- return {
6475
- instance: instance,
6476
- definition: definition,
6439
+ const mutEntry = requireMutationActivityEntry(mutation, activityName), statusBefore = mutEntry.status, {ranOps: ranOps} = await applyActionFire({
6440
+ ctx: ctx,
6441
+ mutation: mutation,
6442
+ activity: activity,
6443
+ action: action,
6444
+ params: params,
6477
6445
  actor: actor,
6478
- currentStage: currentStage,
6479
- pendingOnYou: pendingOnYou,
6480
- canInteract: canInteract,
6481
- editableFields: editableFields,
6482
- fieldInsights: await buildFieldInsights({
6483
- sites: sites,
6484
- snapshot: snapshot
6485
- }),
6486
- autonomy: autonomy
6446
+ triggered: !1
6447
+ }), newStatus = mutEntry.status !== statusBefore ? mutEntry.status : void 0;
6448
+ return await persistThenMaybeRefresh({
6449
+ ctx: ctx,
6450
+ mutation: mutation,
6451
+ stageName: stage.name,
6452
+ didChangeState: ranOps.some(isFieldOp)
6453
+ }), {
6454
+ fired: !0,
6455
+ activity: activityName,
6456
+ action: actionName,
6457
+ ...newStatus !== void 0 ? {
6458
+ newStatus: newStatus
6459
+ } : {},
6460
+ ...ranOps.length > 0 ? {
6461
+ ranOps: ranOps
6462
+ } : {}
6487
6463
  };
6488
6464
  }
6489
6465
 
6490
- const AUTONOMY_CACHE = /* @__PURE__ */ new WeakMap;
6491
-
6492
- function autonomyOf(definition) {
6493
- const hit = AUTONOMY_CACHE.get(definition);
6494
- if (hit !== void 0) return hit;
6495
- const derived = deriveWorkflowAutonomy(definition);
6496
- return AUTONOMY_CACHE.set(definition, derived), derived;
6466
+ async function runTriggeredActions({ctx: ctx, mutation: mutation, stage: stage}) {
6467
+ let fires = 0, ranFieldOps = !1;
6468
+ const liveCtx = liveViewContext(ctx, mutation);
6469
+ let firedThisPass = !0;
6470
+ for (;firedThisPass; ) {
6471
+ firedThisPass = !1;
6472
+ for (const activity of stage.activities ?? []) {
6473
+ const fired = await runActivityTriggers({
6474
+ ctx: liveCtx,
6475
+ mutation: mutation,
6476
+ activity: activity
6477
+ });
6478
+ fires += fired.fires, ranFieldOps = ranFieldOps || fired.ranFieldOps, fired.fires > 0 && (firedThisPass = !0);
6479
+ }
6480
+ }
6481
+ return {
6482
+ fires: fires,
6483
+ ranFieldOps: ranFieldOps
6484
+ };
6497
6485
  }
6498
6486
 
6499
- async function evaluateEditableFields(args) {
6500
- const {instance: instance, definition: definition, stage: stage, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, guardDenial: guardDenial, sites: sites} = args, fields = [];
6501
- for (const site of editableFieldsInStage(definition, stage)) {
6502
- const window = fieldWindowOpen(instance, site), insight = await editPredicateInsight({
6503
- site: site,
6504
- snapshot: snapshot,
6505
- scope: scope,
6506
- scopeForActivity: scopeForActivity,
6507
- sites: sites
6508
- }), predicateSatisfied = insight === void 0 || insight.outcome === "satisfied", reason = editDisabledReason({
6509
- effective: site.effective,
6510
- instance: instance,
6511
- window: window,
6512
- guardDenial: guardDenial,
6513
- predicateSatisfied: predicateSatisfied
6514
- }), value = readFieldValue(instance, site);
6515
- fields.push({
6516
- scope: site.scope,
6517
- ...site.activity !== void 0 ? {
6518
- activity: site.activity
6519
- } : {},
6520
- name: site.name,
6521
- type: site.type,
6522
- ...site.title !== void 0 ? {
6523
- title: site.title
6524
- } : {},
6525
- ...site.validation !== void 0 ? {
6526
- validation: site.validation
6527
- } : {},
6528
- value: value,
6529
- editable: reason === void 0,
6530
- ...reason !== void 0 ? {
6531
- disabledReason: reason
6532
- } : {},
6533
- ...fieldProvenance(instance, site.ref),
6534
- ...insight !== void 0 ? {
6535
- insight: insight
6536
- } : {}
6487
+ async function runActivityTriggers({ctx: ctx, mutation: mutation, activity: activity}) {
6488
+ let fires = 0, ranFieldOps = !1;
6489
+ for (const action of activity.actions ?? []) {
6490
+ if (!invariants.isCascadeFired(action)) continue;
6491
+ const entry = currentActivities(mutation).find(e => e.name === activity.name);
6492
+ if (entry === void 0 || entry.status !== "active") break;
6493
+ if ((entry.firedActions ?? []).includes(action.name) || !await triggerIsLive({
6494
+ ctx: ctx,
6495
+ activity: activity,
6496
+ action: action
6497
+ })) continue;
6498
+ const result = await fireTriggeredAction({
6499
+ ctx: ctx,
6500
+ mutation: mutation,
6501
+ activity: activity,
6502
+ action: action,
6503
+ entry: entry
6537
6504
  });
6505
+ if (fires++, ranFieldOps = ranFieldOps || result.ranFieldOps, invariants.isTerminalActivityStatus(entry.status)) break;
6538
6506
  }
6539
- return fields;
6507
+ return {
6508
+ fires: fires,
6509
+ ranFieldOps: ranFieldOps
6510
+ };
6540
6511
  }
6541
6512
 
6542
- async function editPredicateInsight(args) {
6543
- const {site: site, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, sites: sites} = args;
6544
- if (typeof site.effective != "string") return;
6545
- const params = site.scope === "activity" && site.activity !== void 0 ? await scopeForActivity(site.activity) : scope;
6546
- return explainSite({
6547
- site: {
6548
- kind: "editable-field",
6549
- scope: site.scope,
6550
- name: site.name,
6551
- ...site.activity !== void 0 ? {
6552
- activity: site.activity
6553
- } : {}
6554
- },
6555
- condition: site.effective,
6556
- params: params,
6557
- snapshot: snapshot,
6558
- sites: sites
6559
- });
6513
+ async function triggerIsLive({ctx: ctx, activity: activity, action: action}) {
6514
+ if (!tokenMayExecute({
6515
+ actor: ctx.actor,
6516
+ action: action,
6517
+ ctx: ctx
6518
+ })) return !1;
6519
+ const scope = {
6520
+ activityName: activity.name
6521
+ };
6522
+ return action.filter !== void 0 && await ctxEvaluateConditionOutcome({
6523
+ ctx: ctx,
6524
+ condition: action.filter,
6525
+ opts: scope
6526
+ }) !== "satisfied" ? !1 : await ctxEvaluateConditionOutcome({
6527
+ ctx: ctx,
6528
+ condition: action.when,
6529
+ opts: scope
6530
+ }) === "satisfied";
6560
6531
  }
6561
6532
 
6562
- async function advisoryCan({instance: instance, actor: actor, grants: grants}) {
6563
- if (grants === void 0) return;
6564
- const can = {};
6565
- for (const permission of invariants.DOCUMENT_VALUE_PERMISSIONS) can[permission] = await grantsPermissionOn({
6566
- document: instance,
6567
- grants: grants,
6568
- permission: permission,
6569
- userId: actor.id
6533
+ function tokenMayExecute({actor: actor, action: action, ctx: ctx}) {
6534
+ const roles = action.roles;
6535
+ return roles === void 0 || roles.length === 0 ? !0 : actor === void 0 ? !1 : roles.some(required => invariants.actorFulfillsRole({
6536
+ actorRoles: actor.roles,
6537
+ required: required,
6538
+ aliases: ctx.definition.roleAliases
6539
+ }));
6540
+ }
6541
+
6542
+ async function fireTriggeredAction({ctx: ctx, mutation: mutation, activity: activity, action: action, entry: entry}) {
6543
+ entry.firedActions = [ ...entry.firedActions ?? [], action.name ];
6544
+ const {ranOps: ranOps} = await applyActionFire({
6545
+ ctx: ctx,
6546
+ mutation: mutation,
6547
+ activity: activity,
6548
+ action: action,
6549
+ params: {},
6550
+ actor: ctx.actor,
6551
+ triggered: !0
6570
6552
  });
6571
- return can;
6553
+ return {
6554
+ ranFieldOps: ranOps.some(isFieldOp)
6555
+ };
6572
6556
  }
6573
6557
 
6574
- async function forecastSubjectDenials(args) {
6575
- const {instance: instance, actor: actor, snapshot: snapshot, resourceGrants: resourceGrants} = args;
6576
- if (resourceGrants === void 0 || resourceGrants.size === 0) return [];
6577
- const denials = [], seen = /* @__PURE__ */ new Set;
6578
- for (const {ref: ref, parsed: parsed, resource: resource} of foreignSubjectRefs(instance)) {
6579
- if (seen.has(ref.id)) continue;
6580
- seen.add(ref.id);
6581
- const grants = resourceGrants.get(invariants.resourceGdr(resource));
6582
- if (grants === void 0) continue;
6583
- const doc = snapshot.docs.find(d => d._id === ref.id);
6584
- doc !== void 0 && await subjectUpdateAllowed({
6585
- doc: doc,
6586
- parsed: parsed,
6587
- grants: grants,
6588
- actorId: actor.id
6589
- }) === !1 && denials.push({
6590
- subject: ref.id,
6591
- resource: invariants.resourceGdr(resource),
6592
- permission: "update"
6593
- });
6594
- }
6595
- return denials;
6596
- }
6597
-
6598
- async function subjectUpdateAllowed(args) {
6599
- const {doc: doc, parsed: parsed, grants: grants, actorId: actorId} = args;
6600
- try {
6601
- return await grantsPermissionOn({
6602
- document: {
6603
- ...doc,
6604
- _id: parsed.documentId
6605
- },
6606
- grants: grants,
6607
- permission: "update",
6608
- userId: actorId
6609
- });
6610
- } catch (err) {
6611
- console.warn(`workflow: subject-write forecast skipped for "${doc._id}" — evaluating that resource's grants failed (the lake still enforces the write). Original error: ${invariants.errorMessage(err)}`);
6612
- return;
6613
- }
6614
- }
6615
-
6616
- async function evaluateActivity(args) {
6617
- const {activity: activity, statusEntry: statusEntry, instance: instance, snapshot: snapshot, activityScope: activityScope, cascadeActivityScope: cascadeActivityScope, stageHasExits: stageHasExits, guardDenial: guardDenial, subjectPermissionReason: subjectPermissionReason, sites: sites, autonomy: autonomy} = args, status = statusEntry?.status ?? "skipped", assigned = activityScope.assigned === !0, {unmetRequirements: unmetRequirements, ...conditionInsights} = await explainActivityConditions({
6618
- activity: activity,
6619
- activityScope: activityScope,
6620
- cascadeActivityScope: cascadeActivityScope,
6621
- snapshot: snapshot,
6622
- sites: sites
6623
- }), requirementsReason = unmetRequirements.length > 0 ? {
6624
- kind: "requirements-unmet",
6625
- unmetRequirements: unmetRequirements
6626
- } : void 0, actions = [];
6627
- for (const action of activity.actions ?? []) actions.push(await evaluateAction({
6628
- action: action,
6629
- activityName: activity.name,
6630
- status: status,
6558
+ async function primeInitialStage({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry}) {
6559
+ const instance = await getInstanceDocument(client, instanceId);
6560
+ if (!instance || instance.stages.length > 0) return;
6561
+ const definition = invariants.parseDefinitionSnapshot(instance), stage = definition.stages.find(s => s.name === instance.currentStage);
6562
+ if (stage === void 0) return;
6563
+ const ctx = await buildEngineContext({
6564
+ client: client,
6565
+ clientForGdr: clientForGdr,
6566
+ refSurface: refSurface,
6631
6567
  instance: instance,
6632
- snapshot: snapshot,
6633
- activityScope: activityScope,
6634
- cascadeActivityScope: cascadeActivityScope,
6635
- stageHasExits: stageHasExits,
6636
- guardDenial: guardDenial,
6637
- subjectPermissionReason: subjectPermissionReason,
6638
- requirementsReason: requirementsReason,
6639
- sites: sites
6640
- }));
6641
- return {
6642
- activity: activity,
6643
- status: status,
6644
- kind: invariants.deriveActivityKind(activity),
6645
- classification: invariants.deriveExecutorClassification(activity),
6646
- autonomy: autonomy,
6647
- pendingOnActor: status === "active" && assigned,
6648
- scopedOut: isFilterScopedOut({
6649
- status: status,
6650
- startedAt: statusEntry?.startedAt
6651
- }),
6652
- ...unmetRequirements.length > 0 ? {
6653
- unmetRequirements: unmetRequirements
6568
+ definition: definition,
6569
+ ...clock ? {
6570
+ clock: clock
6654
6571
  } : {},
6655
- ...conditionInsights,
6656
- actions: actions
6572
+ ...actor ? {
6573
+ actor: actor
6574
+ } : {},
6575
+ ...executionContext ? {
6576
+ executionContext: executionContext
6577
+ } : {},
6578
+ ...telemetry ? {
6579
+ telemetry: telemetry
6580
+ } : {}
6581
+ }), now = ctx.now, discards = [], initialStageEntry = {
6582
+ _key: randomKey(),
6583
+ name: stage.name,
6584
+ enteredAt: now,
6585
+ fields: await resolveStageFieldEntries({
6586
+ client: client,
6587
+ instance: instance,
6588
+ stage: stage,
6589
+ now: now,
6590
+ refSurface: refSurface,
6591
+ recordDiscard: recordFieldDiscards({
6592
+ target: discards,
6593
+ scope: "stage",
6594
+ at: now
6595
+ })
6596
+ }),
6597
+ activities: []
6598
+ }, primedCtx = {
6599
+ ...ctx,
6600
+ instance: {
6601
+ ...instance,
6602
+ stages: [ initialStageEntry ]
6603
+ }
6657
6604
  };
6658
- }
6659
-
6660
- async function explainActivityConditions({activity: activity, activityScope: activityScope, cascadeActivityScope: cascadeActivityScope, snapshot: snapshot, sites: sites}) {
6661
- const explainAt = (site, condition) => explainSite({
6662
- site: site,
6663
- condition: condition,
6664
- params: activityScope,
6665
- snapshot: snapshot,
6666
- sites: sites
6667
- }), requirementEntries = [];
6668
- for (const [name, condition] of Object.entries(activity.requirements ?? {})) requirementEntries.push([ name, await explainAt({
6669
- kind: "requirement",
6670
- activity: activity.name,
6671
- requirement: name
6672
- }, condition) ]);
6673
- const requirementInsights = Object.fromEntries(requirementEntries), unmetRequirements = requirementEntries.filter(([, insight]) => insight.outcome !== "satisfied").map(([name]) => name), filterInsight = activity.filter !== void 0 ? await explainSite({
6674
- site: {
6675
- kind: "activity-filter",
6676
- activity: activity.name
6677
- },
6678
- condition: activity.filter,
6679
- params: await cascadeActivityScope(),
6680
- snapshot: snapshot,
6681
- sites: sites
6682
- }) : void 0;
6683
- return {
6684
- unmetRequirements: unmetRequirements,
6685
- ...activity.requirements !== void 0 ? {
6686
- requirementInsights: requirementInsights
6605
+ initialStageEntry.activities = await buildStageActivities({
6606
+ ctx: primedCtx,
6607
+ stage: stage,
6608
+ recordDiscard: recordFieldDiscards({
6609
+ target: discards,
6610
+ scope: "activity",
6611
+ at: now
6612
+ })
6613
+ });
6614
+ const terminal = isTerminalStage(stage), committed = await client.patch(instance._id).set({
6615
+ stages: [ initialStageEntry ],
6616
+ lastChangedAt: now,
6617
+ ...terminal ? {
6618
+ completedAt: now
6687
6619
  } : {},
6688
- ...filterInsight !== void 0 ? {
6689
- filterInsight: filterInsight
6620
+ ...discards.length > 0 ? {
6621
+ history: [ ...instance.history, ...stampHistoryEntries(discards, ctx.executionContext) ]
6690
6622
  } : {}
6623
+ }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
6624
+ await deployOrRollback({
6625
+ client: client,
6626
+ instanceId: instance._id,
6627
+ committedRev: committed._rev,
6628
+ restore: {
6629
+ stages: instance.stages,
6630
+ history: instance.history
6631
+ },
6632
+ ...terminal ? {
6633
+ unset: [ "completedAt" ]
6634
+ } : {},
6635
+ reversible: !0,
6636
+ deploy: () => deployStageGuards({
6637
+ client: client,
6638
+ clientForGdr: clientForGdr,
6639
+ instance: instance,
6640
+ definition: definition,
6641
+ stageName: stage.name,
6642
+ now: now,
6643
+ snapshot: ctx.snapshot
6644
+ })
6645
+ });
6646
+ }
6647
+
6648
+ const CASCADE_LIMIT = 100;
6649
+
6650
+ async function runCascadeHop({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, overlay: overlay}) {
6651
+ const ctx = await loadContext({
6652
+ client: client,
6653
+ instanceId: instanceId,
6654
+ options: {
6655
+ clientForGdr: clientForGdr,
6656
+ refSurface: refSurface,
6657
+ ...actor ? {
6658
+ actor: actor
6659
+ } : {},
6660
+ ...clock ? {
6661
+ clock: clock
6662
+ } : {},
6663
+ ...executionContext ? {
6664
+ executionContext: executionContext
6665
+ } : {},
6666
+ ...telemetry ? {
6667
+ telemetry: telemetry
6668
+ } : {},
6669
+ ...overlay ? {
6670
+ overlay: overlay
6671
+ } : {}
6672
+ }
6673
+ });
6674
+ if (isTerminal(ctx)) return {
6675
+ moved: !1
6691
6676
  };
6677
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), fired = await runTriggeredActions({
6678
+ ctx: ctx,
6679
+ mutation: mutation,
6680
+ stage: stage
6681
+ }), hopCtx = liveViewContext(ctx, mutation), transition = await pickTransition(hopCtx, stage);
6682
+ return transition === void 0 ? (fired.fires > 0 && await persistThenMaybeRefresh({
6683
+ ctx: ctx,
6684
+ mutation: mutation,
6685
+ stageName: stage.name,
6686
+ didChangeState: fired.ranFieldOps
6687
+ }), {
6688
+ moved: !1
6689
+ }) : (await commitStageMove({
6690
+ ctx: ctx,
6691
+ mutation: mutation,
6692
+ fromStage: stage,
6693
+ toStage: findStage(ctx.definition, transition.to),
6694
+ transition: transition.name,
6695
+ via: "transition",
6696
+ actor: actor
6697
+ }), {
6698
+ moved: !0
6699
+ });
6700
+ }
6701
+
6702
+ async function cascadeAutoTransitions({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, overlay: overlay}) {
6703
+ let count = 0;
6704
+ for (;;) {
6705
+ if (await drainCondemnedChildren({
6706
+ client: client,
6707
+ instanceId: instanceId,
6708
+ actor: actor,
6709
+ clientForGdr: clientForGdr,
6710
+ refSurface: refSurface,
6711
+ clock: clock,
6712
+ executionContext: executionContext,
6713
+ telemetry: telemetry
6714
+ }), !(await runCascadeHop({
6715
+ client: client,
6716
+ instanceId: instanceId,
6717
+ actor: actor,
6718
+ clientForGdr: clientForGdr,
6719
+ refSurface: refSurface,
6720
+ clock: clock,
6721
+ executionContext: executionContext,
6722
+ telemetry: telemetry,
6723
+ overlay: overlay
6724
+ })).moved) return count;
6725
+ if (count++, count >= CASCADE_LIMIT) throw new CascadeLimitError({
6726
+ instanceId: instanceId,
6727
+ limit: CASCADE_LIMIT
6728
+ });
6729
+ }
6730
+ }
6731
+
6732
+ async function drainCondemnedChildren({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, draining: draining = /* @__PURE__ */ new Set}) {
6733
+ if (draining.has(instanceId)) return;
6734
+ draining.add(instanceId);
6735
+ const instance = await getInstanceDocument(client, instanceId);
6736
+ if (!instance) return;
6737
+ const condemned = condemnedSubworkflows(instance);
6738
+ if (condemned.length !== 0) {
6739
+ for (const row of condemned) await settleCondemnedRow({
6740
+ client: client,
6741
+ ownerId: instanceId,
6742
+ row: row,
6743
+ actor: actor,
6744
+ clientForGdr: clientForGdr,
6745
+ refSurface: refSurface,
6746
+ clock: clock,
6747
+ executionContext: executionContext,
6748
+ telemetry: telemetry,
6749
+ draining: draining
6750
+ });
6751
+ await stampDrainedRows({
6752
+ client: client,
6753
+ instance: instance,
6754
+ condemned: condemned,
6755
+ clock: clock,
6756
+ executionContext: executionContext
6757
+ });
6758
+ }
6692
6759
  }
6693
6760
 
6694
- async function evaluateAction(args) {
6695
- const {action: action, activityName: activityName, snapshot: snapshot, activityScope: activityScope, sites: sites} = args, conditionScope = invariants.isCascadeFired(action) ? await args.cascadeActivityScope() : activityScope, insights = await explainActionGates({
6696
- action: action,
6697
- activityName: activityName,
6698
- conditionScope: conditionScope,
6699
- snapshot: snapshot,
6700
- sites: sites
6701
- });
6702
- return action.when !== void 0 ? triggeredActionVerdict({
6703
- action: action,
6704
- when: action.when,
6705
- insights: insights
6706
- }) : fireableActionVerdict({
6707
- args: args,
6708
- insights: insights
6761
+ async function settleCondemnedRow({client: client, ownerId: ownerId, row: row, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, draining: draining}) {
6762
+ const childId = invariants.toBareId(row.ref.id);
6763
+ try {
6764
+ await abortInstance({
6765
+ client: client,
6766
+ instanceId: childId,
6767
+ reason: row.abortPending?.reason ?? "condemned by parent",
6768
+ options: {
6769
+ ...actor ? {
6770
+ actor: actor
6771
+ } : {},
6772
+ clientForGdr: clientForGdr,
6773
+ refSurface: refSurface,
6774
+ ...clock ? {
6775
+ clock: clock
6776
+ } : {},
6777
+ ...executionContext ? {
6778
+ executionContext: executionContext
6779
+ } : {},
6780
+ ...telemetry ? {
6781
+ telemetry: telemetry
6782
+ } : {}
6783
+ }
6784
+ }), await drainCondemnedChildren({
6785
+ client: client,
6786
+ instanceId: childId,
6787
+ actor: actor,
6788
+ clientForGdr: clientForGdr,
6789
+ refSurface: refSurface,
6790
+ clock: clock,
6791
+ executionContext: executionContext,
6792
+ telemetry: telemetry,
6793
+ draining: draining
6794
+ });
6795
+ } catch (cause) {
6796
+ if (cause instanceof invariants.InstanceNotFoundError) return;
6797
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
6798
+ instanceId: ownerId,
6799
+ guardError: cause,
6800
+ reason: `owed child abort of "${childId}" failed while draining condemned rows`
6801
+ });
6802
+ }
6803
+ }
6804
+
6805
+ async function stampDrainedRows({client: client, instance: instance, condemned: condemned, clock: clock, executionContext: executionContext}) {
6806
+ const now = (clock ?? wallClock)(), stamp = resolveExecutionContext(executionContext), ids = condemned.map(row => invariants.toBareId(row.ref.id)), children = await client.fetch("*[_id in $ids]{_id, currentStage, completedAt, abortedAt, modelVersion, minReaderModel}", {
6807
+ ids: ids
6808
+ }), byId = new Map(children.map(c => [ invariants.assertReadableModel(c)._id, c ])), condemnedKeys = new Set(condemned.map(row => row._key)), history = [ ...instance.history ], subworkflows = (instance.subworkflows ?? []).map(row => {
6809
+ if (row.resolved !== void 0 || !condemnedKeys.has(row._key)) return row;
6810
+ const child = byId.get(invariants.toBareId(row.ref.id)), resolved = child !== void 0 ? terminalResolution(child) : {
6811
+ at: now,
6812
+ aborted: !0
6813
+ };
6814
+ return resolved === void 0 ? row : (history.push(...stampHistoryEntries([ subworkflowResolvedEntry({
6815
+ row: row,
6816
+ at: now,
6817
+ status: resolved.aborted === !0 ? "aborted" : "done"
6818
+ }) ], stamp)), {
6819
+ ...row,
6820
+ resolved: resolved
6821
+ });
6709
6822
  });
6823
+ await client.patch(instance._id).set({
6824
+ subworkflows: subworkflows,
6825
+ history: history,
6826
+ lastChangedAt: now
6827
+ }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
6710
6828
  }
6711
6829
 
6712
- async function explainActionGates({action: action, activityName: activityName, conditionScope: conditionScope, snapshot: snapshot, sites: sites}) {
6713
- const insight = action.filter !== void 0 ? await explainSite({
6714
- site: {
6715
- kind: "action",
6716
- activity: activityName,
6717
- action: action.name
6718
- },
6719
- condition: action.filter,
6720
- params: conditionScope,
6721
- snapshot: snapshot,
6722
- sites: sites
6723
- }) : void 0, whenInsight = action.when !== void 0 ? await explainSite({
6724
- site: {
6725
- kind: "action-when",
6726
- activity: activityName,
6727
- action: action.name
6728
- },
6729
- condition: action.when,
6730
- params: conditionScope,
6731
- snapshot: snapshot,
6732
- sites: sites
6733
- }) : void 0;
6734
- return {
6735
- ...insight !== void 0 ? {
6736
- insight: insight
6737
- } : {},
6738
- ...whenInsight !== void 0 ? {
6739
- whenInsight: whenInsight
6830
+ function terminalResolution(child) {
6831
+ const status = resolvedChildStatus(child);
6832
+ if (!(status === void 0 || child.completedAt === void 0 || child.completedAt === null)) return {
6833
+ at: child.completedAt,
6834
+ stage: child.currentStage,
6835
+ ...status === "aborted" ? {
6836
+ aborted: !0
6740
6837
  } : {}
6741
6838
  };
6742
6839
  }
6743
6840
 
6744
- function triggeredActionVerdict({action: action, when: when, insights: insights}) {
6745
- return insights.insight !== void 0 && insights.insight.outcome === "unsatisfied" ? disabled({
6746
- action: action,
6747
- reason: {
6748
- kind: "filter-failed",
6749
- filter: action.filter ?? ""
6750
- },
6751
- ...insights
6752
- }) : {
6753
- ...actionEvaluationIdentity(action),
6754
- allowed: !1,
6755
- triggered: !0,
6756
- disabledReason: {
6757
- kind: "cascade-fired",
6758
- when: when
6759
- },
6760
- ...insights
6841
+ function subworkflowResolvedEntry({row: row, at: at, status: status}) {
6842
+ return {
6843
+ _key: randomKey(),
6844
+ _type: "subworkflowResolved",
6845
+ at: at,
6846
+ activity: row.activity,
6847
+ instanceRef: row.ref,
6848
+ status: status
6761
6849
  };
6762
6850
  }
6763
6851
 
6764
- function fireableActionVerdict({args: args, insights: insights}) {
6765
- const {action: action, status: status, instance: instance, stageHasExits: stageHasExits, guardDenial: guardDenial} = args, lifecycle = lifecycleReason({
6766
- instance: instance,
6767
- status: status,
6768
- stageHasExits: stageHasExits
6769
- });
6770
- if (lifecycle !== void 0) return disabled({
6771
- action: action,
6772
- reason: lifecycle,
6773
- ...insights
6774
- });
6775
- if (guardDenial !== void 0) return disabled({
6776
- action: action,
6777
- reason: guardDenial,
6778
- ...insights
6779
- });
6780
- if (args.subjectPermissionReason !== void 0 && (action.effects?.length ?? 0) > 0) return disabled({
6781
- action: action,
6782
- reason: args.subjectPermissionReason,
6783
- ...insights
6784
- });
6785
- if (args.requirementsReason !== void 0) return disabled({
6786
- action: action,
6787
- reason: args.requirementsReason,
6788
- ...insights
6789
- });
6790
- const {insight: insight} = insights;
6791
- return action.filter !== void 0 && insight !== void 0 && insight.outcome !== "satisfied" ? disabled({
6792
- action: action,
6793
- reason: {
6794
- kind: "filter-failed",
6795
- filter: action.filter
6796
- },
6797
- ...insights
6798
- }) : {
6799
- ...actionEvaluationIdentity(action),
6800
- allowed: !0,
6801
- ...insights
6802
- };
6852
+ async function propagateToAncestors({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry}) {
6853
+ const loaded = await loadPropagationPair(client, instanceId);
6854
+ if (loaded === void 0) return;
6855
+ const {child: child, parent: parent} = loaded, definition = invariants.parseDefinitionSnapshot(parent), ctx = await buildEngineContext({
6856
+ client: client,
6857
+ clientForGdr: clientForGdr,
6858
+ refSurface: refSurface,
6859
+ instance: parent,
6860
+ definition: definition,
6861
+ ...clock ? {
6862
+ clock: clock
6863
+ } : {},
6864
+ ...actor ? {
6865
+ actor: actor
6866
+ } : {},
6867
+ ...executionContext ? {
6868
+ executionContext: executionContext
6869
+ } : {},
6870
+ ...telemetry ? {
6871
+ telemetry: telemetry
6872
+ } : {}
6873
+ }), mutation = startMutation(parent), row = mutation.subworkflows.find(r => invariants.toBareId(r.ref.id) === child._id);
6874
+ if (row === void 0) {
6875
+ await recordOrphanedPropagation({
6876
+ ctx: ctx,
6877
+ mutation: mutation,
6878
+ child: child
6879
+ });
6880
+ return;
6881
+ }
6882
+ const changed = stampResolvedChild({
6883
+ mutation: mutation,
6884
+ row: row,
6885
+ child: child,
6886
+ now: ctx.now
6887
+ }), parentTerminal = parent.completedAt !== void 0;
6888
+ changed && await persist(ctx, mutation), !parentTerminal && (await cascadeAutoTransitions({
6889
+ client: client,
6890
+ instanceId: parent._id,
6891
+ actor: actor,
6892
+ clientForGdr: clientForGdr,
6893
+ refSurface: refSurface,
6894
+ clock: clock,
6895
+ executionContext: executionContext,
6896
+ telemetry: telemetry
6897
+ }), await propagateToAncestors({
6898
+ client: client,
6899
+ instanceId: parent._id,
6900
+ actor: actor,
6901
+ clientForGdr: clientForGdr,
6902
+ refSurface: refSurface,
6903
+ clock: clock,
6904
+ executionContext: executionContext,
6905
+ telemetry: telemetry
6906
+ }));
6803
6907
  }
6804
6908
 
6805
- function actionEvaluationIdentity(action) {
6806
- return {
6807
- action: action,
6808
- ...action.semantics !== void 0 ? {
6809
- semantics: action.semantics
6810
- } : {}
6909
+ async function loadPropagationPair(client, instanceId) {
6910
+ const child = await getInstanceDocument(client, instanceId);
6911
+ if (!child) return;
6912
+ const parentGdr = invariants.parentRef(child);
6913
+ if (parentGdr === void 0) return;
6914
+ const parentDoc = await client.getDocument(invariants.toBareId(parentGdr.id));
6915
+ if (!(!parentDoc || parentDoc._type !== invariants.WORKFLOW_INSTANCE_TYPE)) return {
6916
+ child: child,
6917
+ parent: readInstanceDoc(parentDoc)
6811
6918
  };
6812
6919
  }
6813
6920
 
6814
- async function instanceGuardReason({instance: instance, actor: actor, guards: guards}) {
6815
- if (guards === void 0 || guards.length === 0) return;
6816
- const denied = await instanceWriteDenials({
6817
- instance: instance,
6818
- guards: guards,
6819
- identity: actor.id
6820
- });
6821
- if (denied.length !== 0) return {
6822
- kind: "mutation-guard-denied",
6823
- denied: deniedGuardRefs(denied)
6824
- };
6921
+ function stampResolvedChild({mutation: mutation, row: row, child: child, now: now}) {
6922
+ if (row.resolved !== void 0) return !1;
6923
+ const resolved = terminalResolution(child);
6924
+ return resolved === void 0 ? !1 : (row.resolved = resolved, mutation.history.push(subworkflowResolvedEntry({
6925
+ row: row,
6926
+ at: now,
6927
+ status: resolved.aborted === !0 ? "aborted" : "done"
6928
+ })), !0);
6825
6929
  }
6826
6930
 
6827
- function lifecycleReason({instance: instance, status: status, stageHasExits: stageHasExits}) {
6828
- const terminalReason = instanceTerminalReason(instance);
6829
- if (terminalReason !== void 0) return terminalReason;
6830
- if (!stageHasExits) return {
6831
- kind: "stage-terminal",
6832
- stage: instance.currentStage
6833
- };
6834
- if (invariants.isTerminalActivityStatus(status)) return {
6835
- kind: "activity-not-active",
6836
- status: status
6837
- };
6931
+ async function recordOrphanedPropagation({ctx: ctx, mutation: mutation, child: child}) {
6932
+ const childUri = invariants.selfGdr(child);
6933
+ mutation.history.some(h => h._type === "subworkflowOrphaned" && h.instanceRef.id === childUri) || (mutation.history.push({
6934
+ _key: randomKey(),
6935
+ _type: "subworkflowOrphaned",
6936
+ at: ctx.now,
6937
+ instanceRef: {
6938
+ id: childUri,
6939
+ type: invariants.WORKFLOW_INSTANCE_TYPE
6940
+ },
6941
+ detail: `Instance "${child._id}" names this instance as its parent but no subworkflow-registry row matches it, so its state cannot drive any gate here.`
6942
+ }), await persist(ctx, mutation));
6838
6943
  }
6839
6944
 
6840
- function disabled(args) {
6945
+ function startMutation(instance) {
6841
6946
  return {
6842
- ...actionEvaluationIdentity(args.action),
6843
- allowed: !1,
6844
- disabledReason: args.reason,
6845
- ...args.insight !== void 0 ? {
6846
- insight: args.insight
6947
+ minReaderModel: invariants.minReaderModelOf(instance),
6948
+ currentStage: instance.currentStage,
6949
+ fields: (instance.fields ?? []).map(s => ({
6950
+ ...s
6951
+ })),
6952
+ stages: instance.stages.map(s => ({
6953
+ ...s,
6954
+ fields: (s.fields ?? []).map(entry => ({
6955
+ ...entry
6956
+ })),
6957
+ activities: s.activities.map(t => ({
6958
+ ...t,
6959
+ ...t.fields !== void 0 ? {
6960
+ fields: t.fields.map(entry => ({
6961
+ ...entry
6962
+ }))
6963
+ } : {}
6964
+ }))
6965
+ })),
6966
+ subworkflows: (instance.subworkflows ?? []).map(row => ({
6967
+ ...row,
6968
+ ...row.abortPending !== void 0 ? {
6969
+ abortPending: {
6970
+ ...row.abortPending
6971
+ }
6972
+ } : {},
6973
+ ...row.resolved !== void 0 ? {
6974
+ resolved: {
6975
+ ...row.resolved
6976
+ }
6977
+ } : {}
6978
+ })),
6979
+ pendingEffects: [ ...instance.pendingEffects ],
6980
+ effectHistory: [ ...instance.effectHistory ],
6981
+ context: [ ...instance.context ],
6982
+ history: [ ...instance.history ],
6983
+ processedRequests: [ ...instance.processedRequests ?? [] ],
6984
+ lastChangedAt: instance.lastChangedAt,
6985
+ ...instance.completedAt !== void 0 ? {
6986
+ completedAt: instance.completedAt
6847
6987
  } : {},
6848
- ...args.whenInsight !== void 0 ? {
6849
- whenInsight: args.whenInsight
6988
+ ...instance.abortedAt !== void 0 ? {
6989
+ abortedAt: instance.abortedAt
6990
+ } : {},
6991
+ pendingCreates: []
6992
+ };
6993
+ }
6994
+
6995
+ function instanceStateFields(src) {
6996
+ const state = {
6997
+ currentStage: src.currentStage,
6998
+ fields: src.fields,
6999
+ stages: src.stages,
7000
+ subworkflows: src.subworkflows ?? [],
7001
+ pendingEffects: src.pendingEffects,
7002
+ effectHistory: src.effectHistory,
7003
+ context: src.context,
7004
+ history: src.history,
7005
+ processedRequests: src.processedRequests ?? [],
7006
+ ...src.completedAt !== void 0 ? {
7007
+ completedAt: src.completedAt
7008
+ } : {},
7009
+ ...src.abortedAt !== void 0 ? {
7010
+ abortedAt: src.abortedAt
6850
7011
  } : {}
6851
7012
  };
7013
+ return {
7014
+ ...invariants.modelStampFor({
7015
+ documentType: "instance",
7016
+ document: state,
7017
+ storedMinReaderModel: src.minReaderModel
7018
+ }),
7019
+ ...state
7020
+ };
6852
7021
  }
6853
7022
 
6854
- function subjectDenialLabels(denied) {
6855
- return denied.map(d => `${d.permission} on ${d.subject} (${d.resource})`);
7023
+ function liveViewContext(ctx, mutation) {
7024
+ const materialized = materializeInstance(ctx.instance, mutation);
7025
+ return {
7026
+ ...ctx,
7027
+ instance: materialized,
7028
+ snapshot: overlayInstanceInSnapshot(ctx.snapshot, materialized)
7029
+ };
6856
7030
  }
6857
7031
 
6858
- class ActionDisabledError extends invariants.WorkflowError {
6859
- reason;
6860
- activity;
6861
- action;
6862
- constructor(args) {
6863
- super("action-disabled", formatDisabledReason({
6864
- activity: args.activity,
6865
- action: args.action,
6866
- reason: args.reason
6867
- })), this.name = "ActionDisabledError", this.reason = args.reason, this.activity = args.activity,
6868
- this.action = args.action;
6869
- }
7032
+ function materializeInstance(base, mutation) {
7033
+ return {
7034
+ ...base,
7035
+ currentStage: mutation.currentStage,
7036
+ fields: mutation.fields,
7037
+ stages: mutation.stages,
7038
+ subworkflows: mutation.subworkflows,
7039
+ context: mutation.context,
7040
+ pendingEffects: mutation.pendingEffects,
7041
+ effectHistory: mutation.effectHistory
7042
+ };
6870
7043
  }
6871
7044
 
6872
- class StartNotAllowedError extends invariants.WorkflowError {
6873
- definition;
6874
- insight;
6875
- constructor(args) {
6876
- super("start-not-allowed", `startInstance refused: start.allowed on definition "${args.definition}" evaluated ` + (args.insight.outcome === "unevaluable" ? `GROQ null ("can't decide" — fail-closed)` : "false") + " for the supplied initialFields. Pre-flight the verdict with evaluateStart."),
6877
- this.name = "StartNotAllowedError", this.definition = args.definition, this.insight = args.insight;
7045
+ async function persist(ctx, mutation) {
7046
+ stampExecutionContext(ctx, mutation);
7047
+ const set = {
7048
+ ...instanceStateFields(mutation),
7049
+ lastChangedAt: ctx.now
7050
+ }, pendingCreates = mutation.pendingCreates;
7051
+ if (pendingCreates.length === 0) return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
7052
+ const tx = ctx.client.transaction();
7053
+ for (const {body: body} of pendingCreates) tx.create(body);
7054
+ tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)),
7055
+ await tx.commit(), mutation.pendingCreates = [];
7056
+ const actorForPriming = ctx.actor;
7057
+ for (const {body: body, started: started} of pendingCreates) try {
7058
+ await primeInitialStage({
7059
+ client: ctx.client,
7060
+ instanceId: body._id,
7061
+ actor: actorForPriming,
7062
+ clientForGdr: ctx.clientForGdr,
7063
+ refSurface: ctx.refSurface,
7064
+ clock: ctx.clock,
7065
+ executionContext: ctx.executionContext,
7066
+ telemetry: ctx.telemetry
7067
+ }), await cascadeAutoTransitions({
7068
+ client: ctx.client,
7069
+ instanceId: body._id,
7070
+ actor: actorForPriming,
7071
+ clientForGdr: ctx.clientForGdr,
7072
+ refSurface: ctx.refSurface,
7073
+ clock: ctx.clock,
7074
+ executionContext: ctx.executionContext,
7075
+ telemetry: ctx.telemetry
7076
+ }), await propagateToAncestors({
7077
+ client: ctx.client,
7078
+ instanceId: body._id,
7079
+ actor: actorForPriming,
7080
+ clientForGdr: ctx.clientForGdr,
7081
+ refSurface: ctx.refSurface,
7082
+ clock: ctx.clock,
7083
+ executionContext: ctx.executionContext,
7084
+ telemetry: ctx.telemetry
7085
+ }), ctx.telemetry.log(WorkflowInstanceStarted, started);
7086
+ } catch (cause) {
7087
+ throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
7088
+ instanceId: ctx.instance._id,
7089
+ guardError: cause,
7090
+ reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
7091
+ });
6878
7092
  }
7093
+ const reloaded = await getInstanceDocument(ctx.client, ctx.instance._id);
7094
+ if (!reloaded) throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
7095
+ return reloaded;
6879
7096
  }
6880
7097
 
6881
- function actionRendering(action) {
6882
- const kind = action.disabledReason?.kind;
6883
- return kind === "filter-failed" ? "absent" : action.triggered === !0 || kind === "cascade-fired" ? "automation" : "button";
7098
+ function stampExecutionContext(ctx, mutation) {
7099
+ const stamp = ctx.executionContext, appended = mutation.history.slice(ctx.instance.history.length);
7100
+ mutation.history = [ ...mutation.history.slice(0, ctx.instance.history.length), ...stampHistoryEntries(appended, stamp) ];
7101
+ for (const {body: body} of mutation.pendingCreates) body.history = stampHistoryEntries(body.history, stamp);
6884
7102
  }
6885
7103
 
6886
- const disabledReasonDetail = {
6887
- "filter-failed": r => `action filter returned false${r.detail ? ` (${r.detail})` : ""}`,
6888
- "cascade-fired": r => `the action is cascade-fired (when: ${JSON.stringify(r.when)}) the engine fires it on truth; it cannot be invoked via fireAction`,
6889
- "activity-not-active": r => `activity status is "${r.status}"`,
6890
- "stage-terminal": r => `stage "${r.stage}" is terminal`,
6891
- "instance-completed": r => `instance completed at ${r.completedAt}`,
6892
- "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
6893
- "requirements-unmet": r => `unmet requirement(s): ${r.unmetRequirements.join(", ")}`,
6894
- "subject-permission-denied": r => `missing subject permission(s): ${subjectDenialLabels(r.denied).join(", ")}`
6895
- };
7104
+ function currentStageEntry(mutation) {
7105
+ const entry = findOpenStageEntry(mutation);
7106
+ if (entry === void 0) throw new Error(`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`);
7107
+ return entry;
7108
+ }
6896
7109
 
6897
- function actionDisabledDetail(reason) {
6898
- return disabledReasonDetail[reason.kind](reason);
7110
+ function currentActivities(mutation) {
7111
+ return currentStageEntry(mutation).activities;
6899
7112
  }
6900
7113
 
6901
- function formatDisabledReason({activity: activity, action: action, reason: reason}) {
6902
- return `Action "${activity}:${action}" is not allowed: ${actionDisabledDetail(reason)}`;
7114
+ function findActivityInCurrentStage(ctx, activityName) {
7115
+ const stage = findStage(ctx.definition, ctx.instance.currentStage), activity = (stage.activities ?? []).find(t => t.name === activityName);
7116
+ if (activity === void 0) throw new invariants.ContractViolationError(`Activity "${activityName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`);
7117
+ return {
7118
+ stage: stage,
7119
+ activity: activity
7120
+ };
6903
7121
  }
6904
7122
 
6905
- class EditFieldDeniedError extends invariants.WorkflowError {
6906
- reason;
6907
- target;
6908
- constructor(args) {
6909
- super("edit-field-denied", formatEditDisabledReason(args.target, args.reason)),
6910
- this.name = "EditFieldDeniedError", this.reason = args.reason, this.target = args.target;
6911
- }
7123
+ function requireMutationActivityEntry(mutation, activity) {
7124
+ const mutEntry = currentActivities(mutation).find(t => t.name === activity);
7125
+ if (mutEntry === void 0) throw new Error(`Activity "${activity}" disappeared from mutation copy — invariant broken`);
7126
+ return mutEntry;
6912
7127
  }
6913
7128
 
6914
- const editDisabledReasonDetail = {
6915
- "not-editable": () => "field is not declared editable",
6916
- "instance-completed": r => `instance completed at ${r.completedAt}`,
6917
- "instance-aborted": r => `instance aborted at ${r.abortedAt}`,
6918
- "edit-window-closed": r => `edit window closed (${r.detail})`,
6919
- "editor-not-permitted": r => `editor not permitted (${r.predicate})`
6920
- };
7129
+ function findCurrentStageEntry(instance) {
7130
+ return findOpenStageEntry(instance);
7131
+ }
6921
7132
 
6922
- function formatEditDisabledReason(target, reason) {
6923
- const detail = editDisabledReasonDetail[reason.kind](reason), where = target.activity !== void 0 ? `${target.activity}.${target.field}` : target.field;
6924
- return `Field "${target.scope}:${where}" is not editable: ${detail}`;
7133
+ function findCurrentActivities(instance) {
7134
+ return findCurrentStageEntry(instance)?.activities ?? [];
6925
7135
  }
6926
7136
 
6927
- async function fireAction(args) {
6928
- const {client: client, instanceId: instanceId, activity: activity, action: action, params: params, requestRecord: requestRecord, options: options} = args;
7137
+ async function completeEffect(args) {
7138
+ const {client: client, instanceId: instanceId, effectKey: effectKey, status: status, outputs: outputs, ops: ops, detail: detail, error: error, durationMs: durationMs, requestRecord: requestRecord, options: options} = args;
7139
+ if (status !== "done" && status !== "failed") throw new invariants.ContractViolationError(`completeEffect: status must be "done" or "failed", got ${JSON.stringify(status)} — "cancelled" is engine-stamped by abort, never reportable`);
6929
7140
  return retryOnRevisionConflict({
6930
7141
  client: client,
6931
7142
  instanceId: instanceId,
6932
7143
  options: options,
6933
- commit: ctx => commitAction({
7144
+ commit: ctx => commitCompleteEffect({
6934
7145
  ctx: ctx,
6935
- activityName: activity,
6936
- actionName: action,
6937
- callerParams: params,
7146
+ effectKey: effectKey,
7147
+ status: status,
7148
+ outputs: outputs,
7149
+ ops: ops,
7150
+ detail: detail,
7151
+ error: error,
7152
+ durationMs: durationMs,
6938
7153
  requestRecord: requestRecord,
6939
- options: options
7154
+ actor: options.actor
6940
7155
  }),
6941
- onExhausted: () => new ConcurrentFireActionError({
7156
+ onExhausted: () => new ConcurrentCompleteEffectError({
6942
7157
  instanceId: instanceId,
6943
- activity: activity,
6944
- action: action,
7158
+ effectKey: effectKey,
6945
7159
  attempts: CONCURRENT_COMMIT_MAX_ATTEMPTS
6946
7160
  })
6947
7161
  });
6948
7162
  }
6949
7163
 
6950
- async function resolveActionCommit({ctx: ctx, activityName: activityName, actionName: actionName, callerParams: callerParams, options: options}) {
6951
- const actor = options?.actor, {stage: stage, activity: activity} = findActivityInCurrentStage(ctx, activityName), action = (activity.actions ?? []).find(a => a.name === actionName);
6952
- if (action === void 0) throw new invariants.ContractViolationError(`Action "${actionName}" not declared on activity "${activityName}"`);
6953
- if (action.when !== void 0) throw new ActionDisabledError({
6954
- activity: activityName,
6955
- action: actionName,
6956
- reason: {
6957
- kind: "cascade-fired",
6958
- when: action.when
6959
- }
6960
- });
6961
- if (action.filter !== void 0) {
6962
- const can = options?.grants !== void 0 && actor !== void 0 ? await advisoryCan({
6963
- instance: ctx.instance,
6964
- actor: actor,
6965
- grants: options.grants
6966
- }) : void 0;
6967
- if (!await ctxEvaluateCondition({
6968
- ctx: ctx,
6969
- condition: action.filter,
6970
- opts: {
6971
- activityName: activityName,
6972
- ...actor !== void 0 ? {
6973
- actor: actor
6974
- } : {},
6975
- ...can !== void 0 ? {
6976
- vars: {
6977
- can: can
6978
- }
6979
- } : {}
6980
- }
6981
- })) throw new ActionDisabledError({
6982
- activity: activityName,
6983
- action: actionName,
6984
- reason: {
6985
- kind: "filter-failed",
6986
- filter: action.filter,
6987
- detail: "commit re-check"
6988
- }
6989
- });
6990
- }
6991
- const entry = findCurrentActivities(ctx.instance).find(t => t.name === activityName);
6992
- if (entry === void 0 || entry.status !== "active") throw notActiveAtCommit({
6993
- activityName: activityName,
6994
- actionName: actionName,
6995
- status: entry?.status
6996
- });
6997
- const params = validateActionParams({
6998
- action: action,
6999
- activityName: activityName,
7000
- callerParams: callerParams
7001
- });
7164
+ function buildEffectHistoryEntry(pending, outcome) {
7165
+ const {status: status, ranAt: ranAt, actor: actor, detail: detail, error: error, durationMs: durationMs, outputs: outputs} = outcome, resolvedActor = actor ?? pending.actor;
7002
7166
  return {
7003
- stage: stage,
7004
- activity: activity,
7005
- action: action,
7006
- params: params
7167
+ _key: pending._key,
7168
+ name: pending.name,
7169
+ ...pending.title !== void 0 ? {
7170
+ title: pending.title
7171
+ } : {},
7172
+ ...pending.description !== void 0 ? {
7173
+ description: pending.description
7174
+ } : {},
7175
+ params: pending.params,
7176
+ origin: pending.origin,
7177
+ ...resolvedActor !== void 0 ? {
7178
+ actor: resolvedActor
7179
+ } : {},
7180
+ ...pending.stageEntryKey !== void 0 ? {
7181
+ stageEntryKey: pending.stageEntryKey
7182
+ } : {},
7183
+ ranAt: ranAt,
7184
+ ...durationMs !== void 0 ? {
7185
+ durationMs: durationMs
7186
+ } : {},
7187
+ status: status,
7188
+ ...detail !== void 0 ? {
7189
+ detail: detail
7190
+ } : {},
7191
+ ...error !== void 0 ? {
7192
+ error: error
7193
+ } : {},
7194
+ ...outputs !== void 0 ? {
7195
+ outputs: outputs
7196
+ } : {}
7007
7197
  };
7008
7198
  }
7009
7199
 
7010
- function notActiveAtCommit(args) {
7011
- const {activityName: activityName, actionName: actionName, status: status} = args;
7012
- return status !== void 0 && invariants.isTerminalActivityStatus(status) ? new ActionDisabledError({
7013
- activity: activityName,
7014
- action: actionName,
7015
- reason: {
7016
- kind: "activity-not-active",
7017
- status: status
7200
+ function buildEffectSettlement(pending, outcome) {
7201
+ const {status: status, ranAt: ranAt, actor: actor, detail: detail, outputs: outputs} = outcome;
7202
+ return {
7203
+ run: buildEffectHistoryEntry(pending, outcome),
7204
+ event: {
7205
+ _key: randomKey(),
7206
+ _type: "effectCompleted",
7207
+ at: ranAt,
7208
+ effectKey: pending._key,
7209
+ effect: pending.name,
7210
+ status: status,
7211
+ ...outputs !== void 0 ? {
7212
+ outputs: outputs
7213
+ } : {},
7214
+ ...detail !== void 0 ? {
7215
+ detail: detail
7216
+ } : {},
7217
+ ...actor !== void 0 ? {
7218
+ actor: actor
7219
+ } : {}
7018
7220
  }
7019
- }) : new Error(`Activity "${activityName}" must be active to fire action "${actionName}"; status is ${status ?? "missing"}`);
7221
+ };
7020
7222
  }
7021
7223
 
7022
- async function applyActionFire({ctx: ctx, mutation: mutation, activity: activity, action: action, params: params, actor: actor, triggered: triggered}) {
7023
- mutation.history.push({
7024
- _key: randomKey(),
7025
- _type: "actionFired",
7026
- at: ctx.now,
7027
- stage: mutation.currentStage,
7028
- activity: activity.name,
7029
- action: action.name,
7030
- ...actor !== void 0 ? {
7031
- actor: actor,
7032
- driverKind: invariants.driverKind(actor)
7224
+ function validateEffectOutputs(args) {
7225
+ const {outputs: outputs, declared: declared, effectName: effectName} = args, byName = new Map(declared.map(shape => [ shape.name, shape ])), issues = [];
7226
+ for (const [key, value] of Object.entries(outputs)) {
7227
+ const shape = byName.get(key);
7228
+ if (shape === void 0) {
7229
+ issues.push(`"${key}" is not a declared output`);
7230
+ continue;
7231
+ }
7232
+ const shapeIssues = effectOutputIssues(shape, value);
7233
+ shapeIssues !== void 0 && issues.push(...shapeIssues.map(i => `"${key}": ${i}`));
7234
+ }
7235
+ if (issues.length > 0) throw new EffectOutputsInvalidError({
7236
+ effect: effectName,
7237
+ issues: issues
7238
+ });
7239
+ }
7240
+
7241
+ function effectOutputIssues(shape, value) {
7242
+ return invariants.checkFieldValue({
7243
+ entryType: shape.type,
7244
+ value: value,
7245
+ ...shape.fields !== void 0 ? {
7246
+ fields: shape.fields
7033
7247
  } : {},
7034
- ...triggered ? {
7035
- triggered: !0
7248
+ ...shape.of !== void 0 ? {
7249
+ of: shape.of
7250
+ } : {},
7251
+ ...shape.options !== void 0 ? {
7252
+ options: shape.options
7253
+ } : {},
7254
+ ...shape.validation !== void 0 ? {
7255
+ validation: shape.validation
7036
7256
  } : {}
7037
7257
  });
7038
- const ranOps = await runOps({
7039
- ops: action.ops,
7040
- mutation: mutation,
7041
- stage: mutation.currentStage,
7042
- origin: {
7043
- activity: activity.name,
7044
- action: action.name
7045
- },
7046
- params: params,
7047
- actor: actor,
7048
- self: invariants.selfGdr(ctx.instance),
7049
- now: ctx.now,
7050
- snapshot: ctx.snapshot,
7051
- refSurface: ctx.refSurface
7258
+ }
7259
+
7260
+ function requirePendingEffect(instance, effectKey) {
7261
+ const pending = instance.pendingEffects.find(e => e._key === effectKey);
7262
+ if (pending !== void 0) return pending;
7263
+ const run = instance.effectHistory.find(e => e._key === effectKey), settled = run !== void 0 ? {
7264
+ status: run.status,
7265
+ ranAt: run.ranAt,
7266
+ ...run.detail !== void 0 ? {
7267
+ detail: run.detail
7268
+ } : {}
7269
+ } : void 0;
7270
+ throw new invariants.EffectNotFoundError({
7271
+ instanceId: instance._id,
7272
+ effectKey: effectKey,
7273
+ ...settled !== void 0 ? {
7274
+ settled: settled
7275
+ } : {}
7276
+ });
7277
+ }
7278
+
7279
+ function validateCompletionInput({pending: pending, definition: definition, status: status, ops: ops, outputs: outputs}) {
7280
+ if (status === "failed" && ops !== void 0 && ops.length > 0) throw new EffectOpsInvalidError({
7281
+ effect: pending.name,
7282
+ issues: [ "ops cannot accompany a failed completion — field.set the outcome on a done completion instead" ]
7052
7283
  });
7053
- return await queueEffects({
7054
- ctx: ctx,
7055
- mutation: mutation,
7056
- effects: action.effects,
7057
- origin: {
7058
- kind: "action",
7059
- name: action.name
7060
- },
7061
- actor: actor,
7062
- opts: {
7063
- callerParams: params,
7064
- activityName: activity.name
7065
- }
7066
- }), action.spawn !== void 0 && await spawnSubworkflows({
7067
- ctx: ctx,
7068
- mutation: mutation,
7069
- activity: activity,
7070
- action: action,
7071
- sub: action.spawn,
7072
- actor: actor
7073
- }), {
7074
- ranOps: ranOps
7075
- };
7284
+ if (status === "failed" && outputs !== void 0) throw new EffectOutputsInvalidError({
7285
+ effect: pending.name,
7286
+ issues: [ "outputs cannot accompany a failed completion — report outputs on a done completion instead" ]
7287
+ });
7288
+ if (outputs !== void 0) {
7289
+ const declared = findEffect(definition, pending.name)?.outputs ?? [];
7290
+ validateEffectOutputs({
7291
+ outputs: outputs,
7292
+ declared: declared,
7293
+ effectName: pending.name
7294
+ });
7295
+ }
7296
+ return ops !== void 0 ? validateEffectOps(ops, pending.name) : [];
7076
7297
  }
7077
7298
 
7078
- async function commitAction({ctx: ctx, activityName: activityName, actionName: actionName, callerParams: callerParams, requestRecord: requestRecord, options: options}) {
7299
+ async function commitCompleteEffect({ctx: ctx, effectKey: effectKey, status: status, outputs: outputs, ops: ops, detail: detail, error: error, durationMs: durationMs, requestRecord: requestRecord, actor: actor}) {
7079
7300
  assertRequestUnprocessed({
7080
7301
  instance: ctx.instance,
7081
7302
  record: requestRecord,
7082
7303
  now: ctx.now
7083
7304
  });
7084
- const actor = options?.actor, {stage: stage, activity: activity, action: action, params: params} = await resolveActionCommit({
7085
- ctx: ctx,
7086
- activityName: activityName,
7087
- actionName: actionName,
7088
- callerParams: callerParams,
7089
- options: options
7305
+ const pending = requirePendingEffect(ctx.instance, effectKey), validatedOps = validateCompletionInput({
7306
+ pending: pending,
7307
+ definition: ctx.definition,
7308
+ status: status,
7309
+ ops: ops,
7310
+ outputs: outputs
7090
7311
  }), mutation = startMutation(ctx.instance);
7091
7312
  recordProcessedRequest({
7092
7313
  mutation: mutation,
7093
7314
  record: requestRecord,
7094
7315
  now: ctx.now
7316
+ }), mutation.pendingEffects = mutation.pendingEffects.filter(e => e._key !== effectKey);
7317
+ const ranAt = ctx.now, settlement = buildEffectSettlement(pending, {
7318
+ status: status,
7319
+ ranAt: ranAt,
7320
+ actor: actor,
7321
+ detail: detail,
7322
+ error: error,
7323
+ durationMs: durationMs,
7324
+ outputs: outputs
7095
7325
  });
7096
- const mutEntry = requireMutationActivityEntry(mutation, activityName), statusBefore = mutEntry.status, {ranOps: ranOps} = await applyActionFire({
7097
- ctx: ctx,
7326
+ mutation.effectHistory.push(settlement.run);
7327
+ const wroteEffectOutputs = status === "done" && outputs !== void 0;
7328
+ mutation.history.push(settlement.event);
7329
+ const ranOps = await runOps({
7330
+ ops: validatedOps,
7098
7331
  mutation: mutation,
7099
- activity: activity,
7100
- action: action,
7101
- params: params,
7332
+ stage: ctx.instance.currentStage,
7333
+ origin: {
7334
+ effect: pending.name
7335
+ },
7336
+ params: pending.params,
7102
7337
  actor: actor,
7103
- triggered: !1
7104
- }), newStatus = mutEntry.status !== statusBefore ? mutEntry.status : void 0;
7338
+ self: invariants.selfGdr(ctx.instance),
7339
+ now: ranAt,
7340
+ snapshot: ctx.snapshot,
7341
+ refSurface: ctx.refSurface
7342
+ }), needsGuardRefresh = wroteEffectOutputs || ranOps.some(isFieldOp);
7105
7343
  return await persistThenMaybeRefresh({
7106
7344
  ctx: ctx,
7107
7345
  mutation: mutation,
7108
- stageName: stage.name,
7109
- didChangeState: ranOps.some(isFieldOp)
7346
+ stageName: ctx.instance.currentStage,
7347
+ didChangeState: needsGuardRefresh
7110
7348
  }), {
7111
- fired: !0,
7112
- activity: activityName,
7113
- action: actionName,
7114
- ...newStatus !== void 0 ? {
7115
- newStatus: newStatus
7116
- } : {},
7117
- ...ranOps.length > 0 ? {
7118
- ranOps: ranOps
7119
- } : {}
7120
- };
7121
- }
7122
-
7123
- async function runTriggeredActions({ctx: ctx, mutation: mutation, stage: stage}) {
7124
- let fires = 0, ranFieldOps = !1;
7125
- const liveCtx = liveViewContext(ctx, mutation);
7126
- let firedThisPass = !0;
7127
- for (;firedThisPass; ) {
7128
- firedThisPass = !1;
7129
- for (const activity of stage.activities ?? []) {
7130
- const fired = await runActivityTriggers({
7131
- ctx: liveCtx,
7132
- mutation: mutation,
7133
- activity: activity
7134
- });
7135
- fires += fired.fires, ranFieldOps = ranFieldOps || fired.ranFieldOps, fired.fires > 0 && (firedThisPass = !0);
7136
- }
7137
- }
7138
- return {
7139
- fires: fires,
7140
- ranFieldOps: ranFieldOps
7141
- };
7142
- }
7143
-
7144
- async function runActivityTriggers({ctx: ctx, mutation: mutation, activity: activity}) {
7145
- let fires = 0, ranFieldOps = !1;
7146
- for (const action of activity.actions ?? []) {
7147
- if (!invariants.isCascadeFired(action)) continue;
7148
- const entry = currentActivities(mutation).find(e => e.name === activity.name);
7149
- if (entry === void 0 || entry.status !== "active") break;
7150
- if ((entry.firedActions ?? []).includes(action.name) || !await triggerIsLive({
7151
- ctx: ctx,
7152
- activity: activity,
7153
- action: action
7154
- })) continue;
7155
- const result = await fireTriggeredAction({
7156
- ctx: ctx,
7157
- mutation: mutation,
7158
- activity: activity,
7159
- action: action,
7160
- entry: entry
7161
- });
7162
- if (fires++, ranFieldOps = ranFieldOps || result.ranFieldOps, invariants.isTerminalActivityStatus(entry.status)) break;
7163
- }
7164
- return {
7165
- fires: fires,
7166
- ranFieldOps: ranFieldOps
7167
- };
7168
- }
7169
-
7170
- async function triggerIsLive({ctx: ctx, activity: activity, action: action}) {
7171
- if (!tokenMayExecute({
7172
- actor: ctx.actor,
7173
- action: action,
7174
- ctx: ctx
7175
- })) return !1;
7176
- const scope = {
7177
- activityName: activity.name
7178
- };
7179
- return action.filter !== void 0 && await ctxEvaluateConditionOutcome({
7180
- ctx: ctx,
7181
- condition: action.filter,
7182
- opts: scope
7183
- }) !== "satisfied" ? !1 : await ctxEvaluateConditionOutcome({
7184
- ctx: ctx,
7185
- condition: action.when,
7186
- opts: scope
7187
- }) === "satisfied";
7188
- }
7189
-
7190
- function tokenMayExecute({actor: actor, action: action, ctx: ctx}) {
7191
- const roles = action.roles;
7192
- return roles === void 0 || roles.length === 0 ? !0 : actor === void 0 ? !1 : roles.some(required => invariants.actorFulfillsRole({
7193
- actorRoles: actor.roles,
7194
- required: required,
7195
- aliases: ctx.definition.roleAliases
7196
- }));
7197
- }
7198
-
7199
- async function fireTriggeredAction({ctx: ctx, mutation: mutation, activity: activity, action: action, entry: entry}) {
7200
- entry.firedActions = [ ...entry.firedActions ?? [], action.name ];
7201
- const {ranOps: ranOps} = await applyActionFire({
7202
- ctx: ctx,
7203
- mutation: mutation,
7204
- activity: activity,
7205
- action: action,
7206
- params: {},
7207
- actor: ctx.actor,
7208
- triggered: !0
7209
- });
7210
- return {
7211
- ranFieldOps: ranOps.some(isFieldOp)
7349
+ effectKey: effectKey,
7350
+ effect: pending.name,
7351
+ status: status,
7352
+ origin: pending.origin.kind
7212
7353
  };
7213
7354
  }
7214
7355
 
7215
- async function primeInitialStage({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry}) {
7216
- const instance = await getInstanceDocument(client, instanceId);
7217
- if (!instance || instance.stages.length > 0) return;
7218
- const definition = invariants.parseDefinitionSnapshot(instance), stage = definition.stages.find(s => s.name === instance.currentStage);
7219
- if (stage === void 0) return;
7220
- const ctx = await buildEngineContext({
7221
- client: client,
7222
- clientForGdr: clientForGdr,
7223
- refSurface: refSurface,
7224
- instance: instance,
7225
- definition: definition,
7226
- ...clock ? {
7227
- clock: clock
7228
- } : {},
7229
- ...actor ? {
7230
- actor: actor
7231
- } : {},
7232
- ...executionContext ? {
7233
- executionContext: executionContext
7234
- } : {},
7235
- ...telemetry ? {
7236
- telemetry: telemetry
7237
- } : {}
7238
- }), now = ctx.now, discards = [], initialStageEntry = {
7239
- _key: randomKey(),
7240
- name: stage.name,
7241
- enteredAt: now,
7242
- fields: await resolveStageFieldEntries({
7243
- client: client,
7244
- instance: instance,
7245
- stage: stage,
7246
- now: now,
7247
- refSurface: refSurface,
7248
- recordDiscard: recordFieldDiscards({
7249
- target: discards,
7250
- scope: "stage",
7251
- at: now
7252
- })
7253
- }),
7254
- activities: []
7255
- }, primedCtx = {
7256
- ...ctx,
7257
- instance: {
7258
- ...instance,
7259
- stages: [ initialStageEntry ]
7260
- }
7261
- };
7262
- initialStageEntry.activities = await buildStageActivities({
7263
- ctx: primedCtx,
7264
- stage: stage,
7265
- recordDiscard: recordFieldDiscards({
7266
- target: discards,
7267
- scope: "activity",
7268
- at: now
7269
- })
7270
- });
7271
- const terminal = isTerminalStage(stage), committed = await client.patch(instance._id).set({
7272
- stages: [ initialStageEntry ],
7273
- lastChangedAt: now,
7274
- ...terminal ? {
7275
- completedAt: now
7356
+ function buildQueuedEffect({effect: effect, origin: origin, params: params, actor: actor, now: now, stageEntryKey: stageEntryKey}) {
7357
+ const key = randomKey(), pending = {
7358
+ _key: key,
7359
+ _type: "pendingEffect",
7360
+ name: effect.name,
7361
+ ...effect.title !== void 0 ? {
7362
+ title: effect.title
7276
7363
  } : {},
7277
- ...discards.length > 0 ? {
7278
- history: [ ...instance.history, ...stampHistoryEntries(discards, ctx.executionContext) ]
7364
+ ...effect.description !== void 0 ? {
7365
+ description: effect.description
7366
+ } : {},
7367
+ ...effect.bindings !== void 0 ? {
7368
+ bindings: effect.bindings
7369
+ } : {},
7370
+ params: params,
7371
+ origin: origin,
7372
+ ...actor !== void 0 ? {
7373
+ actor: actor
7374
+ } : {},
7375
+ queuedAt: now,
7376
+ ...stageEntryKey !== void 0 ? {
7377
+ stageEntryKey: stageEntryKey
7279
7378
  } : {}
7280
- }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
7281
- await deployOrRollback({
7282
- client: client,
7283
- instanceId: instance._id,
7284
- committedRev: committed._rev,
7285
- restore: {
7286
- stages: instance.stages,
7287
- history: instance.history
7288
- },
7289
- ...terminal ? {
7290
- unset: [ "completedAt" ]
7379
+ }, history = {
7380
+ _key: randomKey(),
7381
+ _type: "effectQueued",
7382
+ at: now,
7383
+ effectKey: key,
7384
+ effect: effect.name,
7385
+ origin: origin
7386
+ };
7387
+ return {
7388
+ pending: pending,
7389
+ history: history
7390
+ };
7391
+ }
7392
+
7393
+ async function queueEffects({ctx: ctx, mutation: mutation, effects: effects, origin: origin, actor: actor, opts: opts}) {
7394
+ if (!effects || effects.length === 0) return;
7395
+ const now = ctx.now, liveCtx = {
7396
+ ...ctx,
7397
+ instance: materializeInstance(ctx.instance, mutation)
7398
+ }, stageEntryKey = opts?.stageEntryKey ?? findOpenStageEntry(liveCtx.instance)?._key, params = await ctxConditionParams(liveCtx, {
7399
+ ...opts?.activityName !== void 0 ? {
7400
+ activityName: opts.activityName
7291
7401
  } : {},
7292
- reversible: !0,
7293
- deploy: () => deployStageGuards({
7294
- client: client,
7295
- clientForGdr: clientForGdr,
7296
- instance: instance,
7297
- definition: definition,
7298
- stageName: stage.name,
7402
+ ...actor !== void 0 ? {
7403
+ actor: actor
7404
+ } : {},
7405
+ vars: {
7406
+ params: opts?.callerParams ?? {}
7407
+ }
7408
+ });
7409
+ for (const effect of effects) {
7410
+ const resolved = await resolveBindings({
7411
+ bindings: effect.bindings,
7412
+ staticInput: effect.input,
7413
+ snapshot: ctx.snapshot,
7414
+ params: params
7415
+ }), {pending: pending, history: history} = buildQueuedEffect({
7416
+ effect: effect,
7417
+ origin: origin,
7418
+ params: resolved,
7419
+ actor: actor,
7299
7420
  now: now,
7300
- snapshot: ctx.snapshot
7421
+ stageEntryKey: stageEntryKey
7422
+ });
7423
+ mutation.pendingEffects.push(pending), mutation.history.push(history);
7424
+ }
7425
+ }
7426
+
7427
+ async function commitEffectOps(args) {
7428
+ const {client: client, instanceId: instanceId, effectKey: effectKey, claimToken: claimToken, ops: ops, requestRecord: requestRecord, leaseMs: leaseMs, options: options} = args;
7429
+ return retryOnRevisionConflict({
7430
+ client: client,
7431
+ instanceId: instanceId,
7432
+ options: options,
7433
+ commit: ctx => commitReport({
7434
+ ctx: ctx,
7435
+ effectKey: effectKey,
7436
+ claimToken: claimToken,
7437
+ ops: ops,
7438
+ requestRecord: requestRecord,
7439
+ leaseMs: leaseMs
7440
+ }),
7441
+ onExhausted: () => new ConcurrentCommitEffectOpsError({
7442
+ instanceId: instanceId,
7443
+ effectKey: effectKey,
7444
+ attempts: CONCURRENT_COMMIT_MAX_ATTEMPTS
7301
7445
  })
7302
7446
  });
7303
7447
  }
7304
7448
 
7305
- const CASCADE_LIMIT = 100;
7449
+ function staleClaimReason(args) {
7450
+ const claim = args.pending.claim;
7451
+ if (claim === void 0) return "unclaimed";
7452
+ if (claim.claimToken !== args.claimToken) return "claim-superseded";
7453
+ if (isClaimExpired(claim, args.now)) return "lease-expired";
7454
+ }
7306
7455
 
7307
- async function runCascadeHop({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, overlay: overlay}) {
7308
- const ctx = await loadContext({
7309
- client: client,
7310
- instanceId: instanceId,
7311
- options: {
7312
- clientForGdr: clientForGdr,
7313
- refSurface: refSurface,
7314
- ...actor ? {
7315
- actor: actor
7316
- } : {},
7317
- ...clock ? {
7318
- clock: clock
7319
- } : {},
7320
- ...executionContext ? {
7321
- executionContext: executionContext
7322
- } : {},
7323
- ...telemetry ? {
7324
- telemetry: telemetry
7325
- } : {},
7326
- ...overlay ? {
7327
- overlay: overlay
7328
- } : {}
7456
+ function renewClaimLease(args) {
7457
+ const {mutation: mutation, effectKey: effectKey, now: now, leaseMs: leaseMs} = args;
7458
+ mutation.pendingEffects = mutation.pendingEffects.map(entry => entry._key === effectKey && entry.claim !== void 0 ? {
7459
+ ...entry,
7460
+ claim: {
7461
+ ...entry.claim,
7462
+ leaseExpiresAt: addMs(now, leaseMs)
7329
7463
  }
7464
+ } : entry);
7465
+ }
7466
+
7467
+ async function commitReport({ctx: ctx, effectKey: effectKey, claimToken: claimToken, ops: ops, requestRecord: requestRecord, leaseMs: leaseMs}) {
7468
+ assertRequestUnprocessed({
7469
+ instance: ctx.instance,
7470
+ record: requestRecord,
7471
+ now: ctx.now
7330
7472
  });
7331
- if (isTerminal(ctx)) return {
7332
- moved: !1
7333
- };
7334
- const stage = findStage(ctx.definition, ctx.instance.currentStage), mutation = startMutation(ctx.instance), fired = await runTriggeredActions({
7335
- ctx: ctx,
7473
+ const pending = requirePendingEffect(ctx.instance, effectKey), reason = staleClaimReason({
7474
+ pending: pending,
7475
+ claimToken: claimToken,
7476
+ now: ctx.now
7477
+ });
7478
+ if (reason !== void 0) throw new StaleEffectClaimError({
7479
+ instanceId: ctx.instance._id,
7480
+ effectKey: effectKey,
7481
+ reason: reason
7482
+ });
7483
+ const validatedOps = validateEffectOps(ops, pending.name);
7484
+ if (validatedOps.length === 0) throw new EffectOpsInvalidError({
7485
+ effect: pending.name,
7486
+ issues: [ "a mid-dispatch report must carry at least one field op — there is nothing to commit" ]
7487
+ });
7488
+ const mutation = startMutation(ctx.instance);
7489
+ return recordProcessedRequest({
7336
7490
  mutation: mutation,
7337
- stage: stage
7338
- }), hopCtx = liveViewContext(ctx, mutation), transition = await pickTransition(hopCtx, stage);
7339
- return transition === void 0 ? (fired.fires > 0 && await persistThenMaybeRefresh({
7340
- ctx: ctx,
7491
+ record: requestRecord,
7492
+ now: ctx.now
7493
+ }), renewClaimLease({
7341
7494
  mutation: mutation,
7342
- stageName: stage.name,
7343
- didChangeState: fired.ranFieldOps
7344
- }), {
7345
- moved: !1
7346
- }) : (await commitStageMove({
7495
+ effectKey: effectKey,
7496
+ now: ctx.now,
7497
+ leaseMs: leaseMs
7498
+ }), await runOps({
7499
+ ops: validatedOps,
7500
+ mutation: mutation,
7501
+ stage: ctx.instance.currentStage,
7502
+ origin: {
7503
+ effect: pending.name
7504
+ },
7505
+ params: pending.params,
7506
+ actor: ctx.actor,
7507
+ self: invariants.selfGdr(ctx.instance),
7508
+ now: ctx.now,
7509
+ snapshot: ctx.snapshot,
7510
+ refSurface: ctx.refSurface
7511
+ }), await persistThenMaybeRefresh({
7347
7512
  ctx: ctx,
7348
7513
  mutation: mutation,
7349
- fromStage: stage,
7350
- toStage: findStage(ctx.definition, transition.to),
7351
- transition: transition.name,
7352
- via: "transition",
7353
- actor: actor
7514
+ stageName: ctx.instance.currentStage,
7515
+ didChangeState: !0
7354
7516
  }), {
7355
- moved: !0
7517
+ effectKey: effectKey,
7518
+ effect: pending.name
7519
+ };
7520
+ }
7521
+
7522
+ const actorCache = /* @__PURE__ */ new WeakMap, grantsCache = /* @__PURE__ */ new WeakMap;
7523
+
7524
+ async function resolveAccess(taggedClient, args = {}) {
7525
+ const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
7526
+ if (requestFn === void 0) throw new invariants.ContractViolationError("workflow: no actor available. The engine resolves the actor from the client's token via `client.request({ uri: '/users/me' })`. Supply a real `@sanity/client` configured with a token (the test bench serves these endpoints per registered token).");
7527
+ const grantsPromise = args.grantsFromPath !== void 0 ? cachedGrants({
7528
+ client: client,
7529
+ requestFn: requestFn,
7530
+ resourcePath: args.grantsFromPath
7531
+ }) : Promise.resolve(void 0), [actor, grants] = await Promise.all([ cachedActor(client, requestFn), grantsPromise ]);
7532
+ if (actor === void 0) throw new invariants.ContractViolationError("workflow: failed to resolve actor from `/users/me`. The client is configured but the endpoint returned no usable identity — check the token.");
7533
+ return {
7534
+ actor: actor,
7535
+ ...grants !== void 0 ? {
7536
+ grants: grants
7537
+ } : {}
7538
+ };
7539
+ }
7540
+
7541
+ function cachedActor(client, requestFn) {
7542
+ const cached = actorCache.get(client);
7543
+ if (cached !== void 0) return cached;
7544
+ const pending = fetchActor(requestFn).catch(err => {
7545
+ throw actorCache.get(client) === pending && actorCache.delete(client), err;
7356
7546
  });
7547
+ return actorCache.set(client, pending), pending;
7357
7548
  }
7358
7549
 
7359
- async function cascadeAutoTransitions({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, overlay: overlay}) {
7360
- let count = 0;
7361
- for (;;) {
7362
- if (await drainCondemnedChildren({
7363
- client: client,
7364
- instanceId: instanceId,
7365
- actor: actor,
7366
- clientForGdr: clientForGdr,
7367
- refSurface: refSurface,
7368
- clock: clock,
7369
- executionContext: executionContext,
7370
- telemetry: telemetry
7371
- }), !(await runCascadeHop({
7372
- client: client,
7373
- instanceId: instanceId,
7374
- actor: actor,
7375
- clientForGdr: clientForGdr,
7376
- refSurface: refSurface,
7377
- clock: clock,
7378
- executionContext: executionContext,
7379
- telemetry: telemetry,
7380
- overlay: overlay
7381
- })).moved) return count;
7382
- if (count++, count >= CASCADE_LIMIT) throw new CascadeLimitError({
7383
- instanceId: instanceId,
7384
- limit: CASCADE_LIMIT
7550
+ function grantsForClientPath(taggedClient, resourcePath) {
7551
+ const client = unwrapRequestTag(taggedClient), requestFn = lazyRequest(client);
7552
+ return requestFn === void 0 ? Promise.resolve(void 0) : cachedGrants({
7553
+ client: client,
7554
+ requestFn: requestFn,
7555
+ resourcePath: resourcePath
7556
+ });
7557
+ }
7558
+
7559
+ function lazyRequest(client) {
7560
+ return client.request === void 0 ? void 0 : opts => client.request(opts);
7561
+ }
7562
+
7563
+ function cachedGrants({client: client, requestFn: requestFn, resourcePath: resourcePath}) {
7564
+ let byPath = grantsCache.get(client);
7565
+ byPath === void 0 && (byPath = /* @__PURE__ */ new Map, grantsCache.set(client, byPath));
7566
+ let cached = byPath.get(resourcePath);
7567
+ return cached === void 0 && (cached = fetchGrantsCached(requestFn, resourcePath),
7568
+ byPath.set(resourcePath, cached)), cached;
7569
+ }
7570
+
7571
+ async function fetchActor(requestFn) {
7572
+ let user;
7573
+ try {
7574
+ user = await requestFn({
7575
+ uri: "/users/me",
7576
+ tag: REQUEST_TAG.accessResolveActor
7577
+ });
7578
+ } catch (err) {
7579
+ throw new Error('workflow: /users/me request failed. The engine resolves the actor from the client\'s token via `client.request({ uri: "/users/me" })`. Check the token/connectivity.', {
7580
+ cause: err
7385
7581
  });
7386
7582
  }
7583
+ if (!user || typeof user.id != "string" || user.id.length === 0) return;
7584
+ const roleNames = user.roles?.map(r => r.name).filter(n => !!n) ?? [];
7585
+ return {
7586
+ kind: "person",
7587
+ id: user.id,
7588
+ ...roleNames.length > 0 ? {
7589
+ roles: roleNames
7590
+ } : {}
7591
+ };
7387
7592
  }
7388
7593
 
7389
- async function drainCondemnedChildren({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, draining: draining = /* @__PURE__ */ new Set}) {
7390
- if (draining.has(instanceId)) return;
7391
- draining.add(instanceId);
7392
- const instance = await getInstanceDocument(client, instanceId);
7393
- if (!instance) return;
7394
- const condemned = condemnedSubworkflows(instance);
7395
- if (condemned.length !== 0) {
7396
- for (const row of condemned) await settleCondemnedRow({
7397
- client: client,
7398
- ownerId: instanceId,
7399
- row: row,
7400
- actor: actor,
7401
- clientForGdr: clientForGdr,
7402
- refSurface: refSurface,
7403
- clock: clock,
7404
- executionContext: executionContext,
7405
- telemetry: telemetry,
7406
- draining: draining
7407
- });
7408
- await stampDrainedRows({
7409
- client: client,
7410
- instance: instance,
7411
- condemned: condemned,
7412
- clock: clock,
7413
- executionContext: executionContext
7594
+ async function fetchGrantsCached(requestFn, resourcePath) {
7595
+ try {
7596
+ return await fetchGrants({
7597
+ client: {
7598
+ request: requestFn
7599
+ },
7600
+ resourcePath: resourcePath
7414
7601
  });
7602
+ } catch (err) {
7603
+ console.warn(`workflow: failed to fetch grants from "${resourcePath}"; advisory permission reads that depend on them are skipped — a rendered $can stays undefined (conditions referencing it fail closed) and the subject-write forecast omits this resource. The lake still enforces writes. Original error: ${invariants.errorMessage(err)}`);
7604
+ return;
7415
7605
  }
7416
7606
  }
7417
7607
 
7418
- async function settleCondemnedRow({client: client, ownerId: ownerId, row: row, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry, draining: draining}) {
7419
- const childId = invariants.toBareId(row.ref.id);
7608
+ const REPLAY_SURFACE = {
7609
+ allows: () => !0,
7610
+ description: "any resource (in-memory replay; the committing surface enforces)"
7611
+ };
7612
+
7613
+ async function whatIfFireAction(args) {
7614
+ const {instance: instance, definition: definition, snapshot: snapshot, now: now, actor: actor, stage: stage, activity: activity, action: action} = args;
7615
+ if (!replayable({
7616
+ instance: instance,
7617
+ stage: stage,
7618
+ action: action
7619
+ })) return;
7620
+ const ctx = {
7621
+ now: now,
7622
+ instance: instance,
7623
+ definition: definition,
7624
+ snapshot: snapshot,
7625
+ refSurface: REPLAY_SURFACE,
7626
+ actor: actor
7627
+ }, mutation = startMutation(instance);
7420
7628
  try {
7421
- await abortInstance({
7422
- client: client,
7423
- instanceId: childId,
7424
- reason: row.abortPending?.reason ?? "condemned by parent",
7425
- options: {
7426
- ...actor ? {
7427
- actor: actor
7428
- } : {},
7429
- clientForGdr: clientForGdr,
7430
- refSurface: refSurface,
7431
- ...clock ? {
7432
- clock: clock
7433
- } : {},
7434
- ...executionContext ? {
7435
- executionContext: executionContext
7436
- } : {},
7437
- ...telemetry ? {
7438
- telemetry: telemetry
7439
- } : {}
7440
- }
7441
- }), await drainCondemnedChildren({
7442
- client: client,
7443
- instanceId: childId,
7629
+ await applyActionFire({
7630
+ ctx: ctx,
7631
+ mutation: mutation,
7632
+ activity: activity,
7633
+ action: action,
7634
+ params: {},
7444
7635
  actor: actor,
7445
- clientForGdr: clientForGdr,
7446
- refSurface: refSurface,
7447
- clock: clock,
7448
- executionContext: executionContext,
7449
- telemetry: telemetry,
7450
- draining: draining
7451
- });
7452
- } catch (cause) {
7453
- if (cause instanceof invariants.InstanceNotFoundError) return;
7454
- throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
7455
- instanceId: ownerId,
7456
- guardError: cause,
7457
- reason: `owed child abort of "${childId}" failed while draining condemned rows`
7636
+ triggered: !1
7637
+ }), await runTriggeredActions({
7638
+ ctx: ctx,
7639
+ mutation: mutation,
7640
+ stage: stage
7458
7641
  });
7642
+ } catch (err) {
7643
+ if (err instanceof invariants.FieldValueShapeError) return;
7644
+ throw err;
7459
7645
  }
7646
+ const transition = await pickTransition(liveViewContext(ctx, mutation), stage);
7647
+ return transition === void 0 ? {
7648
+ exitsStage: !1
7649
+ } : {
7650
+ exitsStage: !0,
7651
+ transition: transition.name
7652
+ };
7460
7653
  }
7461
7654
 
7462
- async function stampDrainedRows({client: client, instance: instance, condemned: condemned, clock: clock, executionContext: executionContext}) {
7463
- const now = (clock ?? wallClock)(), stamp = resolveExecutionContext(executionContext), ids = condemned.map(row => invariants.toBareId(row.ref.id)), children = await client.fetch("*[_id in $ids]{_id, currentStage, completedAt, abortedAt, modelVersion, minReaderModel}", {
7464
- ids: ids
7465
- }), byId = new Map(children.map(c => [ invariants.assertReadableModel(c)._id, c ])), condemnedKeys = new Set(condemned.map(row => row._key)), history = [ ...instance.history ], subworkflows = (instance.subworkflows ?? []).map(row => {
7466
- if (row.resolved !== void 0 || !condemnedKeys.has(row._key)) return row;
7467
- const child = byId.get(invariants.toBareId(row.ref.id)), resolved = child !== void 0 ? terminalResolution(child) : {
7468
- at: now,
7469
- aborted: !0
7470
- };
7471
- return resolved === void 0 ? row : (history.push(...stampHistoryEntries([ subworkflowResolvedEntry({
7472
- row: row,
7473
- at: now,
7474
- status: resolved.aborted === !0 ? "aborted" : "done"
7475
- }) ], stamp)), {
7476
- ...row,
7477
- resolved: resolved
7655
+ function replayable(args) {
7656
+ const {instance: instance, stage: stage, action: action} = args;
7657
+ if (invariants.terminalState(instance) !== "in-flight" || (action.params ?? []).length > 0 || action.spawn !== void 0) return !1;
7658
+ const primed = new Set((findOpenStageEntry(instance)?.activities ?? []).map(e => e.name));
7659
+ return (stage.activities ?? []).every(declared => primed.has(declared.name)) ? !(stage.activities ?? []).flatMap(declared => declared.actions ?? []).some(sibling => invariants.isCascadeFired(sibling) && sibling.spawn !== void 0) : !1;
7660
+ }
7661
+
7662
+ async function buildFieldInsights({sites: sites, snapshot: snapshot}) {
7663
+ const insights = [];
7664
+ for (const field of fieldsReadAcross(sites)) {
7665
+ const involved = sites.filter(entry => readsField(entry.insight, field));
7666
+ insights.push({
7667
+ field: field,
7668
+ reads: groqConditionDescribe.dedupeReads(involved.flatMap(entry => fieldReads(entry.insight, field))),
7669
+ involvedIn: involved.map(entry => entry.site),
7670
+ proposals: await verifyProposals({
7671
+ field: field,
7672
+ involved: involved,
7673
+ snapshot: snapshot
7674
+ })
7478
7675
  });
7479
- });
7480
- await client.patch(instance._id).set({
7481
- subworkflows: subworkflows,
7482
- history: history,
7483
- lastChangedAt: now
7484
- }).ifRevisionId(instance._rev).commit(SYNC_COMMIT);
7676
+ }
7677
+ return insights;
7485
7678
  }
7486
7679
 
7487
- function terminalResolution(child) {
7488
- const status = resolvedChildStatus(child);
7489
- if (!(status === void 0 || child.completedAt === void 0 || child.completedAt === null)) return {
7490
- at: child.completedAt,
7491
- stage: child.currentStage,
7492
- ...status === "aborted" ? {
7493
- aborted: !0
7494
- } : {}
7495
- };
7680
+ function fieldsReadAcross(sites) {
7681
+ const names = sites.flatMap(entry => entry.insight.analysis.reads).filter(read => read.variable === "fields").map(read => read.path[0]).filter(head => typeof head == "string");
7682
+ return [ ...new Set(names) ];
7496
7683
  }
7497
7684
 
7498
- function subworkflowResolvedEntry({row: row, at: at, status: status}) {
7499
- return {
7500
- _key: randomKey(),
7501
- _type: "subworkflowResolved",
7502
- at: at,
7503
- activity: row.activity,
7504
- instanceRef: row.ref,
7505
- status: status
7506
- };
7685
+ function fieldReads(insight, field) {
7686
+ return insight.analysis.reads.filter(read => read.variable === "fields" && read.path[0] === field);
7507
7687
  }
7508
7688
 
7509
- async function propagateToAncestors({client: client, instanceId: instanceId, actor: actor, clientForGdr: clientForGdr, refSurface: refSurface, clock: clock, executionContext: executionContext, telemetry: telemetry}) {
7510
- const loaded = await loadPropagationPair(client, instanceId);
7511
- if (loaded === void 0) return;
7512
- const {child: child, parent: parent} = loaded, definition = invariants.parseDefinitionSnapshot(parent), ctx = await buildEngineContext({
7513
- client: client,
7514
- clientForGdr: clientForGdr,
7515
- refSurface: refSurface,
7516
- instance: parent,
7517
- definition: definition,
7518
- ...clock ? {
7519
- clock: clock
7520
- } : {},
7521
- ...actor ? {
7522
- actor: actor
7523
- } : {},
7524
- ...executionContext ? {
7525
- executionContext: executionContext
7526
- } : {},
7527
- ...telemetry ? {
7528
- telemetry: telemetry
7529
- } : {}
7530
- }), mutation = startMutation(parent), row = mutation.subworkflows.find(r => invariants.toBareId(r.ref.id) === child._id);
7531
- if (row === void 0) {
7532
- await recordOrphanedPropagation({
7533
- ctx: ctx,
7534
- mutation: mutation,
7535
- child: child
7536
- });
7537
- return;
7538
- }
7539
- const changed = stampResolvedChild({
7540
- mutation: mutation,
7541
- row: row,
7542
- child: child,
7543
- now: ctx.now
7544
- }), parentTerminal = parent.completedAt !== void 0;
7545
- changed && await persist(ctx, mutation), !parentTerminal && (await cascadeAutoTransitions({
7546
- client: client,
7547
- instanceId: parent._id,
7548
- actor: actor,
7549
- clientForGdr: clientForGdr,
7550
- refSurface: refSurface,
7551
- clock: clock,
7552
- executionContext: executionContext,
7553
- telemetry: telemetry
7554
- }), await propagateToAncestors({
7555
- client: client,
7556
- instanceId: parent._id,
7557
- actor: actor,
7558
- clientForGdr: clientForGdr,
7559
- refSurface: refSurface,
7560
- clock: clock,
7561
- executionContext: executionContext,
7562
- telemetry: telemetry
7563
- }));
7689
+ function readsField(insight, field) {
7690
+ return fieldReads(insight, field).length > 0;
7564
7691
  }
7565
7692
 
7566
- async function loadPropagationPair(client, instanceId) {
7567
- const child = await getInstanceDocument(client, instanceId);
7568
- if (!child) return;
7569
- const parentGdr = invariants.parentRef(child);
7570
- if (parentGdr === void 0) return;
7571
- const parentDoc = await client.getDocument(invariants.toBareId(parentGdr.id));
7572
- if (!(!parentDoc || parentDoc._type !== invariants.WORKFLOW_INSTANCE_TYPE)) return {
7573
- child: child,
7574
- parent: readInstanceDoc(parentDoc)
7575
- };
7693
+ async function verifyProposals({field: field, involved: involved, snapshot: snapshot}) {
7694
+ const proposals = [];
7695
+ for (const assign of candidateAssignments(involved, field)) {
7696
+ const consequences = [];
7697
+ for (const entry of involved) {
7698
+ const result = await groqConditionDescribe.whatIfCondition({
7699
+ condition: entry.condition,
7700
+ dataset: snapshot.docs,
7701
+ params: entry.params,
7702
+ assign: assign
7703
+ });
7704
+ result.changed && consequences.push({
7705
+ site: entry.site,
7706
+ before: result.before,
7707
+ after: result.after
7708
+ });
7709
+ }
7710
+ consequences.length > 0 && proposals.push({
7711
+ assign: assign,
7712
+ consequences: consequences
7713
+ });
7714
+ }
7715
+ return proposals;
7576
7716
  }
7577
7717
 
7578
- function stampResolvedChild({mutation: mutation, row: row, child: child, now: now}) {
7579
- if (row.resolved !== void 0) return !1;
7580
- const resolved = terminalResolution(child);
7581
- return resolved === void 0 ? !1 : (row.resolved = resolved, mutation.history.push(subworkflowResolvedEntry({
7582
- row: row,
7583
- at: now,
7584
- status: resolved.aborted === !0 ? "aborted" : "done"
7585
- })), !0);
7718
+ function candidateAssignments(involved, field) {
7719
+ const candidates = involved.flatMap(entry => entry.insight.blockedBy).flatMap(atom => atom.requirement !== void 0 ? [ atom.requirement ] : []).filter(requirement => requirement.target.variable === "fields").filter(requirement => requirement.target.path[0] === field).flatMap(assignmentFor).filter(fabricatable);
7720
+ return groqConditionDescribe.dedupeBy(candidates, assignment => JSON.stringify([ assignment.target.path, assignment.value ]));
7586
7721
  }
7587
7722
 
7588
- async function recordOrphanedPropagation({ctx: ctx, mutation: mutation, child: child}) {
7589
- const childUri = invariants.selfGdr(child);
7590
- mutation.history.some(h => h._type === "subworkflowOrphaned" && h.instanceRef.id === childUri) || (mutation.history.push({
7591
- _key: randomKey(),
7592
- _type: "subworkflowOrphaned",
7593
- at: ctx.now,
7594
- instanceRef: {
7595
- id: childUri,
7596
- type: invariants.WORKFLOW_INSTANCE_TYPE
7597
- },
7598
- detail: `Instance "${child._id}" names this instance as its parent but no subworkflow-registry row matches it, so its state cannot drive any gate here.`
7599
- }), await persist(ctx, mutation));
7723
+ function assignmentFor(requirement) {
7724
+ switch (requirement.kind) {
7725
+ case "equals":
7726
+ return [ {
7727
+ target: requirement.target,
7728
+ value: requirement.value
7729
+ } ];
7730
+
7731
+ case "truthy":
7732
+ return [ {
7733
+ target: requirement.target,
7734
+ value: !0
7735
+ } ];
7736
+
7737
+ case "falsy":
7738
+ return [ {
7739
+ target: requirement.target,
7740
+ value: !1
7741
+ } ];
7742
+
7743
+ case "differs":
7744
+ case "defined":
7745
+ case "undefined":
7746
+ case "compares":
7747
+ return [];
7748
+ }
7600
7749
  }
7601
7750
 
7602
- function startMutation(instance) {
7603
- return {
7604
- minReaderModel: invariants.minReaderModelOf(instance),
7605
- currentStage: instance.currentStage,
7606
- fields: (instance.fields ?? []).map(s => ({
7607
- ...s
7608
- })),
7609
- stages: instance.stages.map(s => ({
7610
- ...s,
7611
- fields: (s.fields ?? []).map(entry => ({
7612
- ...entry
7613
- })),
7614
- activities: s.activities.map(t => ({
7615
- ...t,
7616
- ...t.fields !== void 0 ? {
7617
- fields: t.fields.map(entry => ({
7618
- ...entry
7619
- }))
7620
- } : {}
7621
- }))
7622
- })),
7623
- subworkflows: (instance.subworkflows ?? []).map(row => ({
7624
- ...row,
7625
- ...row.abortPending !== void 0 ? {
7626
- abortPending: {
7627
- ...row.abortPending
7628
- }
7629
- } : {},
7630
- ...row.resolved !== void 0 ? {
7631
- resolved: {
7632
- ...row.resolved
7633
- }
7634
- } : {}
7635
- })),
7636
- pendingEffects: [ ...instance.pendingEffects ],
7637
- effectHistory: [ ...instance.effectHistory ],
7638
- context: [ ...instance.context ],
7639
- history: [ ...instance.history ],
7640
- processedRequests: [ ...instance.processedRequests ?? [] ],
7641
- lastChangedAt: instance.lastChangedAt,
7642
- ...instance.completedAt !== void 0 ? {
7643
- completedAt: instance.completedAt
7644
- } : {},
7645
- ...instance.abortedAt !== void 0 ? {
7646
- abortedAt: instance.abortedAt
7647
- } : {},
7648
- pendingCreates: []
7649
- };
7751
+ function fabricatable(assignment) {
7752
+ return assignment.target.path.every(segment => typeof segment != "number" || segment <= groqConditionDescribe.MAX_COUNTERFACTUAL_INDEX);
7650
7753
  }
7651
7754
 
7652
- function instanceStateFields(src) {
7653
- const state = {
7654
- currentStage: src.currentStage,
7655
- fields: src.fields,
7656
- stages: src.stages,
7657
- subworkflows: src.subworkflows ?? [],
7658
- pendingEffects: src.pendingEffects,
7659
- effectHistory: src.effectHistory,
7660
- context: src.context,
7661
- history: src.history,
7662
- processedRequests: src.processedRequests ?? [],
7663
- ...src.completedAt !== void 0 ? {
7664
- completedAt: src.completedAt
7665
- } : {},
7666
- ...src.abortedAt !== void 0 ? {
7667
- abortedAt: src.abortedAt
7668
- } : {}
7669
- };
7670
- return {
7671
- ...invariants.modelStampFor({
7672
- documentType: "instance",
7673
- document: state,
7674
- storedMinReaderModel: src.minReaderModel
7675
- }),
7676
- ...state
7677
- };
7755
+ async function subjectResourceGrants(args) {
7756
+ const {clientForGdr: clientForGdr, instance: instance} = args, clients = /* @__PURE__ */ new Map;
7757
+ for (const {parsed: parsed, resource: resource} of foreignSubjectRefs(instance)) {
7758
+ const key = invariants.resourceGdr(resource);
7759
+ clients.has(key) || clients.set(key, {
7760
+ resource: resource,
7761
+ client: clientForGdr(parsed)
7762
+ });
7763
+ }
7764
+ const resolved = await Promise.all([ ...clients.entries() ].map(async ([key, entry]) => {
7765
+ const path = aclPathForResource(entry.resource);
7766
+ if (path === void 0) return;
7767
+ const grants = await grantsForClientPath(entry.client, path);
7768
+ return grants === void 0 ? void 0 : [ key, grants ];
7769
+ }));
7770
+ return new Map(resolved.filter(entry => entry !== void 0));
7678
7771
  }
7679
7772
 
7680
- function liveViewContext(ctx, mutation) {
7681
- const materialized = materializeInstance(ctx.instance, mutation);
7682
- return {
7683
- ...ctx,
7684
- instance: materialized,
7685
- snapshot: overlayInstanceInSnapshot(ctx.snapshot, materialized)
7686
- };
7773
+ async function evaluateInstance(args) {
7774
+ const {client: client, tag: tag, workflowResource: workflowResource, instanceId: instanceId, resourceClients: resourceClients} = args, now = (args.clock ?? wallClock)();
7775
+ invariants.validateTag(tag);
7776
+ const {actor: actor, grants: grants} = await resolveAccess(client, {
7777
+ ...args.grantsFromPath !== void 0 ? {
7778
+ grantsFromPath: args.grantsFromPath
7779
+ } : {}
7780
+ }), instance = await reload({
7781
+ client: client,
7782
+ instanceId: instanceId,
7783
+ tag: tag
7784
+ }), definition = invariants.parseDefinitionSnapshot(instance), clientForGdr = buildClientForGdr({
7785
+ client: client,
7786
+ workflowResource: workflowResource,
7787
+ resourceClients: resourceClients
7788
+ }), snapshot = await hydrateSnapshot({
7789
+ client: client,
7790
+ clientForGdr: clientForGdr,
7791
+ instance: instance
7792
+ }), guards = await verdictGuardsForInstance(client, instance._id), resourceGrants = await subjectResourceGrants({
7793
+ clientForGdr: clientForGdr,
7794
+ instance: instance
7795
+ });
7796
+ return evaluateFromSnapshot({
7797
+ instance: instance,
7798
+ definition: definition,
7799
+ actor: actor,
7800
+ snapshot: snapshot,
7801
+ guards: guards,
7802
+ now: now,
7803
+ resourceGrants: resourceGrants,
7804
+ ...grants !== void 0 ? {
7805
+ grants: grants
7806
+ } : {}
7807
+ });
7687
7808
  }
7688
7809
 
7689
- function materializeInstance(base, mutation) {
7690
- return {
7691
- ...base,
7692
- currentStage: mutation.currentStage,
7693
- fields: mutation.fields,
7694
- stages: mutation.stages,
7695
- subworkflows: mutation.subworkflows,
7696
- context: mutation.context,
7697
- pendingEffects: mutation.pendingEffects,
7698
- effectHistory: mutation.effectHistory
7810
+ function memoizedByName(render) {
7811
+ const rendered = /* @__PURE__ */ new Map;
7812
+ return activityName => {
7813
+ const hit = rendered.get(activityName);
7814
+ if (hit !== void 0) return hit;
7815
+ const scope = render(activityName);
7816
+ return rendered.set(activityName, scope), scope;
7699
7817
  };
7700
7818
  }
7701
7819
 
7702
- async function persist(ctx, mutation) {
7703
- stampExecutionContext(ctx, mutation);
7704
- const set = {
7705
- ...instanceStateFields(mutation),
7706
- lastChangedAt: ctx.now
7707
- }, pendingCreates = mutation.pendingCreates;
7708
- if (pendingCreates.length === 0) return ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev).commit(SYNC_COMMIT);
7709
- const tx = ctx.client.transaction();
7710
- for (const {body: body} of pendingCreates) tx.create(body);
7711
- tx.patch(ctx.client.patch(ctx.instance._id).set(set).ifRevisionId(ctx.instance._rev)),
7712
- await tx.commit(), mutation.pendingCreates = [];
7713
- const actorForPriming = ctx.actor;
7714
- for (const {body: body, started: started} of pendingCreates) try {
7715
- await primeInitialStage({
7716
- client: ctx.client,
7717
- instanceId: body._id,
7718
- actor: actorForPriming,
7719
- clientForGdr: ctx.clientForGdr,
7720
- refSurface: ctx.refSurface,
7721
- clock: ctx.clock,
7722
- executionContext: ctx.executionContext,
7723
- telemetry: ctx.telemetry
7724
- }), await cascadeAutoTransitions({
7725
- client: ctx.client,
7726
- instanceId: body._id,
7727
- actor: actorForPriming,
7728
- clientForGdr: ctx.clientForGdr,
7729
- refSurface: ctx.refSurface,
7730
- clock: ctx.clock,
7731
- executionContext: ctx.executionContext,
7732
- telemetry: ctx.telemetry
7733
- }), await propagateToAncestors({
7734
- client: ctx.client,
7735
- instanceId: body._id,
7736
- actor: actorForPriming,
7737
- clientForGdr: ctx.clientForGdr,
7738
- refSurface: ctx.refSurface,
7739
- clock: ctx.clock,
7740
- executionContext: ctx.executionContext,
7741
- telemetry: ctx.telemetry
7742
- }), ctx.telemetry.log(WorkflowInstanceStarted, started);
7743
- } catch (cause) {
7744
- throw cause instanceof WorkflowStateDivergedError ? cause : new WorkflowStateDivergedError({
7745
- instanceId: ctx.instance._id,
7746
- guardError: cause,
7747
- reason: `spawned child "${body._id}" failed to settle after the spawn transaction committed`
7820
+ function currentStageOf(instance, definition) {
7821
+ try {
7822
+ return findStage(definition, instance.currentStage);
7823
+ } catch (err) {
7824
+ throw new Error(`Instance "${instance._id}" currentStage "${instance.currentStage}" not in definition`, {
7825
+ cause: err
7748
7826
  });
7749
7827
  }
7750
- const reloaded = await getInstanceDocument(ctx.client, ctx.instance._id);
7751
- if (!reloaded) throw new Error(`Instance ${ctx.instance._id} disappeared after transaction commit`);
7752
- return reloaded;
7753
7828
  }
7754
7829
 
7755
- function stampExecutionContext(ctx, mutation) {
7756
- const stamp = ctx.executionContext, appended = mutation.history.slice(ctx.instance.history.length);
7757
- mutation.history = [ ...mutation.history.slice(0, ctx.instance.history.length), ...stampHistoryEntries(appended, stamp) ];
7758
- for (const {body: body} of mutation.pendingCreates) body.history = stampHistoryEntries(body.history, stamp);
7830
+ async function explainSite(args) {
7831
+ const {site: site, condition: condition, params: params, snapshot: snapshot, sites: sites} = args, insight = await groqConditionDescribe.explainCondition({
7832
+ condition: condition,
7833
+ dataset: snapshot.docs,
7834
+ params: params
7835
+ });
7836
+ return sites.push({
7837
+ site: site,
7838
+ condition: condition,
7839
+ params: params,
7840
+ insight: insight
7841
+ }), insight;
7759
7842
  }
7760
7843
 
7761
- function currentStageEntry(mutation) {
7762
- const entry = findOpenStageEntry(mutation);
7763
- if (entry === void 0) throw new Error(`Mutation invariant broken: no current (un-exited) StageEntry for currentStage "${mutation.currentStage}"`);
7764
- return entry;
7844
+ async function evaluateFromSnapshot(args) {
7845
+ const {instance: instance, definition: definition, actor: actor, grants: grants, snapshot: snapshot} = args, now = args.now ?? wallClock(), stage = currentStageOf(instance, definition), autonomy = autonomyOf(definition), stageAutonomy2 = stageAutonomyOf(autonomy, stage.name), fireConsequence = (activity, action) => whatIfFireAction({
7846
+ instance: instance,
7847
+ definition: definition,
7848
+ snapshot: snapshot,
7849
+ now: now,
7850
+ actor: actor,
7851
+ stage: stage,
7852
+ activity: activity,
7853
+ action: action
7854
+ }), scopeSource = {
7855
+ instance: instance,
7856
+ definition: definition,
7857
+ snapshot: snapshot,
7858
+ now: now
7859
+ }, can = await advisoryCan({
7860
+ instance: instance,
7861
+ actor: actor,
7862
+ grants: grants
7863
+ }), scope = await renderConditionScope(scopeSource, {
7864
+ actor: actor,
7865
+ vars: {
7866
+ can: can
7867
+ }
7868
+ }), scopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
7869
+ activityName: activityName,
7870
+ actor: actor,
7871
+ vars: {
7872
+ can: can
7873
+ }
7874
+ })), cascadeScopeForActivity = memoizedByName(activityName => renderConditionScope(scopeSource, {
7875
+ activityName: activityName
7876
+ })), currentActivityEntries = findOpenStageEntry(instance)?.activities ?? [], guardDenial = await instanceGuardReason({
7877
+ instance: instance,
7878
+ actor: actor,
7879
+ guards: args.guards
7880
+ }), subjectDenials = await forecastSubjectDenials({
7881
+ instance: instance,
7882
+ actor: actor,
7883
+ snapshot: snapshot,
7884
+ resourceGrants: args.resourceGrants
7885
+ }), subjectPermissionReason = subjectDenials.length > 0 ? {
7886
+ kind: "subject-permission-denied",
7887
+ denied: subjectDenials
7888
+ } : void 0, sites = [], activityEvaluations = [];
7889
+ for (const activity of stage.activities ?? []) activityEvaluations.push(await evaluateActivity({
7890
+ activity: activity,
7891
+ statusEntry: currentActivityEntries.find(t => t.name === activity.name),
7892
+ instance: instance,
7893
+ snapshot: snapshot,
7894
+ activityScope: await scopeForActivity(activity.name),
7895
+ cascadeActivityScope: () => cascadeScopeForActivity(activity.name),
7896
+ stageHasExits: !isTerminalStage(stage),
7897
+ guardDenial: guardDenial,
7898
+ subjectPermissionReason: subjectPermissionReason,
7899
+ sites: sites,
7900
+ autonomy: activityAutonomyOf(stageAutonomy2, activity.name),
7901
+ fireConsequence: fireConsequence
7902
+ }));
7903
+ const cascadeParams = await renderConditionScope(scopeSource), transitionEvaluations = [];
7904
+ for (const transition of stage.transitions ?? []) {
7905
+ const insight = await explainSite({
7906
+ site: {
7907
+ kind: "transition",
7908
+ transition: transition.name
7909
+ },
7910
+ condition: transition.when,
7911
+ params: cascadeParams,
7912
+ snapshot: snapshot,
7913
+ sites: sites
7914
+ });
7915
+ transitionEvaluations.push({
7916
+ transition: transition,
7917
+ whenSatisfied: insight.outcome === "satisfied",
7918
+ unevaluable: insight.outcome === "unevaluable",
7919
+ insight: insight
7920
+ });
7921
+ }
7922
+ const currentStage = {
7923
+ stage: stage,
7924
+ activities: activityEvaluations,
7925
+ transitions: transitionEvaluations,
7926
+ autonomy: stageAutonomy2
7927
+ }, pendingOnYou = activityEvaluations.filter(t => t.pendingOnActor), canInteract = activityEvaluations.some(t => t.actions.some(a => a.allowed)), editGuardDenial = guardDenial?.kind === "mutation-guard-denied" ? guardDenial : void 0, editableFields = await evaluateEditableFields({
7928
+ instance: instance,
7929
+ definition: definition,
7930
+ stage: stage,
7931
+ snapshot: snapshot,
7932
+ scope: scope,
7933
+ scopeForActivity: scopeForActivity,
7934
+ guardDenial: editGuardDenial,
7935
+ sites: sites
7936
+ });
7937
+ return {
7938
+ instance: instance,
7939
+ definition: definition,
7940
+ actor: actor,
7941
+ currentStage: currentStage,
7942
+ pendingOnYou: pendingOnYou,
7943
+ canInteract: canInteract,
7944
+ editableFields: editableFields,
7945
+ fieldInsights: await buildFieldInsights({
7946
+ sites: sites,
7947
+ snapshot: snapshot
7948
+ }),
7949
+ autonomy: autonomy
7950
+ };
7765
7951
  }
7766
7952
 
7767
- function currentActivities(mutation) {
7768
- return currentStageEntry(mutation).activities;
7769
- }
7953
+ const AUTONOMY_CACHE = /* @__PURE__ */ new WeakMap;
7770
7954
 
7771
- function findActivityInCurrentStage(ctx, activityName) {
7772
- const stage = findStage(ctx.definition, ctx.instance.currentStage), activity = (stage.activities ?? []).find(t => t.name === activityName);
7773
- if (activity === void 0) throw new invariants.ContractViolationError(`Activity "${activityName}" not found in current stage "${stage.name}" of ${ctx.definition.name}`);
7774
- return {
7775
- stage: stage,
7776
- activity: activity
7777
- };
7955
+ function autonomyOf(definition) {
7956
+ const hit = AUTONOMY_CACHE.get(definition);
7957
+ if (hit !== void 0) return hit;
7958
+ const derived = deriveWorkflowAutonomy(definition);
7959
+ return AUTONOMY_CACHE.set(definition, derived), derived;
7778
7960
  }
7779
7961
 
7780
- function requireMutationActivityEntry(mutation, activity) {
7781
- const mutEntry = currentActivities(mutation).find(t => t.name === activity);
7782
- if (mutEntry === void 0) throw new Error(`Activity "${activity}" disappeared from mutation copy — invariant broken`);
7783
- return mutEntry;
7962
+ async function evaluateEditableFields(args) {
7963
+ const {instance: instance, definition: definition, stage: stage, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, guardDenial: guardDenial, sites: sites} = args, fields = [];
7964
+ for (const site of editableFieldsInStage(definition, stage)) {
7965
+ const window = fieldWindowOpen(instance, site), insight = await editPredicateInsight({
7966
+ site: site,
7967
+ snapshot: snapshot,
7968
+ scope: scope,
7969
+ scopeForActivity: scopeForActivity,
7970
+ sites: sites
7971
+ }), predicateSatisfied = insight === void 0 || insight.outcome === "satisfied", reason = editDisabledReason({
7972
+ effective: site.effective,
7973
+ instance: instance,
7974
+ window: window,
7975
+ guardDenial: guardDenial,
7976
+ predicateSatisfied: predicateSatisfied
7977
+ }), value = readFieldValue(instance, site);
7978
+ fields.push({
7979
+ scope: site.scope,
7980
+ ...site.activity !== void 0 ? {
7981
+ activity: site.activity
7982
+ } : {},
7983
+ name: site.name,
7984
+ type: site.type,
7985
+ ...site.title !== void 0 ? {
7986
+ title: site.title
7987
+ } : {},
7988
+ ...site.validation !== void 0 ? {
7989
+ validation: site.validation
7990
+ } : {},
7991
+ value: value,
7992
+ editable: reason === void 0,
7993
+ ...reason !== void 0 ? {
7994
+ disabledReason: reason
7995
+ } : {},
7996
+ ...fieldProvenance(instance, site.ref),
7997
+ ...insight !== void 0 ? {
7998
+ insight: insight
7999
+ } : {}
8000
+ });
8001
+ }
8002
+ return fields;
7784
8003
  }
7785
8004
 
7786
- function findCurrentStageEntry(instance) {
7787
- return findOpenStageEntry(instance);
8005
+ async function editPredicateInsight(args) {
8006
+ const {site: site, snapshot: snapshot, scope: scope, scopeForActivity: scopeForActivity, sites: sites} = args;
8007
+ if (typeof site.effective != "string") return;
8008
+ const params = site.scope === "activity" && site.activity !== void 0 ? await scopeForActivity(site.activity) : scope;
8009
+ return explainSite({
8010
+ site: {
8011
+ kind: "editable-field",
8012
+ scope: site.scope,
8013
+ name: site.name,
8014
+ ...site.activity !== void 0 ? {
8015
+ activity: site.activity
8016
+ } : {}
8017
+ },
8018
+ condition: site.effective,
8019
+ params: params,
8020
+ snapshot: snapshot,
8021
+ sites: sites
8022
+ });
7788
8023
  }
7789
8024
 
7790
- function findCurrentActivities(instance) {
7791
- return findCurrentStageEntry(instance)?.activities ?? [];
8025
+ async function forecastSubjectDenials(args) {
8026
+ const {instance: instance, actor: actor, snapshot: snapshot, resourceGrants: resourceGrants} = args;
8027
+ if (resourceGrants === void 0 || resourceGrants.size === 0) return [];
8028
+ const denials = [], seen = /* @__PURE__ */ new Set;
8029
+ for (const {ref: ref, parsed: parsed, resource: resource} of foreignSubjectRefs(instance)) {
8030
+ if (seen.has(ref.id)) continue;
8031
+ seen.add(ref.id);
8032
+ const grants = resourceGrants.get(invariants.resourceGdr(resource));
8033
+ if (grants === void 0) continue;
8034
+ const doc = snapshot.docs.find(d => d._id === ref.id);
8035
+ doc !== void 0 && await subjectUpdateAllowed({
8036
+ doc: doc,
8037
+ parsed: parsed,
8038
+ grants: grants,
8039
+ actorId: actor.id
8040
+ }) === !1 && denials.push({
8041
+ subject: ref.id,
8042
+ resource: invariants.resourceGdr(resource),
8043
+ permission: "update"
8044
+ });
8045
+ }
8046
+ return denials;
7792
8047
  }
7793
8048
 
7794
- async function completeEffect(args) {
7795
- const {client: client, instanceId: instanceId, effectKey: effectKey, status: status, outputs: outputs, ops: ops, detail: detail, error: error, durationMs: durationMs, requestRecord: requestRecord, options: options} = args;
7796
- if (status !== "done" && status !== "failed") throw new invariants.ContractViolationError(`completeEffect: status must be "done" or "failed", got ${JSON.stringify(status)} — "cancelled" is engine-stamped by abort, never reportable`);
7797
- return retryOnRevisionConflict({
7798
- client: client,
7799
- instanceId: instanceId,
7800
- options: options,
7801
- commit: ctx => commitCompleteEffect({
7802
- ctx: ctx,
7803
- effectKey: effectKey,
7804
- status: status,
7805
- outputs: outputs,
7806
- ops: ops,
7807
- detail: detail,
7808
- error: error,
7809
- durationMs: durationMs,
7810
- requestRecord: requestRecord,
7811
- actor: options.actor
7812
- }),
7813
- onExhausted: () => new ConcurrentCompleteEffectError({
7814
- instanceId: instanceId,
7815
- effectKey: effectKey,
7816
- attempts: CONCURRENT_COMMIT_MAX_ATTEMPTS
7817
- })
7818
- });
8049
+ async function subjectUpdateAllowed(args) {
8050
+ const {doc: doc, parsed: parsed, grants: grants, actorId: actorId} = args;
8051
+ try {
8052
+ return await grantsPermissionOn({
8053
+ document: {
8054
+ ...doc,
8055
+ _id: parsed.documentId
8056
+ },
8057
+ grants: grants,
8058
+ permission: "update",
8059
+ userId: actorId
8060
+ });
8061
+ } catch (err) {
8062
+ console.warn(`workflow: subject-write forecast skipped for "${doc._id}" — evaluating that resource's grants failed (the lake still enforces the write). Original error: ${invariants.errorMessage(err)}`);
8063
+ return;
8064
+ }
7819
8065
  }
7820
8066
 
7821
- function buildEffectHistoryEntry(pending, outcome) {
7822
- const {status: status, ranAt: ranAt, actor: actor, detail: detail, error: error, durationMs: durationMs, outputs: outputs} = outcome, resolvedActor = actor ?? pending.actor;
8067
+ async function evaluateActivity(args) {
8068
+ const {activity: activity, statusEntry: statusEntry, instance: instance, snapshot: snapshot, activityScope: activityScope, cascadeActivityScope: cascadeActivityScope, stageHasExits: stageHasExits, guardDenial: guardDenial, subjectPermissionReason: subjectPermissionReason, sites: sites, autonomy: autonomy, fireConsequence: fireConsequence} = args, status = statusEntry?.status ?? "skipped", assigned = activityScope.assigned === !0, {unmetRequirements: unmetRequirements, ...conditionInsights} = await explainActivityConditions({
8069
+ activity: activity,
8070
+ activityScope: activityScope,
8071
+ cascadeActivityScope: cascadeActivityScope,
8072
+ snapshot: snapshot,
8073
+ sites: sites
8074
+ }), requirementsReason = unmetRequirements.length > 0 ? {
8075
+ kind: "requirements-unmet",
8076
+ unmetRequirements: unmetRequirements
8077
+ } : void 0, actions = [];
8078
+ for (const action of activity.actions ?? []) actions.push(await evaluateAction({
8079
+ action: action,
8080
+ activityName: activity.name,
8081
+ status: status,
8082
+ instance: instance,
8083
+ snapshot: snapshot,
8084
+ activityScope: activityScope,
8085
+ cascadeActivityScope: cascadeActivityScope,
8086
+ stageHasExits: stageHasExits,
8087
+ guardDenial: guardDenial,
8088
+ subjectPermissionReason: subjectPermissionReason,
8089
+ requirementsReason: requirementsReason,
8090
+ sites: sites,
8091
+ fireConsequence: a => fireConsequence(activity, a)
8092
+ }));
7823
8093
  return {
7824
- _key: pending._key,
7825
- name: pending.name,
7826
- ...pending.title !== void 0 ? {
7827
- title: pending.title
7828
- } : {},
7829
- ...pending.description !== void 0 ? {
7830
- description: pending.description
7831
- } : {},
7832
- params: pending.params,
7833
- origin: pending.origin,
7834
- ...resolvedActor !== void 0 ? {
7835
- actor: resolvedActor
7836
- } : {},
7837
- ...pending.stageEntryKey !== void 0 ? {
7838
- stageEntryKey: pending.stageEntryKey
7839
- } : {},
7840
- ranAt: ranAt,
7841
- ...durationMs !== void 0 ? {
7842
- durationMs: durationMs
7843
- } : {},
8094
+ activity: activity,
7844
8095
  status: status,
7845
- ...detail !== void 0 ? {
7846
- detail: detail
7847
- } : {},
7848
- ...error !== void 0 ? {
7849
- error: error
8096
+ kind: invariants.deriveActivityKind(activity),
8097
+ classification: invariants.deriveExecutorClassification(activity),
8098
+ autonomy: autonomy,
8099
+ pendingOnActor: status === "active" && assigned,
8100
+ scopedOut: isFilterScopedOut({
8101
+ status: status,
8102
+ startedAt: statusEntry?.startedAt
8103
+ }),
8104
+ ...unmetRequirements.length > 0 ? {
8105
+ unmetRequirements: unmetRequirements
7850
8106
  } : {},
7851
- ...outputs !== void 0 ? {
7852
- outputs: outputs
7853
- } : {}
8107
+ ...conditionInsights,
8108
+ actions: actions
7854
8109
  };
7855
8110
  }
7856
8111
 
7857
- function buildEffectSettlement(pending, outcome) {
7858
- const {status: status, ranAt: ranAt, actor: actor, detail: detail, outputs: outputs} = outcome;
8112
+ async function explainActivityConditions({activity: activity, activityScope: activityScope, cascadeActivityScope: cascadeActivityScope, snapshot: snapshot, sites: sites}) {
8113
+ const explainAt = (site, condition) => explainSite({
8114
+ site: site,
8115
+ condition: condition,
8116
+ params: activityScope,
8117
+ snapshot: snapshot,
8118
+ sites: sites
8119
+ }), requirementEntries = [];
8120
+ for (const [name, condition] of Object.entries(activity.requirements ?? {})) requirementEntries.push([ name, await explainAt({
8121
+ kind: "requirement",
8122
+ activity: activity.name,
8123
+ requirement: name
8124
+ }, condition) ]);
8125
+ const requirementInsights = Object.fromEntries(requirementEntries), unmetRequirements = requirementEntries.filter(([, insight]) => insight.outcome !== "satisfied").map(([name]) => name), filterInsight = activity.filter !== void 0 ? await explainSite({
8126
+ site: {
8127
+ kind: "activity-filter",
8128
+ activity: activity.name
8129
+ },
8130
+ condition: activity.filter,
8131
+ params: await cascadeActivityScope(),
8132
+ snapshot: snapshot,
8133
+ sites: sites
8134
+ }) : void 0;
7859
8135
  return {
7860
- run: buildEffectHistoryEntry(pending, outcome),
7861
- event: {
7862
- _key: randomKey(),
7863
- _type: "effectCompleted",
7864
- at: ranAt,
7865
- effectKey: pending._key,
7866
- effect: pending.name,
7867
- status: status,
7868
- ...outputs !== void 0 ? {
7869
- outputs: outputs
7870
- } : {},
7871
- ...detail !== void 0 ? {
7872
- detail: detail
7873
- } : {},
7874
- ...actor !== void 0 ? {
7875
- actor: actor
7876
- } : {}
7877
- }
8136
+ unmetRequirements: unmetRequirements,
8137
+ ...activity.requirements !== void 0 ? {
8138
+ requirementInsights: requirementInsights
8139
+ } : {},
8140
+ ...filterInsight !== void 0 ? {
8141
+ filterInsight: filterInsight
8142
+ } : {}
7878
8143
  };
7879
8144
  }
7880
8145
 
7881
- function validateEffectOutputs(args) {
7882
- const {outputs: outputs, declared: declared, effectName: effectName} = args, byName = new Map(declared.map(shape => [ shape.name, shape ])), issues = [];
7883
- for (const [key, value] of Object.entries(outputs)) {
7884
- const shape = byName.get(key);
7885
- if (shape === void 0) {
7886
- issues.push(`"${key}" is not a declared output`);
7887
- continue;
7888
- }
7889
- const shapeIssues = effectOutputIssues(shape, value);
7890
- shapeIssues !== void 0 && issues.push(...shapeIssues.map(i => `"${key}": ${i}`));
7891
- }
7892
- if (issues.length > 0) throw new EffectOutputsInvalidError({
7893
- effect: effectName,
7894
- issues: issues
8146
+ async function evaluateAction(args) {
8147
+ const {action: action, activityName: activityName, snapshot: snapshot, activityScope: activityScope, sites: sites} = args, conditionScope = invariants.isCascadeFired(action) ? await args.cascadeActivityScope() : activityScope, insights = await explainActionGates({
8148
+ action: action,
8149
+ activityName: activityName,
8150
+ conditionScope: conditionScope,
8151
+ snapshot: snapshot,
8152
+ sites: sites
7895
8153
  });
7896
- }
7897
-
7898
- function effectOutputIssues(shape, value) {
7899
- return invariants.checkFieldValue({
7900
- entryType: shape.type,
7901
- value: value,
7902
- ...shape.fields !== void 0 ? {
7903
- fields: shape.fields
7904
- } : {},
7905
- ...shape.of !== void 0 ? {
7906
- of: shape.of
7907
- } : {},
7908
- ...shape.options !== void 0 ? {
7909
- options: shape.options
7910
- } : {},
7911
- ...shape.validation !== void 0 ? {
7912
- validation: shape.validation
7913
- } : {}
8154
+ if (action.when !== void 0) return triggeredActionVerdict({
8155
+ action: action,
8156
+ when: action.when,
8157
+ insights: insights
7914
8158
  });
8159
+ const verdict = fireableActionVerdict({
8160
+ args: args,
8161
+ insights: insights
8162
+ }), firing = await args.fireConsequence(action);
8163
+ return firing !== void 0 ? {
8164
+ ...verdict,
8165
+ firing: firing
8166
+ } : verdict;
7915
8167
  }
7916
8168
 
7917
- function requirePendingEffect(instance, effectKey) {
7918
- const pending = instance.pendingEffects.find(e => e._key === effectKey);
7919
- if (pending !== void 0) return pending;
7920
- const run = instance.effectHistory.find(e => e._key === effectKey), settled = run !== void 0 ? {
7921
- status: run.status,
7922
- ranAt: run.ranAt,
7923
- ...run.detail !== void 0 ? {
7924
- detail: run.detail
7925
- } : {}
7926
- } : void 0;
7927
- throw new invariants.EffectNotFoundError({
7928
- instanceId: instance._id,
7929
- effectKey: effectKey,
7930
- ...settled !== void 0 ? {
7931
- settled: settled
8169
+ async function explainActionGates({action: action, activityName: activityName, conditionScope: conditionScope, snapshot: snapshot, sites: sites}) {
8170
+ const insight = action.filter !== void 0 ? await explainSite({
8171
+ site: {
8172
+ kind: "action",
8173
+ activity: activityName,
8174
+ action: action.name
8175
+ },
8176
+ condition: action.filter,
8177
+ params: conditionScope,
8178
+ snapshot: snapshot,
8179
+ sites: sites
8180
+ }) : void 0, whenInsight = action.when !== void 0 ? await explainSite({
8181
+ site: {
8182
+ kind: "action-when",
8183
+ activity: activityName,
8184
+ action: action.name
8185
+ },
8186
+ condition: action.when,
8187
+ params: conditionScope,
8188
+ snapshot: snapshot,
8189
+ sites: sites
8190
+ }) : void 0;
8191
+ return {
8192
+ ...insight !== void 0 ? {
8193
+ insight: insight
8194
+ } : {},
8195
+ ...whenInsight !== void 0 ? {
8196
+ whenInsight: whenInsight
7932
8197
  } : {}
7933
- });
8198
+ };
7934
8199
  }
7935
8200
 
7936
- function validateCompletionInput({pending: pending, definition: definition, status: status, ops: ops, outputs: outputs}) {
7937
- if (status === "failed" && ops !== void 0 && ops.length > 0) throw new EffectOpsInvalidError({
7938
- effect: pending.name,
7939
- issues: [ "ops cannot accompany a failed completion — field.set the outcome on a done completion instead" ]
7940
- });
7941
- if (status === "failed" && outputs !== void 0) throw new EffectOutputsInvalidError({
7942
- effect: pending.name,
7943
- issues: [ "outputs cannot accompany a failed completion — report outputs on a done completion instead" ]
7944
- });
7945
- if (outputs !== void 0) {
7946
- const declared = findEffect(definition, pending.name)?.outputs ?? [];
7947
- validateEffectOutputs({
7948
- outputs: outputs,
7949
- declared: declared,
7950
- effectName: pending.name
7951
- });
7952
- }
7953
- return ops !== void 0 ? validateEffectOps(ops, pending.name) : [];
8201
+ function triggeredActionVerdict({action: action, when: when, insights: insights}) {
8202
+ return insights.insight !== void 0 && insights.insight.outcome === "unsatisfied" ? disabled({
8203
+ action: action,
8204
+ reason: {
8205
+ kind: "filter-failed",
8206
+ filter: action.filter ?? ""
8207
+ },
8208
+ ...insights
8209
+ }) : {
8210
+ ...actionEvaluationIdentity(action),
8211
+ allowed: !1,
8212
+ triggered: !0,
8213
+ disabledReason: {
8214
+ kind: "cascade-fired",
8215
+ when: when
8216
+ },
8217
+ ...insights
8218
+ };
7954
8219
  }
7955
8220
 
7956
- async function commitCompleteEffect({ctx: ctx, effectKey: effectKey, status: status, outputs: outputs, ops: ops, detail: detail, error: error, durationMs: durationMs, requestRecord: requestRecord, actor: actor}) {
7957
- assertRequestUnprocessed({
7958
- instance: ctx.instance,
7959
- record: requestRecord,
7960
- now: ctx.now
7961
- });
7962
- const pending = requirePendingEffect(ctx.instance, effectKey), validatedOps = validateCompletionInput({
7963
- pending: pending,
7964
- definition: ctx.definition,
7965
- status: status,
7966
- ops: ops,
7967
- outputs: outputs
7968
- }), mutation = startMutation(ctx.instance);
7969
- recordProcessedRequest({
7970
- mutation: mutation,
7971
- record: requestRecord,
7972
- now: ctx.now
7973
- }), mutation.pendingEffects = mutation.pendingEffects.filter(e => e._key !== effectKey);
7974
- const ranAt = ctx.now, settlement = buildEffectSettlement(pending, {
8221
+ function fireableActionVerdict({args: args, insights: insights}) {
8222
+ const {action: action, status: status, instance: instance, stageHasExits: stageHasExits, guardDenial: guardDenial} = args, lifecycle = lifecycleReason({
8223
+ instance: instance,
7975
8224
  status: status,
7976
- ranAt: ranAt,
7977
- actor: actor,
7978
- detail: detail,
7979
- error: error,
7980
- durationMs: durationMs,
7981
- outputs: outputs
8225
+ stageHasExits: stageHasExits
7982
8226
  });
7983
- mutation.effectHistory.push(settlement.run);
7984
- const wroteEffectOutputs = status === "done" && outputs !== void 0;
7985
- mutation.history.push(settlement.event);
7986
- const ranOps = await runOps({
7987
- ops: validatedOps,
7988
- mutation: mutation,
7989
- stage: ctx.instance.currentStage,
7990
- origin: {
7991
- effect: pending.name
8227
+ if (lifecycle !== void 0) return disabled({
8228
+ action: action,
8229
+ reason: lifecycle,
8230
+ ...insights
8231
+ });
8232
+ if (guardDenial !== void 0) return disabled({
8233
+ action: action,
8234
+ reason: guardDenial,
8235
+ ...insights
8236
+ });
8237
+ if (args.subjectPermissionReason !== void 0 && (action.effects?.length ?? 0) > 0) return disabled({
8238
+ action: action,
8239
+ reason: args.subjectPermissionReason,
8240
+ ...insights
8241
+ });
8242
+ if (args.requirementsReason !== void 0) return disabled({
8243
+ action: action,
8244
+ reason: args.requirementsReason,
8245
+ ...insights
8246
+ });
8247
+ const {insight: insight} = insights;
8248
+ return action.filter !== void 0 && insight !== void 0 && insight.outcome !== "satisfied" ? disabled({
8249
+ action: action,
8250
+ reason: {
8251
+ kind: "filter-failed",
8252
+ filter: action.filter
7992
8253
  },
7993
- params: pending.params,
7994
- actor: actor,
7995
- self: invariants.selfGdr(ctx.instance),
7996
- now: ranAt,
7997
- snapshot: ctx.snapshot,
7998
- refSurface: ctx.refSurface
7999
- }), needsGuardRefresh = wroteEffectOutputs || ranOps.some(isFieldOp);
8000
- return await persistThenMaybeRefresh({
8001
- ctx: ctx,
8002
- mutation: mutation,
8003
- stageName: ctx.instance.currentStage,
8004
- didChangeState: needsGuardRefresh
8005
- }), {
8006
- effectKey: effectKey,
8007
- effect: pending.name,
8008
- status: status,
8009
- origin: pending.origin.kind
8254
+ ...insights
8255
+ }) : {
8256
+ ...actionEvaluationIdentity(action),
8257
+ allowed: !0,
8258
+ ...insights
8010
8259
  };
8011
8260
  }
8012
8261
 
8013
- function buildQueuedEffect({effect: effect, origin: origin, params: params, actor: actor, now: now, stageEntryKey: stageEntryKey}) {
8014
- const key = randomKey(), pending = {
8015
- _key: key,
8016
- _type: "pendingEffect",
8017
- name: effect.name,
8018
- ...effect.title !== void 0 ? {
8019
- title: effect.title
8020
- } : {},
8021
- ...effect.description !== void 0 ? {
8022
- description: effect.description
8023
- } : {},
8024
- ...effect.bindings !== void 0 ? {
8025
- bindings: effect.bindings
8026
- } : {},
8027
- params: params,
8028
- origin: origin,
8029
- ...actor !== void 0 ? {
8030
- actor: actor
8031
- } : {},
8032
- queuedAt: now,
8033
- ...stageEntryKey !== void 0 ? {
8034
- stageEntryKey: stageEntryKey
8262
+ function actionEvaluationIdentity(action) {
8263
+ return {
8264
+ action: action,
8265
+ ...action.semantics !== void 0 ? {
8266
+ semantics: action.semantics
8035
8267
  } : {}
8036
- }, history = {
8037
- _key: randomKey(),
8038
- _type: "effectQueued",
8039
- at: now,
8040
- effectKey: key,
8041
- effect: effect.name,
8042
- origin: origin
8043
8268
  };
8044
- return {
8045
- pending: pending,
8046
- history: history
8269
+ }
8270
+
8271
+ async function instanceGuardReason({instance: instance, actor: actor, guards: guards}) {
8272
+ if (guards === void 0 || guards.length === 0) return;
8273
+ const denied = await instanceWriteDenials({
8274
+ instance: instance,
8275
+ guards: guards,
8276
+ identity: actor.id
8277
+ });
8278
+ if (denied.length !== 0) return {
8279
+ kind: "mutation-guard-denied",
8280
+ denied: deniedGuardRefs(denied)
8047
8281
  };
8048
8282
  }
8049
8283
 
8050
- async function queueEffects({ctx: ctx, mutation: mutation, effects: effects, origin: origin, actor: actor, opts: opts}) {
8051
- if (!effects || effects.length === 0) return;
8052
- const now = ctx.now, liveCtx = {
8053
- ...ctx,
8054
- instance: materializeInstance(ctx.instance, mutation)
8055
- }, stageEntryKey = opts?.stageEntryKey ?? findOpenStageEntry(liveCtx.instance)?._key, params = await ctxConditionParams(liveCtx, {
8056
- ...opts?.activityName !== void 0 ? {
8057
- activityName: opts.activityName
8058
- } : {},
8059
- ...actor !== void 0 ? {
8060
- actor: actor
8284
+ function lifecycleReason({instance: instance, status: status, stageHasExits: stageHasExits}) {
8285
+ const terminalReason = instanceTerminalReason(instance);
8286
+ if (terminalReason !== void 0) return terminalReason;
8287
+ if (!stageHasExits) return {
8288
+ kind: "stage-terminal",
8289
+ stage: instance.currentStage
8290
+ };
8291
+ if (invariants.isTerminalActivityStatus(status)) return {
8292
+ kind: "activity-not-active",
8293
+ status: status
8294
+ };
8295
+ }
8296
+
8297
+ function disabled(args) {
8298
+ return {
8299
+ ...actionEvaluationIdentity(args.action),
8300
+ allowed: !1,
8301
+ disabledReason: args.reason,
8302
+ ...args.insight !== void 0 ? {
8303
+ insight: args.insight
8061
8304
  } : {},
8062
- vars: {
8063
- params: opts?.callerParams ?? {}
8064
- }
8065
- });
8066
- for (const effect of effects) {
8067
- const resolved = await resolveBindings({
8068
- bindings: effect.bindings,
8069
- staticInput: effect.input,
8070
- snapshot: ctx.snapshot,
8071
- params: params
8072
- }), {pending: pending, history: history} = buildQueuedEffect({
8073
- effect: effect,
8074
- origin: origin,
8075
- params: resolved,
8076
- actor: actor,
8077
- now: now,
8078
- stageEntryKey: stageEntryKey
8079
- });
8080
- mutation.pendingEffects.push(pending), mutation.history.push(history);
8081
- }
8305
+ ...args.whenInsight !== void 0 ? {
8306
+ whenInsight: args.whenInsight
8307
+ } : {}
8308
+ };
8082
8309
  }
8083
8310
 
8084
8311
  function inFlightFilter() {
@@ -8106,25 +8333,81 @@ function idsArm(filter, params) {
8106
8333
  }
8107
8334
  }
8108
8335
 
8109
- function instancesQuery(args) {
8110
- const {tag: tag, filter: filter = {}} = args;
8111
- if (invariants.validateTag(tag), filter.limit !== void 0 && (!Number.isInteger(filter.limit) || filter.limit <= 0)) throw new invariants.ContractViolationError(`instancesQuery: limit must be a positive integer; got ${JSON.stringify(filter.limit)}`);
8112
- const conditions = [ `_type == "${invariants.WORKFLOW_INSTANCE_TYPE}"`, invariants.tagScopeFilter() ], params = {
8113
- tag: tag
8114
- };
8115
- filter.includeCompleted !== !0 && conditions.push(inFlightFilter()), filter.definition !== void 0 && (conditions.push("definition == $definition"),
8116
- params.definition = filter.definition), filter.stage !== void 0 && (conditions.push("currentStage == $stage"),
8117
- params.stage = filter.stage);
8118
- const arms = [ documentArm(filter, params), idsArm(filter, params) ].filter(arm => arm !== void 0);
8119
- arms.length > 0 && conditions.push(`(${arms.join(" || ")})`);
8120
- const body = `*[${conditions.join(" && ")}]`;
8121
- return filter.limit !== void 0 ? {
8122
- query: `${body} | order(startedAt desc) [0...${filter.limit}]`,
8123
- params: params
8124
- } : {
8125
- query: `${body} | order(startedAt asc)`,
8126
- params: params
8127
- };
8336
+ function instancesQuery(args) {
8337
+ const {tag: tag, filter: filter = {}} = args;
8338
+ if (invariants.validateTag(tag), filter.limit !== void 0 && (!Number.isInteger(filter.limit) || filter.limit <= 0)) throw new invariants.ContractViolationError(`instancesQuery: limit must be a positive integer; got ${JSON.stringify(filter.limit)}`);
8339
+ const conditions = [ `_type == "${invariants.WORKFLOW_INSTANCE_TYPE}"`, invariants.tagScopeFilter() ], params = {
8340
+ tag: tag
8341
+ };
8342
+ filter.includeCompleted !== !0 && conditions.push(inFlightFilter()), filter.definition !== void 0 && (conditions.push("definition == $definition"),
8343
+ params.definition = filter.definition), filter.stage !== void 0 && (conditions.push("currentStage == $stage"),
8344
+ params.stage = filter.stage);
8345
+ const arms = [ documentArm(filter, params), idsArm(filter, params) ].filter(arm => arm !== void 0);
8346
+ arms.length > 0 && conditions.push(`(${arms.join(" || ")})`);
8347
+ const body = `*[${conditions.join(" && ")}]`;
8348
+ return filter.limit !== void 0 ? {
8349
+ query: `${body} | order(startedAt desc) [0...${filter.limit}]`,
8350
+ params: params
8351
+ } : {
8352
+ query: `${body} | order(startedAt asc)`,
8353
+ params: params
8354
+ };
8355
+ }
8356
+
8357
+ const DEFAULT_EFFECT_LEASE_MS = 300 * 1e3;
8358
+
8359
+ function claimReleasedEntry(args) {
8360
+ return {
8361
+ _key: randomKey(),
8362
+ _type: "effectClaimReleased",
8363
+ at: args.at,
8364
+ effectKey: args.pending._key,
8365
+ effect: args.pending.name,
8366
+ claim: args.claim,
8367
+ via: args.via,
8368
+ actor: args.actor
8369
+ };
8370
+ }
8371
+
8372
+ const SWEEP_MAX_ATTEMPTS = 5;
8373
+
8374
+ async function sweepStaleClaims(args) {
8375
+ const {tag: tag, instanceId: instanceId} = args, client = pinApiVersion(args.client);
8376
+ invariants.validateTag(tag);
8377
+ const clock = args.clock ?? wallClock, {actor: actor} = await resolveAccess(client), stamp = resolveExecutionContext(args.executionContext);
8378
+ for (let attempt = 1; attempt <= SWEEP_MAX_ATTEMPTS; attempt++) {
8379
+ const now = clock(), instance = await reload({
8380
+ client: client,
8381
+ instanceId: instanceId,
8382
+ tag: tag
8383
+ }), expired = instance.pendingEffects.filter(e => e.claim !== void 0 && isClaimExpired(e.claim, now));
8384
+ if (expired.length === 0) return {
8385
+ released: []
8386
+ };
8387
+ const expiredKeys = new Set(expired.map(e => e._key)), releasedQueue = instance.pendingEffects.map(e => expiredKeys.has(e._key) ? stripClaim(e) : e), auditRows = stampHistoryEntries(expired.map(pending => claimReleasedEntry({
8388
+ pending: pending,
8389
+ claim: pending.claim,
8390
+ at: now,
8391
+ via: "sweep",
8392
+ actor: actor
8393
+ })), stamp);
8394
+ try {
8395
+ return await client.patch(instance._id).ifRevisionId(instance._rev).set({
8396
+ pendingEffects: releasedQueue,
8397
+ history: [ ...instance.history, ...auditRows ]
8398
+ }).commit(SYNC_COMMIT), {
8399
+ released: expired
8400
+ };
8401
+ } catch (error) {
8402
+ if (!isRevisionConflict(error)) throw error;
8403
+ }
8404
+ }
8405
+ throw new Error(`sweepStaleClaims lost the optimistic-locking race ${SWEEP_MAX_ATTEMPTS} attempts running on ${instanceId} — a concurrent writer kept committing first. Retry later, or investigate a write storm on this instance.`);
8406
+ }
8407
+
8408
+ function stripClaim(entry) {
8409
+ const {claim: _claim, ...rest} = entry;
8410
+ return rest;
8128
8411
  }
8129
8412
 
8130
8413
  async function sortByDependencies({client: client, definitions: definitions, tag: tag}) {
@@ -9418,6 +9701,66 @@ const workflow = {
9418
9701
  }
9419
9702
  });
9420
9703
  },
9704
+ commitEffectOps: async rawArgs => {
9705
+ const args = taggedScope(rawArgs, REQUEST_TAG.commitEffectOps), {client: client, tag: tag, instanceId: instanceId, effectKey: effectKey, claimToken: claimToken, ops: ops, executionContext: executionContext} = args, {actor: actor, clientForGdr: clientForGdr, refSurface: refSurface} = await resolveOperationContext(args), clock = args.clock ?? wallClock, record = requestRecordFor({
9706
+ idempotencyKey: args.idempotencyKey,
9707
+ op: "commitEffectOps",
9708
+ idempotencyTtlMs: args.idempotencyTtlMs
9709
+ });
9710
+ if (record === void 0) throw new invariants.ContractViolationError("commitEffectOps: idempotencyKey is required — the engine cannot assume a supplied field op is idempotent, so every mid-dispatch report must be retry-safe");
9711
+ return runDeduped({
9712
+ client: client,
9713
+ tag: tag,
9714
+ instanceId: instanceId,
9715
+ record: record,
9716
+ now: clock(),
9717
+ actor: actor,
9718
+ clientForGdr: clientForGdr,
9719
+ refSurface: refSurface,
9720
+ clock: clock,
9721
+ executionContext: executionContext,
9722
+ telemetry: args.telemetry,
9723
+ run: async () => {
9724
+ const report = await commitEffectOps({
9725
+ client: client,
9726
+ instanceId: instanceId,
9727
+ effectKey: effectKey,
9728
+ claimToken: claimToken,
9729
+ ops: ops,
9730
+ requestRecord: record,
9731
+ leaseMs: args.leaseMs ?? DEFAULT_EFFECT_LEASE_MS,
9732
+ options: engineOptionsForActor({
9733
+ actor: actor,
9734
+ clock: clock,
9735
+ clientForGdr: clientForGdr,
9736
+ refSurface: refSurface,
9737
+ executionContext: executionContext,
9738
+ telemetry: args.telemetry
9739
+ })
9740
+ }), {cascaded: cascaded, instance: instance} = await cascadeAndReload({
9741
+ client: client,
9742
+ tag: tag,
9743
+ instanceId: instanceId,
9744
+ actor: actor,
9745
+ clientForGdr: clientForGdr,
9746
+ refSurface: refSurface,
9747
+ clock: clock,
9748
+ executionContext: executionContext,
9749
+ telemetry: args.telemetry
9750
+ });
9751
+ return resolveTelemetry(args.telemetry).log(WorkflowEffectStateReported, {
9752
+ ...definitionHashFragment(instance.pinnedContentHash),
9753
+ instanceId: instanceId,
9754
+ effect: report.effect,
9755
+ cascaded: cascaded
9756
+ }), {
9757
+ instance: instance,
9758
+ cascaded: cascaded,
9759
+ changed: !0
9760
+ };
9761
+ }
9762
+ });
9763
+ },
9421
9764
  tick: async rawArgs => {
9422
9765
  const args = taggedScope(rawArgs, REQUEST_TAG.tick), {client: client, tag: tag, instanceId: instanceId, executionContext: executionContext} = args, {actor: actor, clientForGdr: clientForGdr, refSurface: refSurface} = await resolveOperationContext(args), clock = args.clock ?? wallClock, current = await reload({
9423
9766
  client: client,
@@ -10019,64 +10362,100 @@ async function resolveActor(directory, actor) {
10019
10362
  };
10020
10363
  }
10021
10364
 
10022
- const DEFAULT_EFFECT_LEASE_MS = 300 * 1e3;
10365
+ const EFFECT_COMMIT_QUEUE_DEPTH = 32, EFFECT_COMMIT_DISPATCH_CAP = 200;
10023
10366
 
10024
- function isClaimExpired(claim, now) {
10025
- return claim.leaseExpiresAt === void 0 || hasPassed(claim.leaseExpiresAt, now);
10367
+ class EffectCommitQueueOverflowError extends invariants.WorkflowError {
10368
+ effectKey;
10369
+ bound;
10370
+ limit;
10371
+ constructor(args) {
10372
+ super("effect-commit-queue-overflow", args.bound === "queue-depth" ? `Effect dispatch "${args.effectKey}" has ${args.limit} mid-dispatch commits queued — await your ctx.commitOps/ctx.setProgress calls (or report less often); the queue serializes real engine commits and cannot grow without bound` : `Effect dispatch "${args.effectKey}" already made ${args.limit} mid-dispatch commits — the per-dispatch cap bounds instance history growth; report more coarsely`),
10373
+ this.name = "EffectCommitQueueOverflowError", this.effectKey = args.effectKey, this.bound = args.bound,
10374
+ this.limit = args.limit;
10375
+ }
10026
10376
  }
10027
10377
 
10028
- function claimReleasedEntry(args) {
10029
- return {
10030
- _key: randomKey(),
10031
- _type: "effectClaimReleased",
10032
- at: args.at,
10033
- effectKey: args.pending._key,
10034
- effect: args.pending.name,
10035
- claim: args.claim,
10036
- via: args.via,
10037
- actor: args.actor
10038
- };
10378
+ function isClaimLoss(err) {
10379
+ return err instanceof StaleEffectClaimError || err instanceof invariants.EffectNotFoundError;
10039
10380
  }
10040
10381
 
10041
- const SWEEP_MAX_ATTEMPTS = 5;
10382
+ function pendingEntry(args) {
10383
+ let resolve, reject;
10384
+ const promise = new Promise((res, rej) => {
10385
+ resolve = res, reject = rej;
10386
+ });
10387
+ return promise.catch(() => {}), {
10388
+ ...args,
10389
+ resolve: resolve,
10390
+ reject: reject,
10391
+ promise: promise
10392
+ };
10393
+ }
10042
10394
 
10043
- async function sweepStaleClaims(args) {
10044
- const {tag: tag, instanceId: instanceId} = args, client = pinApiVersion(args.client);
10045
- invariants.validateTag(tag);
10046
- const clock = args.clock ?? wallClock, {actor: actor} = await resolveAccess(client), stamp = resolveExecutionContext(args.executionContext);
10047
- for (let attempt = 1; attempt <= SWEEP_MAX_ATTEMPTS; attempt++) {
10048
- const now = clock(), instance = await reload({
10049
- client: client,
10050
- instanceId: instanceId,
10051
- tag: tag
10052
- }), expired = instance.pendingEffects.filter(e => e.claim !== void 0 && isClaimExpired(e.claim, now));
10053
- if (expired.length === 0) return {
10054
- released: []
10055
- };
10056
- const expiredKeys = new Set(expired.map(e => e._key)), releasedQueue = instance.pendingEffects.map(e => expiredKeys.has(e._key) ? stripClaim(e) : e), auditRows = stampHistoryEntries(expired.map(pending => claimReleasedEntry({
10057
- pending: pending,
10058
- claim: pending.claim,
10059
- at: now,
10060
- via: "sweep",
10061
- actor: actor
10062
- })), stamp);
10063
- try {
10064
- return await client.patch(instance._id).ifRevisionId(instance._rev).set({
10065
- pendingEffects: releasedQueue,
10066
- history: [ ...instance.history, ...auditRows ]
10067
- }).commit(SYNC_COMMIT), {
10068
- released: expired
10069
- };
10070
- } catch (error) {
10071
- if (!isRevisionConflict(error)) throw error;
10395
+ function createEffectCommitQueue(args) {
10396
+ const {effectKey: effectKey, claimToken: claimToken, commit: commit} = args, pending = [];
10397
+ let admitted = 0, dead, closed = !1, tail = Promise.resolve();
10398
+ function assertOpen() {
10399
+ if (dead !== void 0) throw dead;
10400
+ if (closed) throw new Error(`Effect dispatch "${effectKey}" already settled — a mid-dispatch report after the handler returned cannot commit; await your reports inside the handler`);
10401
+ }
10402
+ function admit(entry) {
10403
+ if (assertOpen(), pending.length >= EFFECT_COMMIT_QUEUE_DEPTH) throw new EffectCommitQueueOverflowError({
10404
+ effectKey: effectKey,
10405
+ bound: "queue-depth",
10406
+ limit: EFFECT_COMMIT_QUEUE_DEPTH
10407
+ });
10408
+ if (admitted >= EFFECT_COMMIT_DISPATCH_CAP) throw new EffectCommitQueueOverflowError({
10409
+ effectKey: effectKey,
10410
+ bound: "dispatch-cap",
10411
+ limit: EFFECT_COMMIT_DISPATCH_CAP
10412
+ });
10413
+ return admitted += 1, pending.push(entry), tail = tail.then(runNext), entry.promise;
10414
+ }
10415
+ function die(err) {
10416
+ dead = err;
10417
+ for (const entry of pending.splice(0)) entry.reject(err);
10418
+ }
10419
+ async function runNext() {
10420
+ const entry = pending.shift();
10421
+ if (entry !== void 0) try {
10422
+ await commit({
10423
+ ops: entry.buildOps(),
10424
+ idempotencyKey: entry.idempotencyKey
10425
+ }), entry.resolve();
10426
+ } catch (err) {
10427
+ entry.reject(err), die(err);
10072
10428
  }
10073
10429
  }
10074
- throw new Error(`sweepStaleClaims lost the optimistic-locking race ${SWEEP_MAX_ATTEMPTS} attempts running on ${instanceId} — a concurrent writer kept committing first. Retry later, or investigate a write storm on this instance.`);
10075
- }
10076
-
10077
- function stripClaim(entry) {
10078
- const {claim: _claim, ...rest} = entry;
10079
- return rest;
10430
+ return {
10431
+ commitOps: req => admit(pendingEntry({
10432
+ buildOps: () => req.ops,
10433
+ idempotencyKey: req.idempotencyKey
10434
+ })),
10435
+ setProgress: (target, value) => {
10436
+ assertOpen();
10437
+ const ref = typeof target == "string" ? {
10438
+ scope: "workflow",
10439
+ field: target
10440
+ } : target, coalesceKey = `${ref.scope}:${ref.field}`, queued = pending.find(entry2 => entry2.coalesceKey === coalesceKey);
10441
+ if (queued !== void 0) return queued.latestValue = value, queued.promise;
10442
+ const entry = pendingEntry({
10443
+ buildOps: () => [ {
10444
+ type: "field.set",
10445
+ target: ref,
10446
+ value: {
10447
+ type: "literal",
10448
+ value: entry.latestValue
10449
+ }
10450
+ } ],
10451
+ idempotencyKey: `effect:${effectKey}:${claimToken}:progress:${coalesceKey}:${admitted}`,
10452
+ coalesceKey: coalesceKey,
10453
+ latestValue: value
10454
+ });
10455
+ return admit(entry);
10456
+ },
10457
+ settle: async () => (closed = !0, await tail, dead !== void 0 && !isClaimLoss(dead) ? dead : void 0)
10458
+ };
10080
10459
  }
10081
10460
 
10082
10461
  class MissingHandlerError extends invariants.WorkflowError {
@@ -10172,74 +10551,126 @@ async function drainEffectsInternal(args) {
10172
10551
  failed: [],
10173
10552
  skipped: [],
10174
10553
  lost: []
10175
- }, skippedKeys = /* @__PURE__ */ new Set;
10176
- for (;;) {
10177
- const now = clock(), before = await reload({
10178
- client: client,
10179
- instanceId: instanceId,
10180
- tag: tag
10181
- });
10182
- noteDrainedInstanceHash(buckets, before);
10183
- const candidate = findClaimableCandidate({
10184
- instance: before,
10185
- now: now,
10186
- skippedKeys: skippedKeys
10187
- });
10188
- if (candidate === void 0) break;
10189
- const handler = effectHandlers[candidate.name];
10190
- if (handler === void 0) {
10191
- await assertSkippableOrThrow({
10192
- missingHandler: missingHandler,
10193
- candidate: candidate,
10194
- instanceId: instanceId,
10195
- log: log
10196
- }), buckets.skipped.push(candidate), skippedKeys.add(candidate._key);
10197
- continue;
10198
- }
10199
- if (!await claimPendingEffect({
10200
- client: client,
10201
- instance: before,
10202
- candidate: candidate,
10203
- drainerActor: drainerActor,
10204
- now: now,
10205
- leaseMs: leaseMs,
10206
- ...executionContext !== void 0 ? {
10207
- executionContext: executionContext
10208
- } : {}
10209
- })) continue;
10210
- const outcome = await dispatchAndReport({
10211
- handler: handler,
10212
- candidate: candidate,
10213
- client: client,
10214
- handlerClient: handlerClient,
10215
- tag: tag,
10216
- workflowResource: workflowResource,
10217
- ...resourceClients !== void 0 ? {
10218
- resourceClients: resourceClients
10219
- } : {},
10220
- instanceId: instanceId,
10221
- clientFor: clientFor,
10222
- logger: logger,
10223
- log: log,
10224
- ...executionContext !== void 0 ? {
10225
- executionContext: executionContext
10226
- } : {},
10227
- telemetry: cascadeTelemetry,
10228
- clock: clock
10229
- });
10230
- buckets[outcome].push(candidate);
10231
- }
10554
+ }, pass = {
10555
+ client: client,
10556
+ handlerClient: handlerClient,
10557
+ tag: tag,
10558
+ workflowResource: workflowResource,
10559
+ ...resourceClients !== void 0 ? {
10560
+ resourceClients: resourceClients
10561
+ } : {},
10562
+ instanceId: instanceId,
10563
+ effectHandlers: effectHandlers,
10564
+ missingHandler: missingHandler,
10565
+ logger: logger,
10566
+ log: log,
10567
+ leaseMs: leaseMs,
10568
+ clock: clock,
10569
+ ...executionContext !== void 0 ? {
10570
+ executionContext: executionContext
10571
+ } : {},
10572
+ telemetry: cascadeTelemetry,
10573
+ drainerActor: drainerActor,
10574
+ clientFor: clientFor,
10575
+ buckets: buckets,
10576
+ skippedKeys: /* @__PURE__ */ new Set
10577
+ };
10578
+ for (;await drainOneCandidate(pass); ) ;
10232
10579
  return buckets;
10233
10580
  }
10234
10581
 
10582
+ async function drainOneCandidate(pass) {
10583
+ const {client: client, tag: tag, instanceId: instanceId, buckets: buckets, skippedKeys: skippedKeys} = pass, now = pass.clock(), before = await reload({
10584
+ client: client,
10585
+ instanceId: instanceId,
10586
+ tag: tag
10587
+ });
10588
+ noteDrainedInstanceHash(buckets, before);
10589
+ const candidate = findClaimableCandidate({
10590
+ instance: before,
10591
+ now: now,
10592
+ skippedKeys: skippedKeys
10593
+ });
10594
+ if (candidate === void 0) return !1;
10595
+ const handler = pass.effectHandlers[candidate.name];
10596
+ if (handler === void 0) return await assertSkippableOrThrow({
10597
+ missingHandler: pass.missingHandler,
10598
+ candidate: candidate,
10599
+ instanceId: instanceId,
10600
+ log: pass.log
10601
+ }), buckets.skipped.push(candidate), skippedKeys.add(candidate._key), !0;
10602
+ const claimed = await claimPendingEffect({
10603
+ client: client,
10604
+ instance: before,
10605
+ candidate: candidate,
10606
+ drainerActor: pass.drainerActor,
10607
+ now: now,
10608
+ leaseMs: pass.leaseMs,
10609
+ ...pass.executionContext !== void 0 ? {
10610
+ executionContext: pass.executionContext
10611
+ } : {}
10612
+ });
10613
+ if (claimed === void 0) return !0;
10614
+ const outcome = await dispatchAndReport({
10615
+ handler: handler,
10616
+ candidate: candidate,
10617
+ claimToken: claimed.claimToken,
10618
+ leaseMs: pass.leaseMs,
10619
+ client: client,
10620
+ handlerClient: pass.handlerClient,
10621
+ tag: tag,
10622
+ workflowResource: pass.workflowResource,
10623
+ ...pass.resourceClients !== void 0 ? {
10624
+ resourceClients: pass.resourceClients
10625
+ } : {},
10626
+ instanceId: instanceId,
10627
+ clientFor: pass.clientFor,
10628
+ logger: pass.logger,
10629
+ log: pass.log,
10630
+ ...pass.executionContext !== void 0 ? {
10631
+ executionContext: pass.executionContext
10632
+ } : {},
10633
+ telemetry: pass.telemetry,
10634
+ clock: pass.clock
10635
+ });
10636
+ return buckets[outcome].push(candidate), !0;
10637
+ }
10638
+
10235
10639
  function findClaimableCandidate({instance: instance, now: now, skippedKeys: skippedKeys}) {
10236
10640
  return instance.pendingEffects.find(e => (e.claim === void 0 || isClaimExpired(e.claim, now)) && !skippedKeys.has(e._key));
10237
10641
  }
10238
10642
 
10239
10643
  async function dispatchAndReport(args) {
10240
- const {handler: handler, candidate: candidate, client: client, handlerClient: handlerClient, tag: tag, workflowResource: workflowResource, resourceClients: resourceClients, instanceId: instanceId, clientFor: clientFor, logger: logger, log: log, executionContext: executionContext, telemetry: telemetry, clock: clock} = args, {outputs: outputs, ops: ops, dispatchError: dispatchError} = await dispatchEffect({
10644
+ const {handler: handler, candidate: candidate, claimToken: claimToken, leaseMs: leaseMs, client: client, handlerClient: handlerClient, tag: tag, workflowResource: workflowResource, resourceClients: resourceClients, instanceId: instanceId, clientFor: clientFor, logger: logger, log: log, executionContext: executionContext, telemetry: telemetry, clock: clock} = args, commitQueue = createEffectCommitQueue({
10645
+ effectKey: candidate._key,
10646
+ claimToken: claimToken,
10647
+ commit: async req => {
10648
+ await workflow.commitEffectOps({
10649
+ client: client,
10650
+ tag: tag,
10651
+ workflowResource: workflowResource,
10652
+ ...resourceClients !== void 0 ? {
10653
+ resourceClients: resourceClients
10654
+ } : {},
10655
+ ...executionContext !== void 0 ? {
10656
+ executionContext: executionContext
10657
+ } : {},
10658
+ ...telemetry !== void 0 ? {
10659
+ telemetry: telemetry
10660
+ } : {},
10661
+ instanceId: instanceId,
10662
+ effectKey: candidate._key,
10663
+ claimToken: claimToken,
10664
+ ops: req.ops,
10665
+ idempotencyKey: req.idempotencyKey,
10666
+ leaseMs: leaseMs,
10667
+ clock: clock
10668
+ });
10669
+ }
10670
+ }), {outputs: outputs, ops: ops, dispatchError: dispatchError} = await dispatchEffect({
10241
10671
  handler: handler,
10242
10672
  candidate: candidate,
10673
+ queue: commitQueue,
10243
10674
  ctx: {
10244
10675
  client: handlerClient,
10245
10676
  clientFor: clientFor,
@@ -10319,11 +10750,12 @@ async function assertSkippableOrThrow({missingHandler: missingHandler, candidate
10319
10750
  }
10320
10751
 
10321
10752
  async function claimPendingEffect({client: client, instance: instance, candidate: candidate, drainerActor: drainerActor, now: now, leaseMs: leaseMs, executionContext: executionContext}) {
10322
- const priorClaim = candidate.claim, claim = {
10753
+ const priorClaim = candidate.claim, claimToken = randomKey(), claim = {
10323
10754
  _type: "pendingEffect.claim",
10324
10755
  claimedAt: now,
10325
10756
  claimedBy: drainerActor,
10326
- leaseExpiresAt: addMs(now, leaseMs)
10757
+ leaseExpiresAt: addMs(now, leaseMs),
10758
+ claimToken: claimToken
10327
10759
  }, takeoverAudit = priorClaim !== void 0 ? stampHistoryEntries([ claimReleasedEntry({
10328
10760
  pending: candidate,
10329
10761
  claim: priorClaim,
@@ -10337,43 +10769,64 @@ async function claimPendingEffect({client: client, instance: instance, candidate
10337
10769
  ...takeoverAudit !== void 0 ? {
10338
10770
  history: [ ...instance.history, takeoverAudit ]
10339
10771
  } : {}
10340
- }).commit(SYNC_COMMIT), !0;
10772
+ }).commit(SYNC_COMMIT), {
10773
+ claimToken: claimToken
10774
+ };
10341
10775
  } catch (error) {
10342
10776
  if (!isRevisionConflict(error)) throw error;
10343
- return !1;
10777
+ return;
10344
10778
  }
10345
10779
  }
10346
10780
 
10347
- async function dispatchEffect({handler: handler, candidate: candidate, ctx: ctx}) {
10781
+ async function invokeHandler({handler: handler, candidate: candidate, queue: queue, ctx: ctx}) {
10348
10782
  try {
10349
10783
  const result = await handler(candidate.params, {
10350
10784
  client: ctx.client,
10351
10785
  clientFor: ctx.clientFor,
10352
10786
  instanceId: ctx.instanceId,
10353
10787
  effectKey: candidate._key,
10354
- log: (message, extra) => ctx.logger(`effect.${candidate.name}`).info(message, extra)
10788
+ log: (message, extra) => ctx.logger(`effect.${candidate.name}`).info(message, extra),
10789
+ commitOps: queue.commitOps,
10790
+ setProgress: queue.setProgress
10355
10791
  });
10356
- return {
10357
- ...result?.outputs !== void 0 ? {
10358
- outputs: result.outputs
10359
- } : {},
10360
- ...result?.ops !== void 0 ? {
10361
- ops: result.ops
10362
- } : {}
10792
+ return result === void 0 ? {} : {
10793
+ result: result
10363
10794
  };
10364
10795
  } catch (err) {
10365
- const message = invariants.errorMessage(err), stack = err instanceof Error && err.stack !== void 0 ? err.stack : void 0;
10366
10796
  return {
10367
- dispatchError: stack !== void 0 ? {
10368
- message: message,
10369
- stack: stack
10370
- } : {
10371
- message: message
10797
+ thrown: {
10798
+ err: err
10372
10799
  }
10373
10800
  };
10374
10801
  }
10375
10802
  }
10376
10803
 
10804
+ function shapeDispatchError(err) {
10805
+ const message = invariants.errorMessage(err), stack = err instanceof Error && err.stack !== void 0 ? err.stack : void 0;
10806
+ return stack !== void 0 ? {
10807
+ message: message,
10808
+ stack: stack
10809
+ } : {
10810
+ message: message
10811
+ };
10812
+ }
10813
+
10814
+ async function dispatchEffect(args) {
10815
+ const {result: result, thrown: thrown} = await invokeHandler(args), settleFailure = await args.queue.settle(), failed = thrown ?? (settleFailure !== void 0 ? {
10816
+ err: settleFailure
10817
+ } : void 0);
10818
+ return failed !== void 0 ? {
10819
+ dispatchError: shapeDispatchError(failed.err)
10820
+ } : {
10821
+ ...result?.outputs !== void 0 ? {
10822
+ outputs: result.outputs
10823
+ } : {},
10824
+ ...result?.ops !== void 0 ? {
10825
+ ops: result.ops
10826
+ } : {}
10827
+ };
10828
+ }
10829
+
10377
10830
  async function applyMissingHandler({policy: policy, info: info, log: log}) {
10378
10831
  if (policy === "fail") return "fail";
10379
10832
  if (policy === "skip") return log.warn(`Missing effect handler "${info.name}" — skipping`, {
@@ -10850,6 +11303,7 @@ function createEngine(args) {
10850
11303
  fireAction: rest => workflow.fireAction(withScope(rest)),
10851
11304
  editField: rest => workflow.editField(withScope(rest)),
10852
11305
  completeEffect: rest => workflow.completeEffect(withScope(rest)),
11306
+ commitEffectOps: rest => workflow.commitEffectOps(withScope(rest)),
10853
11307
  tick: rest => workflow.tick(withScope(rest)),
10854
11308
  evaluate: rest => workflow.evaluate(withScope(rest)),
10855
11309
  diagnose: rest => workflow.diagnose(withScope(rest)),
@@ -11256,6 +11710,10 @@ const HISTORY_DISPLAY = {
11256
11710
  title: "Number value",
11257
11711
  description: "Numeric entry."
11258
11712
  },
11713
+ progress: {
11714
+ title: "Progress",
11715
+ description: "Application-defined completion, 0–100 inclusive (fractions allowed)."
11716
+ },
11259
11717
  boolean: {
11260
11718
  title: "Boolean value",
11261
11719
  description: "True/false entry."
@@ -11686,6 +12144,8 @@ exports.CONTEXT_ENTRY_DISPLAY = CONTEXT_ENTRY_DISPLAY;
11686
12144
 
11687
12145
  exports.CascadeLimitError = CascadeLimitError;
11688
12146
 
12147
+ exports.ConcurrentCommitEffectOpsError = ConcurrentCommitEffectOpsError;
12148
+
11689
12149
  exports.ConcurrentCompleteEffectError = ConcurrentCompleteEffectError;
11690
12150
 
11691
12151
  exports.ConcurrentEditFieldError = ConcurrentEditFieldError;
@@ -11702,6 +12162,10 @@ exports.DISPLAY = DISPLAY;
11702
12162
 
11703
12163
  exports.DRIVER_KIND_DISPLAY = DRIVER_KIND_DISPLAY;
11704
12164
 
12165
+ exports.EFFECT_COMMIT_DISPATCH_CAP = EFFECT_COMMIT_DISPATCH_CAP;
12166
+
12167
+ exports.EFFECT_COMMIT_QUEUE_DEPTH = EFFECT_COMMIT_QUEUE_DEPTH;
12168
+
11705
12169
  exports.ENGINE_API_VERSION = ENGINE_API_VERSION;
11706
12170
 
11707
12171
  exports.EXECUTION_KINDS = EXECUTION_KINDS;
@@ -11710,6 +12174,8 @@ exports.EXECUTOR_CLASSIFICATION_DISPLAY = EXECUTOR_CLASSIFICATION_DISPLAY;
11710
12174
 
11711
12175
  exports.EditFieldDeniedError = EditFieldDeniedError;
11712
12176
 
12177
+ exports.EffectCommitQueueOverflowError = EffectCommitQueueOverflowError;
12178
+
11713
12179
  exports.EffectOpsInvalidError = EffectOpsInvalidError;
11714
12180
 
11715
12181
  exports.EffectOutputsInvalidError = EffectOutputsInvalidError;
@@ -11740,6 +12206,8 @@ exports.RefResourceUndeclaredError = RefResourceUndeclaredError;
11740
12206
 
11741
12207
  exports.RequiredFieldNotProvidedError = RequiredFieldNotProvidedError;
11742
12208
 
12209
+ exports.StaleEffectClaimError = StaleEffectClaimError;
12210
+
11743
12211
  exports.StartNotAllowedError = StartNotAllowedError;
11744
12212
 
11745
12213
  exports.StartNotPrimedError = StartNotPrimedError;
@@ -11754,6 +12222,8 @@ exports.WorkflowDefinitionDeployed = WorkflowDefinitionDeployed;
11754
12222
 
11755
12223
  exports.WorkflowEffectCompleted = WorkflowEffectCompleted;
11756
12224
 
12225
+ exports.WorkflowEffectStateReported = WorkflowEffectStateReported;
12226
+
11757
12227
  exports.WorkflowEffectsDrained = WorkflowEffectsDrained;
11758
12228
 
11759
12229
  exports.WorkflowFieldEdited = WorkflowFieldEdited;