@sanity/workflow-engine 0.10.0 → 0.12.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/define.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as v from "valibot";
2
- import { andConditions, expandRequiredRoles, GROQ_IDENTIFIER, formatValidationError, issuesFromValibot, AuthoringWorkflowSchema, AuthoringStageSchema, AuthoringTaskSchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GuardSchema, EffectSchema } from "./_chunks-es/schema.js";
2
+ import { andConditions, expandRequiredRoles, normalizeRoleAliases, GROQ_IDENTIFIER, formatValidationError, issuesFromValibot, AuthoringWorkflowSchema, AuthoringStageSchema, AuthoringActivitySchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GuardSchema, EffectSchema } from "./_chunks-es/schema.js";
3
3
  function groq(strings, ...values) {
4
4
  return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
5
5
  }
@@ -12,24 +12,26 @@ function serializeGroqValue(value) {
12
12
  return serialized;
13
13
  }
14
14
  function desugarWorkflow(authoring) {
15
- const ctx = {
16
- issues: [],
15
+ const issues = [];
16
+ checkReservedRoleAliasKeys(authoring.roleAliases, issues);
17
+ const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
18
+ issues,
17
19
  claimFields: /* @__PURE__ */ new Map(),
18
20
  claimedFields: /* @__PURE__ */ new Set(),
19
- roleAliases: authoring.roleAliases
21
+ roleAliases
20
22
  }, workflowFields = desugarFieldEntries(authoring.fields, ["fields"], ctx), workflowLayer = layerOf(workflowFields), stages = authoring.stages.map((stage, i) => {
21
23
  const path = ["stages", i], stageFields = desugarFieldEntries(stage.fields, [...path, "fields"], ctx), stageEnv = {
22
24
  layers: [
23
25
  { scope: "stage", entries: layerOf(stageFields) },
24
26
  { scope: "workflow", entries: workflowLayer }
25
27
  ]
26
- }, tasks = (stage.tasks ?? []).map(
27
- (task, j) => desugarTask(task, [...path, "tasks", j], stageEnv, ctx)
28
+ }, activities = (stage.activities ?? []).map(
29
+ (activity, j) => desugarActivity(activity, [...path, "activities", j], stageEnv, ctx)
28
30
  ), transitions = (stage.transitions ?? []).map(
29
31
  (transition, k) => desugarTransition(transition, [...path, "transitions", k], stageEnv, ctx)
30
32
  ), editable = desugarStageEditable(
31
33
  stage.editable,
32
- editableSlotNames(workflowFields, stageFields, tasks),
34
+ editableSlotNames(workflowFields, stageFields, activities),
33
35
  [...path, "editable"],
34
36
  ctx
35
37
  );
@@ -41,7 +43,7 @@ function desugarWorkflow(authoring) {
41
43
  guards: stage.guards
42
44
  }),
43
45
  ...stageFields ? { fields: stageFields } : {},
44
- ...tasks.length > 0 ? { tasks } : {},
46
+ ...activities.length > 0 ? { activities } : {},
45
47
  ...transitions.length > 0 ? { transitions } : {},
46
48
  ...editable ? { editable } : {}
47
49
  };
@@ -49,41 +51,76 @@ function desugarWorkflow(authoring) {
49
51
  return checkUnclaimedClaimFields(ctx), { definition: {
50
52
  ...stripUndefined({
51
53
  name: authoring.name,
52
- version: authoring.version,
53
54
  title: authoring.title,
54
55
  description: authoring.description,
55
56
  role: authoring.role,
56
57
  initialStage: authoring.initialStage,
57
58
  predicates: authoring.predicates,
58
- roleAliases: authoring.roleAliases
59
+ roleAliases
59
60
  }),
60
61
  ...workflowFields ? { fields: workflowFields } : {},
61
62
  stages
62
63
  }, issues: ctx.issues };
63
64
  }
65
+ function checkReservedRoleAliasKeys(aliases, issues) {
66
+ for (const key of Object.keys(aliases ?? {}))
67
+ key.startsWith("$") && issues.push({
68
+ path: ["roleAliases", key],
69
+ message: `role alias key "${key}" uses the reserved "$" prefix \u2014 that namespace is the engine's stored spelling for the universal fulfiller. Use "*" to mean "fulfills any gate", or rename the role.`
70
+ });
71
+ }
72
+ const TODOLIST_OF = [
73
+ { type: "string", name: "label", title: "Label" },
74
+ { type: "string", name: "status", title: "Status" },
75
+ { type: "assignee", name: "assignee", title: "Assignee" },
76
+ { type: "date", name: "dueDate", title: "Due date" }
77
+ ], NOTES_OF = [
78
+ { type: "text", name: "body", title: "Body" },
79
+ { type: "actor", name: "actor", title: "Actor" },
80
+ { type: "datetime", name: "at", title: "At" }
81
+ ];
64
82
  function desugarFieldEntries(entries, path, ctx) {
65
- return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => {
66
- if (entry.type === "claim") {
67
- const desugared = {
68
- ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
69
- type: "value.actor",
70
- source: { type: "write" }
71
- };
72
- return ctx.claimFields.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
73
- }
74
- const editable = normalizeEditable(entry.editable, [...path, i, "editable"], ctx);
75
- return {
76
- ...stripUndefined({
77
- type: entry.type,
78
- name: entry.name,
79
- title: entry.title,
80
- description: entry.description,
81
- required: entry.required,
82
- editable
83
- }),
84
- source: entry.source
85
- };
86
- });
83
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry(entry, [...path, i], ctx));
84
+ }
85
+ function desugarFieldEntry(entry, path, ctx) {
86
+ if (entry.type === "claim") return desugarClaimField(entry, path, ctx);
87
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField(entry, path, ctx);
88
+ const editable = normalizeEditable(entry.editable, [...path, "editable"], ctx);
89
+ return {
90
+ ...stripUndefined({
91
+ type: entry.type,
92
+ name: entry.name,
93
+ title: entry.title,
94
+ description: entry.description,
95
+ required: entry.required,
96
+ initialValue: entry.initialValue,
97
+ editable,
98
+ fields: entry.fields,
99
+ of: entry.of
100
+ })
101
+ };
102
+ }
103
+ function desugarClaimField(entry, path, ctx) {
104
+ const desugared = {
105
+ ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
106
+ type: "actor"
107
+ };
108
+ return ctx.claimFields.set(desugared, { name: entry.name, path }), desugared;
109
+ }
110
+ function desugarListField(entry, path, ctx) {
111
+ const editable = normalizeEditable(entry.editable, [...path, "editable"], ctx);
112
+ return {
113
+ ...stripUndefined({
114
+ name: entry.name,
115
+ title: entry.title,
116
+ description: entry.description,
117
+ required: entry.required,
118
+ initialValue: entry.initialValue,
119
+ editable
120
+ }),
121
+ type: "array",
122
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
123
+ };
87
124
  }
88
125
  function normalizeEditable(editable, path, ctx) {
89
126
  if (editable === void 0 || editable === !0) return editable;
@@ -100,12 +137,12 @@ function normalizeEditable(editable, path, ctx) {
100
137
  }
101
138
  return editable;
102
139
  }
103
- function editableSlotNames(workflowFields, stageFields, tasks) {
140
+ function editableSlotNames(workflowFields, stageFields, activities) {
104
141
  const names = /* @__PURE__ */ new Set(), collect = (entries) => {
105
142
  for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
106
143
  };
107
144
  collect(workflowFields), collect(stageFields);
108
- for (const task of tasks) collect(task.fields);
145
+ for (const activity of activities) collect(activity.fields);
109
146
  return names;
110
147
  }
111
148
  function desugarStageEditable(overrides, inScope, path, ctx) {
@@ -115,7 +152,7 @@ function desugarStageEditable(overrides, inScope, path, ctx) {
115
152
  if (!inScope.has(name)) {
116
153
  ctx.issues.push({
117
154
  path: [...path, name],
118
- message: `stage editable override "${name}" does not narrow an editable slot in scope \u2014 name a workflow/stage/task slot of this stage that declares \`editable\``
155
+ message: `stage editable override "${name}" does not narrow an editable slot in scope \u2014 name a workflow/stage/activity slot of this stage that declares \`editable\``
119
156
  });
120
157
  continue;
121
158
  }
@@ -127,35 +164,46 @@ function desugarStageEditable(overrides, inScope, path, ctx) {
127
164
  function layerOf(entries) {
128
165
  return new Map((entries ?? []).map((entry) => [entry.name, entry]));
129
166
  }
130
- function desugarTask(task, path, stageEnv, ctx) {
131
- const taskFields = desugarFieldEntries(task.fields, [...path, "fields"], ctx), env = {
132
- layers: [{ scope: "task", entries: layerOf(taskFields) }, ...stageEnv.layers]
133
- }, ops = desugarOps(task.ops, [...path, "ops"], env, task.name, ctx), actions = (task.actions ?? []).map(
134
- (action, a) => desugarAction(action, [...path, "actions", a], env, task.name, ctx)
135
- );
167
+ function desugarActivity(activity, path, stageEnv, ctx) {
168
+ const activityFields = desugarFieldEntries(activity.fields, [...path, "fields"], ctx), env = {
169
+ layers: [{ scope: "activity", entries: layerOf(activityFields) }, ...stageEnv.layers]
170
+ }, ops = desugarOps(activity.ops, [...path, "ops"], env, activity.name, ctx), actions = (activity.actions ?? []).map(
171
+ (action, a) => desugarAction(action, [...path, "actions", a], env, activity.name, ctx)
172
+ ), target = desugarTarget(activity.target, env, [...path, "target"], ctx);
136
173
  return {
137
174
  ...stripUndefined({
138
- name: task.name,
139
- title: task.title,
140
- description: task.description,
141
- filter: task.filter,
142
- requirements: task.requirements,
143
- completeWhen: task.completeWhen,
144
- failWhen: task.failWhen,
145
- effects: task.effects,
146
- subworkflows: task.subworkflows
175
+ name: activity.name,
176
+ title: activity.title,
177
+ description: activity.description,
178
+ kind: activity.kind,
179
+ filter: activity.filter,
180
+ requirements: activity.requirements,
181
+ completeWhen: activity.completeWhen,
182
+ failWhen: activity.failWhen,
183
+ effects: activity.effects,
184
+ subworkflows: activity.subworkflows
147
185
  }),
148
- activation: task.activation ?? "manual",
149
- ...taskFields ? { fields: taskFields } : {},
186
+ activation: activity.activation ?? "manual",
187
+ ...target ? { target } : {},
188
+ ...activityFields ? { fields: activityFields } : {},
150
189
  ...ops ? { ops } : {},
151
190
  ...actions.length > 0 ? { actions } : {}
152
191
  };
153
192
  }
154
- function desugarAction(action, path, env, taskName, ctx) {
193
+ const TARGET_DOC_KINDS = ["doc.ref", "doc.refs", "release.ref"];
194
+ function desugarTarget(target, env, path, ctx) {
195
+ if (target === void 0 || target.type === "url") return target;
196
+ const ref = typeof target.field == "string" ? { field: target.field } : target.field, field = resolveRef(ref, env, [...path, "field"], ctx) ?? fallbackRef(ref), entry = entryAt(env, field);
197
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
198
+ path: [...path, "field"],
199
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" \u2014 a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
200
+ }), { type: "field", field };
201
+ }
202
+ function desugarAction(action, path, env, activityName, ctx) {
155
203
  if ("type" in action)
156
204
  return desugarClaimAction(action, path, env, ctx);
157
- const filter = andConditions([rolesCondition(action.roles, ctx.roleAliases), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
158
- return action.status !== void 0 && ops.push({ type: "status.set", task: taskName, status: action.status }), {
205
+ const filter = andConditions([rolesCondition(action.roles, ctx.roleAliases), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, activityName, ctx) ?? [];
206
+ return action.status !== void 0 && ops.push({ type: "status.set", activity: activityName, status: action.status }), {
159
207
  ...stripUndefined({
160
208
  name: action.name,
161
209
  title: action.title,
@@ -171,9 +219,9 @@ function desugarClaimAction(action, path, env, ctx) {
171
219
  const ref = typeof action.field == "string" ? { field: action.field } : action.field, resolved = resolveRef(ref, env, [...path, "field"], ctx);
172
220
  if (resolved) {
173
221
  const entry = entryAt(env, resolved);
174
- entry && entry.type !== "value.actor" && ctx.issues.push({
222
+ entry && entry.type !== "actor" && ctx.issues.push({
175
223
  path: [...path, "field"],
176
- message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" \u2014 a claim pair needs an actor-valued entry (a "claim" or "value.actor" field declaration)`
224
+ message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" \u2014 a claim pair needs an actor-valued entry (a "claim" or "actor" field declaration)`
177
225
  }), checkShadowedClaimTarget(action.name, ref.field, resolved, env, [...path, "field"], ctx), entry && ctx.claimedFields.add(entry);
178
226
  }
179
227
  const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([
@@ -204,7 +252,7 @@ function checkUnclaimedClaimFields(ctx) {
204
252
  for (const [entry, { name, path }] of ctx.claimFields)
205
253
  ctx.claimedFields.has(entry) || ctx.issues.push({
206
254
  path,
207
- message: `claim field "${name}" is never referenced by a claim action \u2014 you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw value.actor entry`
255
+ message: `claim field "${name}" is never referenced by a claim action \u2014 you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw actor entry`
208
256
  });
209
257
  }
210
258
  function desugarTransition(transition, path, stageEnv, ctx) {
@@ -217,21 +265,21 @@ function desugarTransition(transition, path, stageEnv, ctx) {
217
265
  to: transition.to,
218
266
  effects: transition.effects
219
267
  }),
220
- filter: transition.filter ?? "$allTasksDone",
268
+ filter: transition.filter ?? "$allActivitiesDone",
221
269
  ...ops ? { ops } : {}
222
270
  };
223
271
  }
224
- function desugarOps(ops, path, env, firingTask, ctx) {
272
+ function desugarOps(ops, path, env, firingActivity, ctx) {
225
273
  if (ops)
226
- return ops.map((op, i) => desugarOp(op, [...path, i], env, firingTask, ctx));
274
+ return ops.map((op, i) => desugarOp(op, [...path, i], env, firingActivity, ctx));
227
275
  }
228
- function desugarOp(op, path, env, firingTask, ctx) {
276
+ function desugarOp(op, path, env, firingActivity, ctx) {
229
277
  if (op.type === "status.set") {
230
- const task = op.task ?? firingTask;
231
- return task === void 0 && ctx.issues.push({
232
- path: [...path, "task"],
233
- message: "status.set outside an action must name its target task"
234
- }), { type: "status.set", task: task ?? "", status: op.status };
278
+ const activity = op.activity ?? firingActivity;
279
+ return activity === void 0 && ctx.issues.push({
280
+ path: [...path, "activity"],
281
+ message: "status.set outside an action must name its target activity"
282
+ }), { type: "status.set", activity: activity ?? "", status: op.status };
235
283
  }
236
284
  if (op.type === "audit") return desugarAuditOp(op, path, env, ctx);
237
285
  const target = resolveRef(op.target, env, [...path, "target"], ctx) ?? fallbackRef(op.target);
@@ -295,14 +343,14 @@ const RESERVED_CONDITION_VARS = [
295
343
  "row",
296
344
  "now",
297
345
  "effects",
298
- "tasks",
346
+ "activities",
299
347
  "subworkflows",
300
348
  "self",
301
349
  "stage",
302
350
  "parent",
303
351
  "ancestors",
304
- "allTasksDone",
305
- "anyTaskFailed"
352
+ "allActivitiesDone",
353
+ "anyActivityFailed"
306
354
  ], PREDICATE_CALLER_VARS = ["actor", "assigned", "can"];
307
355
  function knownList(ids) {
308
356
  return [...ids].map((s) => `"${s}"`).join(", ");
@@ -320,12 +368,12 @@ function checkStages(def, issues) {
320
368
  issues
321
369
  );
322
370
  for (const [i, stage] of def.stages.entries()) {
323
- const taskNames = checkDuplicates(
324
- (stage.tasks ?? []).map((task, j) => ({
325
- name: task.name,
326
- path: ["stages", i, "tasks", j, "name"]
371
+ const activityNames = checkDuplicates(
372
+ (stage.activities ?? []).map((activity, j) => ({
373
+ name: activity.name,
374
+ path: ["stages", i, "activities", j, "name"]
327
375
  })),
328
- `task name in stage "${stage.name}"`,
376
+ `activity name in stage "${stage.name}"`,
329
377
  issues
330
378
  );
331
379
  checkDuplicates(
@@ -335,34 +383,34 @@ function checkStages(def, issues) {
335
383
  })),
336
384
  `transition name in stage "${stage.name}"`,
337
385
  issues
338
- ), checkTasks(def, i, taskNames, issues);
386
+ ), checkActivities(def, i, activityNames, issues);
339
387
  }
340
388
  return stageNames;
341
389
  }
342
- function checkTasks(def, i, taskNames, issues) {
390
+ function checkActivities(def, i, activityNames, issues) {
343
391
  const stage = def.stages[i];
344
- for (const [j, task] of (stage.tasks ?? []).entries()) {
345
- const path = ["stages", i, "tasks", j];
392
+ for (const [j, activity] of (stage.activities ?? []).entries()) {
393
+ const path = ["stages", i, "activities", j];
346
394
  checkDuplicates(
347
- (task.actions ?? []).map((action, a) => ({
395
+ (activity.actions ?? []).map((action, a) => ({
348
396
  name: action.name,
349
397
  path: [...path, "actions", a, "name"]
350
398
  })),
351
- `action name in task "${task.name}"`,
399
+ `action name in activity "${activity.name}"`,
352
400
  issues
353
- ), checkStatusSetTargets(task, taskNames, path, issues);
401
+ ), checkStatusSetTargets(activity, activityNames, path, issues);
354
402
  }
355
403
  }
356
- function checkStatusSetTargets(task, taskNames, path, issues) {
357
- checkStatusSetOps(task.ops, taskNames, [...path, "ops"], issues);
358
- for (const [a, action] of (task.actions ?? []).entries())
359
- checkStatusSetOps(action.ops, taskNames, [...path, "actions", a, "ops"], issues);
404
+ function checkStatusSetTargets(activity, activityNames, path, issues) {
405
+ checkStatusSetOps(activity.ops, activityNames, [...path, "ops"], issues);
406
+ for (const [a, action] of (activity.actions ?? []).entries())
407
+ checkStatusSetOps(action.ops, activityNames, [...path, "actions", a, "ops"], issues);
360
408
  }
361
- function checkStatusSetOps(ops, taskNames, path, issues) {
409
+ function checkStatusSetOps(ops, activityNames, path, issues) {
362
410
  for (const [o, op] of (ops ?? []).entries())
363
- op.type !== "status.set" || taskNames.has(op.task) || issues.push({
364
- path: [...path, o, "task"],
365
- message: `status.set targets task "${op.task}" which is not declared in this stage. Known tasks: ${knownList(taskNames)}`
411
+ op.type !== "status.set" || activityNames.has(op.activity) || issues.push({
412
+ path: [...path, o, "activity"],
413
+ message: `status.set targets activity "${op.activity}" which is not declared in this stage. Known activities: ${knownList(activityNames)}`
366
414
  });
367
415
  }
368
416
  function checkInitialStage(def, stageNames, issues) {
@@ -382,10 +430,14 @@ function checkTransitionTargets(def, stageNames, issues) {
382
430
  function checkEffectNames(def, issues) {
383
431
  const sites = [];
384
432
  for (const [i, stage] of def.stages.entries()) {
385
- for (const [j, task] of (stage.tasks ?? []).entries()) {
386
- collectEffects(task.effects, ["stages", i, "tasks", j, "effects"], sites);
387
- for (const [a, action] of (task.actions ?? []).entries())
388
- collectEffects(action.effects, ["stages", i, "tasks", j, "actions", a, "effects"], sites);
433
+ for (const [j, activity] of (stage.activities ?? []).entries()) {
434
+ collectEffects(activity.effects, ["stages", i, "activities", j, "effects"], sites);
435
+ for (const [a, action] of (activity.actions ?? []).entries())
436
+ collectEffects(
437
+ action.effects,
438
+ ["stages", i, "activities", j, "actions", a, "effects"],
439
+ sites
440
+ );
389
441
  }
390
442
  for (const [k, t] of (stage.transitions ?? []).entries())
391
443
  collectEffects(t.effects, ["stages", i, "transitions", k, "effects"], sites);
@@ -420,12 +472,12 @@ function fieldScopes(def) {
420
472
  path: ["stages", i, "fields"],
421
473
  label: `stage "${stage.name}" fields`
422
474
  });
423
- for (const [j, task] of (stage.tasks ?? []).entries())
475
+ for (const [j, activity] of (stage.activities ?? []).entries())
424
476
  scopes.push({
425
- entries: task.fields,
426
- scope: "task",
427
- path: ["stages", i, "tasks", j, "fields"],
428
- label: `task "${task.name}" fields`
477
+ entries: activity.fields,
478
+ scope: "activity",
479
+ path: ["stages", i, "activities", j, "fields"],
480
+ label: `activity "${activity.name}" fields`
429
481
  });
430
482
  }
431
483
  return scopes;
@@ -433,13 +485,20 @@ function fieldScopes(def) {
433
485
  function checkRequiredField(def, issues) {
434
486
  for (const { entries, scope, path, label } of fieldScopes(def))
435
487
  for (const [n, entry] of (entries ?? []).entries())
436
- entry.required === !0 && (scope !== "workflow" ? issues.push({
437
- path: [...path, n, "required"],
438
- message: `${label} entry "${entry.name}" is \`required\`, but \`required\` applies only to workflow-scope \`init\` entries \u2014 only those are caller-supplied at start/spawn`
439
- }) : entry.source.type !== "init" && issues.push({
440
- path: [...path, n, "required"],
441
- message: `field entry "${entry.name}" is \`required\` but its source is "${entry.source.type}" \u2014 \`required\` applies only to \`init\`-sourced entries (the caller fills them at start/spawn)`
442
- }));
488
+ if (entry.required === !0) {
489
+ if (scope !== "workflow")
490
+ issues.push({
491
+ path: [...path, n, "required"],
492
+ message: `${label} entry "${entry.name}" is \`required\`, but \`required\` applies only to workflow-scope \`input\` entries \u2014 only those are caller-supplied at start/spawn`
493
+ });
494
+ else if (entry.initialValue?.type !== "input") {
495
+ const seed = entry.initialValue?.type ?? "working memory (no initialValue)";
496
+ issues.push({
497
+ path: [...path, n, "required"],
498
+ message: `field entry "${entry.name}" has initialValue "${seed}" \u2014 \`required\` applies only to \`input\`-sourced entries (the caller fills them at start/spawn)`
499
+ });
500
+ }
501
+ }
443
502
  }
444
503
  function checkFieldEntryNames(def, issues) {
445
504
  for (const { entries, path, label } of fieldScopes(def))
@@ -495,34 +554,34 @@ function nonActionFilterConditionSites(def) {
495
554
  `transition "${t.name}"`,
496
555
  sites
497
556
  );
498
- for (const [j, task] of (stage.tasks ?? []).entries())
499
- collectTaskConditionSites(task, ["stages", i, "tasks", j], sites);
557
+ for (const [j, activity] of (stage.activities ?? []).entries())
558
+ collectActivityConditionSites(activity, ["stages", i, "activities", j], sites);
500
559
  }
501
560
  return sites;
502
561
  }
503
- function collectTaskConditionSites(task, path, sites) {
562
+ function collectActivityConditionSites(activity, path, sites) {
504
563
  for (const field of ["filter", "completeWhen", "failWhen"]) {
505
- const groq2 = task[field];
506
- groq2 !== void 0 && sites.push({ groq: groq2, path: [...path, field], label: `task "${task.name}".${field}` });
564
+ const groq2 = activity[field];
565
+ groq2 !== void 0 && sites.push({ groq: groq2, path: [...path, field], label: `activity "${activity.name}".${field}` });
507
566
  }
508
- collectBindingSites(task.effects, [...path, "effects"], `task "${task.name}"`, sites);
509
- for (const [a, action] of (task.actions ?? []).entries())
567
+ collectBindingSites(activity.effects, [...path, "effects"], `activity "${activity.name}"`, sites);
568
+ for (const [a, action] of (activity.actions ?? []).entries())
510
569
  collectBindingSites(
511
570
  action.effects,
512
571
  [...path, "actions", a, "effects"],
513
572
  `action "${action.name}"`,
514
573
  sites
515
574
  );
516
- collectSubworkflowSites(task, path, sites);
575
+ collectSubworkflowSites(activity, path, sites);
517
576
  }
518
- function collectSubworkflowSites(task, path, sites) {
519
- const sub = task.subworkflows;
577
+ function collectSubworkflowSites(activity, path, sites) {
578
+ const sub = activity.subworkflows;
520
579
  if (sub === void 0) return;
521
580
  const subPath = [...path, "subworkflows"];
522
581
  sites.push({
523
582
  groq: sub.forEach,
524
583
  path: [...subPath, "forEach"],
525
- label: `task "${task.name}".subworkflows.forEach`
584
+ label: `activity "${activity.name}".subworkflows.forEach`
526
585
  });
527
586
  for (const [group, record] of [
528
587
  ["with", sub.with],
@@ -532,7 +591,7 @@ function collectSubworkflowSites(task, path, sites) {
532
591
  sites.push({
533
592
  groq: groq2,
534
593
  path: [...subPath, group, key],
535
- label: `task "${task.name}".subworkflows.${group} "${key}"`
594
+ label: `activity "${activity.name}".subworkflows.${group} "${key}"`
536
595
  });
537
596
  }
538
597
  function collectBindingSites(effects, path, label, sites) {
@@ -551,9 +610,49 @@ function checkAssigneesEntries(def, issues) {
551
610
  message: "at most one assignees-kind field entry per scope \u2014 the inbox reads it by kind"
552
611
  });
553
612
  }
613
+ function activityShape(activity) {
614
+ return {
615
+ hasActions: (activity.actions ?? []).length > 0,
616
+ hasEffects: (activity.effects ?? []).length > 0,
617
+ hasCompleteWhen: activity.completeWhen !== void 0,
618
+ hasSubworkflows: activity.subworkflows !== void 0
619
+ };
620
+ }
621
+ const KIND_SHAPE_RULES = {
622
+ user: (s) => s.hasActions ? [] : ["needs at least one action for a person to fire"],
623
+ service: (s) => s.hasEffects ? [] : ["needs at least one effect \u2014 the automated work it performs"],
624
+ manual: (s) => [
625
+ s.hasActions || s.hasCompleteWhen ? void 0 : "needs a way to complete \u2014 an action to acknowledge it, or a `completeWhen` predicate to verify it (otherwise it auto-resolves on activation)",
626
+ s.hasSubworkflows ? "is off-system work, not a fan-out \u2014 drop `subworkflows` or move it to its own activity" : void 0
627
+ ].filter((m) => m !== void 0),
628
+ receive: (s) => [
629
+ s.hasCompleteWhen || s.hasSubworkflows ? void 0 : "needs a `completeWhen` (or `subworkflows`) condition to wait on",
630
+ s.hasActions ? "has no actor, so it cannot carry actions" : void 0
631
+ ].filter((m) => m !== void 0),
632
+ script: (s) => [
633
+ s.hasActions ? "runs inline with no actor, so it cannot carry actions" : void 0,
634
+ s.hasCompleteWhen ? "resolves on activation, so it cannot carry a `completeWhen` (that is a `receive`/`service` wait)" : void 0,
635
+ s.hasSubworkflows ? "resolves on activation, so it cannot fan out with `subworkflows`" : void 0
636
+ ].filter((m) => m !== void 0)
637
+ };
638
+ function checkActivityKinds(def, issues) {
639
+ for (const [i, stage] of def.stages.entries())
640
+ for (const [j, activity] of (stage.activities ?? []).entries()) {
641
+ const path = ["stages", i, "activities", j];
642
+ if (activity.target !== void 0 && activity.kind !== "manual" && issues.push({
643
+ path: [...path, "target"],
644
+ message: `activity "${activity.name}" has a \`target\` but is not \`kind: "manual"\` \u2014 a target is the deep-link for off-system manual work`
645
+ }), activity.kind !== void 0)
646
+ for (const fault of KIND_SHAPE_RULES[activity.kind](activityShape(activity)))
647
+ issues.push({
648
+ path: [...path, "kind"],
649
+ message: `activity "${activity.name}" declares kind "${activity.kind}" but ${fault}`
650
+ });
651
+ }
652
+ }
554
653
  function checkWorkflowInvariants(def) {
555
654
  const issues = [], stageNames = checkStages(def, issues);
556
- return checkInitialStage(def, stageNames, issues), checkTransitionTargets(def, stageNames, issues), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues), checkRequiredField(def, issues), checkPredicates(def, issues), checkCanOutsideActionFilters(def, issues), checkAssigneesEntries(def, issues), issues;
655
+ return checkInitialStage(def, stageNames, issues), checkTransitionTargets(def, stageNames, issues), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues), checkRequiredField(def, issues), checkPredicates(def, issues), checkCanOutsideActionFilters(def, issues), checkAssigneesEntries(def, issues), checkActivityKinds(def, issues), issues;
557
656
  }
558
657
  function defineWorkflow(definition) {
559
658
  const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow(AuthoringWorkflowSchema, definition, label), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
@@ -563,8 +662,8 @@ function defineWorkflow(definition) {
563
662
  function defineStage(stage) {
564
663
  return parseOrThrow(AuthoringStageSchema, stage, labelFor("defineStage", stage));
565
664
  }
566
- function defineTask(task) {
567
- return parseOrThrow(AuthoringTaskSchema, task, labelFor("defineTask", task));
665
+ function defineActivity(activity) {
666
+ return parseOrThrow(AuthoringActivitySchema, activity, labelFor("defineActivity", activity));
568
667
  }
569
668
  function defineAction(action) {
570
669
  return parseOrThrow(AuthoringActionSchema, action, labelFor("defineAction", action));
@@ -607,13 +706,13 @@ function labelFor(fn, value) {
607
706
  }
608
707
  export {
609
708
  defineAction,
709
+ defineActivity,
610
710
  defineEffect,
611
711
  defineEffectDescriptor,
612
712
  defineField,
613
713
  defineGuard,
614
714
  defineOp,
615
715
  defineStage,
616
- defineTask,
617
716
  defineTransition,
618
717
  defineWorkflow,
619
718
  groq