@sanity/workflow-engine 0.13.0 → 0.15.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,923 +1,710 @@
1
- import * as v from "valibot";
2
- import { andConditions, expandRequiredRoles, normalizeRoleAliases, GROQ_IDENTIFIER, formatValidationError, issuesFromValibot, AuthoringWorkflowSchema, AuthoringStageSchema, AuthoringActivitySchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GuardSchema, EffectSchema } from "./_chunks-es/schema.js";
1
+ import { printGuardRead, andConditions, expandRequiredRoles, normalizeRoleAliases, parseOrThrow, checkWorkflowInvariants, formatValidationError, AuthoringWorkflowSchema, labelFor, WorkflowConfigSchema, AuthoringStageSchema, AuthoringActivitySchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringFieldEntrySchema, AuthoringOpSchema, GroupSchema, AuthoringGuardSchema, EffectSchema } from "./_chunks-es/invariants.js";
2
+
3
+ import { CONDITION_VARS, FILTER_SCOPE_VARS, GUARD_PREDICATE_VARS, RESERVED_CONDITION_VARS } from "./_chunks-es/invariants.js";
4
+
3
5
  function groq(strings, ...values) {
4
- return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
6
+ return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
5
7
  }
8
+
6
9
  function serializeGroqValue(value) {
7
- const serialized = JSON.stringify(value);
8
- if (serialized === void 0)
9
- throw new Error(
10
- `groq tag cannot serialize ${typeof value} \u2014 interpolate JSON-representable values only`
11
- );
12
- return serialized;
10
+ const serialized = JSON.stringify(value);
11
+ if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
12
+ return serialized;
13
13
  }
14
+
14
15
  function desugarWorkflow(authoring) {
15
- const issues = [];
16
- checkReservedRoleAliasKeys(authoring.roleAliases, issues);
17
- const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
18
- issues,
19
- claimFields: /* @__PURE__ */ new Map(),
20
- claimedFields: /* @__PURE__ */ new Set(),
21
- roleAliases
22
- }, workflowFields = desugarFieldEntries({ entries: authoring.fields, path: ["fields"], ctx }), workflowLayer = layerOf(workflowFields), stages = authoring.stages.map((stage, i) => {
23
- const path = ["stages", i], stageFields = desugarFieldEntries({ entries: stage.fields, path: [...path, "fields"], ctx }), stageEnv = {
24
- layers: [
25
- { scope: "stage", entries: layerOf(stageFields) },
26
- { scope: "workflow", entries: workflowLayer }
27
- ]
28
- }, activities = (stage.activities ?? []).map(
29
- (activity, j) => desugarActivity({ activity, path: [...path, "activities", j], stageEnv, ctx })
30
- ), transitions = (stage.transitions ?? []).map(
31
- (transition, k) => desugarTransition({ transition, path: [...path, "transitions", k], stageEnv, ctx })
32
- ), editable = desugarStageEditable({
33
- overrides: stage.editable,
34
- inScope: editableSlotNames({ workflowFields, stageFields, activities }),
35
- path: [...path, "editable"],
36
- ctx
16
+ const issues = [];
17
+ checkReservedRoleAliasKeys(authoring.roleAliases, issues);
18
+ const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
19
+ issues: issues,
20
+ claimFields: /* @__PURE__ */ new Map,
21
+ claimedFields: /* @__PURE__ */ new Set,
22
+ roleAliases: roleAliases
23
+ }, workflowFields = desugarFieldEntries({
24
+ entries: authoring.fields,
25
+ path: [ "fields" ],
26
+ ctx: ctx
27
+ }), workflowLayer = layerOf(workflowFields), stages = authoring.stages.map((stage, i) => {
28
+ const path = [ "stages", i ], stageFields = desugarFieldEntries({
29
+ entries: stage.fields,
30
+ path: [ ...path, "fields" ],
31
+ ctx: ctx
32
+ }), stageEnv = {
33
+ layers: [ {
34
+ scope: "stage",
35
+ entries: layerOf(stageFields)
36
+ }, {
37
+ scope: "workflow",
38
+ entries: workflowLayer
39
+ } ]
40
+ }, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
41
+ activity: activity,
42
+ path: [ ...path, "activities", j ],
43
+ stageEnv: stageEnv,
44
+ ctx: ctx
45
+ })), transitions = (stage.transitions ?? []).map((transition, k) => desugarTransition({
46
+ transition: transition,
47
+ path: [ ...path, "transitions", k ],
48
+ stageEnv: stageEnv,
49
+ ctx: ctx
50
+ })), editable = desugarStageEditable({
51
+ overrides: stage.editable,
52
+ inScope: editableFieldNames({
53
+ workflowFields: workflowFields,
54
+ stageFields: stageFields,
55
+ activities: activities
56
+ }),
57
+ path: [ ...path, "editable" ],
58
+ ctx: ctx
59
+ });
60
+ return {
61
+ ...stripUndefined({
62
+ name: stage.name,
63
+ title: stage.title,
64
+ description: stage.description,
65
+ groups: stage.groups,
66
+ guards: stage.guards?.map(desugarGuard)
67
+ }),
68
+ ...stageFields ? {
69
+ fields: stageFields
70
+ } : {},
71
+ ...activities.length > 0 ? {
72
+ activities: activities
73
+ } : {},
74
+ ...transitions.length > 0 ? {
75
+ transitions: transitions
76
+ } : {},
77
+ ...editable ? {
78
+ editable: editable
79
+ } : {}
80
+ };
37
81
  });
38
- return {
39
- ...stripUndefined({
40
- name: stage.name,
41
- title: stage.title,
42
- description: stage.description,
43
- guards: stage.guards
44
- }),
45
- ...stageFields ? { fields: stageFields } : {},
46
- ...activities.length > 0 ? { activities } : {},
47
- ...transitions.length > 0 ? { transitions } : {},
48
- ...editable ? { editable } : {}
82
+ return checkUnclaimedClaimFields(ctx), {
83
+ definition: {
84
+ ...stripUndefined({
85
+ name: authoring.name,
86
+ title: authoring.title,
87
+ description: authoring.description,
88
+ groups: authoring.groups,
89
+ lifecycle: authoring.lifecycle,
90
+ applicableWhen: authoring.applicableWhen,
91
+ initialStage: authoring.initialStage,
92
+ predicates: authoring.predicates,
93
+ roleAliases: roleAliases
94
+ }),
95
+ ...workflowFields ? {
96
+ fields: workflowFields
97
+ } : {},
98
+ stages: stages
99
+ },
100
+ issues: ctx.issues
49
101
  };
50
- });
51
- return checkUnclaimedClaimFields(ctx), { definition: {
52
- ...stripUndefined({
53
- name: authoring.name,
54
- title: authoring.title,
55
- description: authoring.description,
56
- role: authoring.role,
57
- initialStage: authoring.initialStage,
58
- predicates: authoring.predicates,
59
- roleAliases
60
- }),
61
- ...workflowFields ? { fields: workflowFields } : {},
62
- stages
63
- }, issues: ctx.issues };
64
102
  }
103
+
65
104
  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.`
105
+ for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
106
+ path: [ "roleAliases", key ],
107
+ message: `role alias key "${key}" uses the reserved "$" prefix — that namespace is the engine's stored spelling for the universal fulfiller. Use "*" to mean "fulfills any gate", or rename the role.`
70
108
  });
71
109
  }
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
- ];
82
- function desugarFieldEntries({
83
- entries,
84
- path,
85
- ctx
86
- }) {
87
- return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({ entry, path: [...path, i], ctx }));
88
- }
89
- function desugarFieldEntry({
90
- entry,
91
- path,
92
- ctx
93
- }) {
94
- if (entry.type === "claim") return desugarClaimField({ entry, path, ctx });
95
- if (entry.type === "todoList" || entry.type === "notes")
96
- return desugarListField({ entry, path, ctx });
97
- const editable = normalizeEditable({ editable: entry.editable, path: [...path, "editable"], ctx });
98
- return {
99
- ...stripUndefined({
100
- type: entry.type,
101
- name: entry.name,
102
- title: entry.title,
103
- description: entry.description,
104
- required: entry.required,
105
- initialValue: entry.initialValue,
106
- editable,
107
- fields: entry.fields,
108
- of: entry.of
109
- })
110
- };
111
- }
112
- function desugarClaimField({
113
- entry,
114
- path,
115
- ctx
116
- }) {
117
- const desugared = {
118
- ...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
119
- type: "actor"
120
- };
121
- return ctx.claimFields.set(desugared, { name: entry.name, path }), desugared;
122
- }
123
- function desugarListField({
124
- entry,
125
- path,
126
- ctx
127
- }) {
128
- const editable = normalizeEditable({ editable: entry.editable, path: [...path, "editable"], ctx });
129
- return {
130
- ...stripUndefined({
131
- name: entry.name,
132
- title: entry.title,
133
- description: entry.description,
134
- required: entry.required,
135
- initialValue: entry.initialValue,
136
- editable
137
- }),
138
- type: "array",
139
- of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
140
- };
141
- }
142
- function normalizeEditable({
143
- editable,
144
- path,
145
- ctx
146
- }) {
147
- if (editable === void 0 || editable === !0) return editable;
148
- if (Array.isArray(editable)) {
149
- const condition = rolesCondition(editable, ctx.roleAliases);
150
- if (condition === void 0) {
151
- ctx.issues.push({
152
- path,
153
- message: "editable: [] names no roles \u2014 use `true` to open the slot to anyone in its scope, or list at least one role"
154
- });
155
- return;
156
- }
157
- return condition;
158
- }
159
- return editable;
160
- }
161
- function editableSlotNames({
162
- workflowFields,
163
- stageFields,
164
- activities
165
- }) {
166
- const names = /* @__PURE__ */ new Set(), collect = (entries) => {
167
- for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
168
- };
169
- collect(workflowFields), collect(stageFields);
170
- for (const activity of activities) collect(activity.fields);
171
- return names;
172
- }
173
- function desugarStageEditable({
174
- overrides,
175
- inScope,
176
- path,
177
- ctx
178
- }) {
179
- if (overrides === void 0) return;
180
- const out = {};
181
- for (const [name, value] of Object.entries(overrides)) {
182
- if (!inScope.has(name)) {
183
- ctx.issues.push({
184
- path: [...path, name],
185
- 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\``
186
- });
187
- continue;
188
- }
189
- const normalized = normalizeEditable({ editable: value, path: [...path, name], ctx });
190
- normalized !== void 0 && (out[name] = normalized);
191
- }
192
- return Object.keys(out).length > 0 ? out : void 0;
193
- }
194
- function layerOf(entries) {
195
- return new Map((entries ?? []).map((entry) => [entry.name, entry]));
196
- }
197
- function desugarActivity({
198
- activity,
199
- path,
200
- stageEnv,
201
- ctx
202
- }) {
203
- const activityFields = desugarFieldEntries({
204
- entries: activity.fields,
205
- path: [...path, "fields"],
206
- ctx
207
- }), env = {
208
- layers: [{ scope: "activity", entries: layerOf(activityFields) }, ...stageEnv.layers]
209
- }, ops = desugarOps({
210
- ops: activity.ops,
211
- path: [...path, "ops"],
212
- env,
213
- firingActivity: activity.name,
214
- ctx
215
- }), actions = (activity.actions ?? []).map(
216
- (action, a) => desugarAction({ action, path: [...path, "actions", a], env, activityName: activity.name, ctx })
217
- ), target = desugarTarget({ target: activity.target, env, path: [...path, "target"], ctx });
218
- return {
219
- ...stripUndefined({
220
- name: activity.name,
221
- title: activity.title,
222
- description: activity.description,
223
- kind: activity.kind,
224
- filter: activity.filter,
225
- requirements: activity.requirements,
226
- completeWhen: activity.completeWhen,
227
- failWhen: activity.failWhen,
228
- effects: activity.effects,
229
- subworkflows: activity.subworkflows
230
- }),
231
- activation: activity.activation ?? "manual",
232
- ...target ? { target } : {},
233
- ...activityFields ? { fields: activityFields } : {},
234
- ...ops ? { ops } : {},
235
- ...actions.length > 0 ? { actions } : {}
236
- };
237
- }
238
- const TARGET_DOC_KINDS = ["doc.ref", "doc.refs", "release.ref"];
239
- function desugarTarget({
240
- target,
241
- env,
242
- path,
243
- ctx
244
- }) {
245
- if (target === void 0 || target.type === "url") return target;
246
- const ref = typeof target.field == "string" ? { field: target.field } : target.field, field = resolveRef({ ref, env, path: [...path, "field"], ctx }) ?? fallbackRef(ref), entry = entryAt(env, field);
247
- return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
248
- path: [...path, "field"],
249
- 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(", ")})`
250
- }), { type: "field", field };
251
- }
252
- function desugarAction({
253
- action,
254
- path,
255
- env,
256
- activityName,
257
- ctx
258
- }) {
259
- if ("type" in action)
260
- return desugarClaimAction({ action, path, env, ctx });
261
- const filter = andConditions([rolesCondition(action.roles, ctx.roleAliases), action.filter]), ops = desugarOps({ ops: action.ops, path: [...path, "ops"], env, firingActivity: activityName, ctx }) ?? [];
262
- return action.status !== void 0 && ops.push({ type: "status.set", activity: activityName, status: action.status }), {
263
- ...stripUndefined({
264
- name: action.name,
265
- title: action.title,
266
- description: action.description,
267
- params: action.params,
268
- effects: action.effects
269
- }),
270
- ...filter ? { filter } : {},
271
- ...ops.length > 0 ? { ops } : {}
272
- };
273
- }
274
- function desugarClaimAction({
275
- action,
276
- path,
277
- env,
278
- ctx
279
- }) {
280
- const ref = typeof action.field == "string" ? { field: action.field } : action.field, resolved = resolveRef({ ref, env, path: [...path, "field"], ctx });
281
- if (resolved) {
282
- const entry = entryAt(env, resolved);
283
- entry && entry.type !== "actor" && ctx.issues.push({
284
- path: [...path, "field"],
285
- 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)`
286
- }), checkShadowedClaimTarget({
287
- actionName: action.name,
288
- field: ref.field,
289
- resolved,
290
- env,
291
- path: [...path, "field"],
292
- ctx
293
- }), entry && ctx.claimedFields.add(entry);
294
- }
295
- const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([
296
- noSteal,
297
- rolesCondition(action.roles, ctx.roleAliases),
298
- action.filter
299
- ]), ops = resolved ? [{ type: "field.set", target: resolved, value: { type: "actor" } }] : [];
300
- return {
301
- ...stripUndefined({
302
- name: action.name,
303
- title: action.title,
304
- description: action.description,
305
- params: action.params,
306
- effects: action.effects
307
- }),
308
- filter,
309
- ...ops.length > 0 ? { ops } : {}
310
- };
311
- }
312
- function checkShadowedClaimTarget({
313
- actionName,
314
- field,
315
- resolved,
316
- env,
317
- path,
318
- ctx
319
- }) {
320
- const nearest = env.layers.find((l) => l.entries.has(field));
321
- nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
322
- path,
323
- message: `claim action "${actionName}" targets "${field}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $fields \u2014 the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
324
- });
110
+
111
+ const TODOLIST_OF = [ {
112
+ type: "string",
113
+ name: "label",
114
+ title: "Label"
115
+ }, {
116
+ type: "string",
117
+ name: "status",
118
+ title: "Status"
119
+ }, {
120
+ type: "assignee",
121
+ name: "assignee",
122
+ title: "Assignee"
123
+ }, {
124
+ type: "date",
125
+ name: "dueDate",
126
+ title: "Due date"
127
+ } ], NOTES_OF = [ {
128
+ type: "text",
129
+ name: "body",
130
+ title: "Body"
131
+ }, {
132
+ type: "actor",
133
+ name: "actor",
134
+ title: "Actor"
135
+ }, {
136
+ type: "datetime",
137
+ name: "at",
138
+ title: "At"
139
+ } ];
140
+
141
+ function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
142
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
143
+ entry: entry,
144
+ path: [ ...path, i ],
145
+ ctx: ctx
146
+ }));
147
+ }
148
+
149
+ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
150
+ if (entry.type === "claim") return desugarClaimField({
151
+ entry: entry,
152
+ path: path,
153
+ ctx: ctx
154
+ });
155
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
156
+ entry: entry,
157
+ path: path,
158
+ ctx: ctx
159
+ });
160
+ const editable = normalizeEditable({
161
+ editable: entry.editable,
162
+ path: [ ...path, "editable" ],
163
+ ctx: ctx
164
+ });
165
+ return {
166
+ ...stripUndefined({
167
+ type: entry.type,
168
+ name: entry.name,
169
+ title: entry.title,
170
+ description: entry.description,
171
+ group: normalizeGroup(entry.group),
172
+ required: entry.required,
173
+ initialValue: entry.initialValue,
174
+ editable: editable,
175
+ types: entry.types,
176
+ fields: entry.fields,
177
+ of: entry.of
178
+ })
179
+ };
325
180
  }
326
- function checkUnclaimedClaimFields(ctx) {
327
- for (const [entry, { name, path }] of ctx.claimFields)
328
- ctx.claimedFields.has(entry) || ctx.issues.push({
329
- path,
330
- 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`
181
+
182
+ function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
183
+ const desugared = {
184
+ ...stripUndefined({
185
+ name: entry.name,
186
+ title: entry.title,
187
+ description: entry.description,
188
+ group: normalizeGroup(entry.group)
189
+ }),
190
+ type: "actor"
191
+ };
192
+ return ctx.claimFields.set(desugared, {
193
+ name: entry.name,
194
+ path: path
195
+ }), desugared;
196
+ }
197
+
198
+ function desugarListField({entry: entry, path: path, ctx: ctx}) {
199
+ const editable = normalizeEditable({
200
+ editable: entry.editable,
201
+ path: [ ...path, "editable" ],
202
+ ctx: ctx
331
203
  });
204
+ return {
205
+ ...stripUndefined({
206
+ name: entry.name,
207
+ title: entry.title,
208
+ description: entry.description,
209
+ group: normalizeGroup(entry.group),
210
+ required: entry.required,
211
+ initialValue: entry.initialValue,
212
+ editable: editable
213
+ }),
214
+ type: "array",
215
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
216
+ };
332
217
  }
333
- function desugarTransition({
334
- transition,
335
- path,
336
- stageEnv,
337
- ctx
338
- }) {
339
- const ops = desugarOps({
340
- ops: transition.ops,
341
- path: [...path, "ops"],
342
- env: stageEnv,
343
- firingActivity: void 0,
344
- ctx
345
- });
346
- return {
347
- ...stripUndefined({
348
- name: transition.name,
349
- title: transition.title,
350
- description: transition.description,
351
- to: transition.to,
352
- effects: transition.effects
353
- }),
354
- filter: transition.filter ?? "$allActivitiesDone",
355
- ...ops ? { ops } : {}
356
- };
357
- }
358
- function desugarOps({
359
- ops,
360
- path,
361
- env,
362
- firingActivity,
363
- ctx
364
- }) {
365
- if (ops)
366
- return ops.map((op, i) => desugarOp({ op, path: [...path, i], env, firingActivity, ctx }));
367
- }
368
- function desugarOp({
369
- op,
370
- path,
371
- env,
372
- firingActivity,
373
- ctx
374
- }) {
375
- if (op.type === "status.set") {
376
- const activity = op.activity ?? firingActivity;
377
- return activity === void 0 && ctx.issues.push({
378
- path: [...path, "activity"],
379
- message: "status.set outside an action must name its target activity"
380
- }), { type: "status.set", activity: activity ?? "", status: op.status };
381
- }
382
- if (op.type === "audit") return desugarAuditOp({ op, path, env, ctx });
383
- const target = resolveRef({ ref: op.target, env, path: [...path, "target"], ctx }) ?? fallbackRef(op.target);
384
- return { ...op, target };
385
- }
386
- const AUDIT_STAMPS = { actor: { type: "actor" }, at: { type: "now" } };
387
- function desugarAuditOp({
388
- op,
389
- path,
390
- env,
391
- ctx
392
- }) {
393
- const target = resolveRef({ ref: op.target, env, path: [...path, "target"], ctx }) ?? fallbackRef(op.target);
394
- if (op.value.type !== "object")
395
- return ctx.issues.push({
396
- path: [...path, "value"],
397
- message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
398
- }), { type: "field.append", target, value: op.value };
399
- const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = { ...op.value.fields };
400
- for (const [stamp, source] of [
401
- [actorField, AUDIT_STAMPS.actor],
402
- [atField, AUDIT_STAMPS.at]
403
- ]) {
404
- if (stamp in fields) {
405
- ctx.issues.push({
406
- path: [...path, "value", "fields", stamp],
407
- message: `audit stamp field "${stamp}" collides with an authored domain field \u2014 rename yours or remap the stamp via stampFields`
408
- });
409
- continue;
218
+
219
+ function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
220
+ if (editable === void 0 || editable === !0) return editable;
221
+ if (Array.isArray(editable)) {
222
+ const condition = rolesCondition(editable, ctx.roleAliases);
223
+ if (condition === void 0) {
224
+ ctx.issues.push({
225
+ path: path,
226
+ message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
227
+ });
228
+ return;
229
+ }
230
+ return condition;
410
231
  }
411
- fields[stamp] = source;
412
- }
413
- return { type: "field.append", target, value: { type: "object", fields } };
414
- }
415
- function resolveRef({
416
- ref,
417
- env,
418
- path,
419
- ctx
420
- }) {
421
- const layers = ref.scope === void 0 ? env.layers : env.layers.filter((l) => l.scope === ref.scope);
422
- for (const layer of layers)
423
- if (layer.entries.has(ref.field)) return { scope: layer.scope, field: ref.field };
424
- const reachable = env.layers.flatMap((l) => [...l.entries.keys()].map((n) => `${l.scope}:${n}`));
425
- ctx.issues.push({
426
- path,
427
- message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
428
- });
232
+ return editable;
429
233
  }
430
- function entryAt(env, ref) {
431
- return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.field);
234
+
235
+ function editableFieldNames({workflowFields: workflowFields, stageFields: stageFields, activities: activities}) {
236
+ const names = /* @__PURE__ */ new Set, collect = entries => {
237
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
238
+ };
239
+ collect(workflowFields), collect(stageFields);
240
+ for (const activity of activities) collect(activity.fields);
241
+ return names;
242
+ }
243
+
244
+ function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
245
+ if (overrides === void 0) return;
246
+ const out = {};
247
+ for (const [name, value] of Object.entries(overrides)) {
248
+ if (!inScope.has(name)) {
249
+ ctx.issues.push({
250
+ path: [ ...path, name ],
251
+ message: `stage editable override "${name}" does not narrow an editable field in scope — name a workflow/stage/activity field of this stage that declares \`editable\``
252
+ });
253
+ continue;
254
+ }
255
+ const normalized = normalizeEditable({
256
+ editable: value,
257
+ path: [ ...path, name ],
258
+ ctx: ctx
259
+ });
260
+ normalized !== void 0 && (out[name] = normalized);
261
+ }
262
+ return Object.keys(out).length > 0 ? out : void 0;
432
263
  }
433
- function fallbackRef(ref) {
434
- return { scope: ref.scope ?? "workflow", field: ref.field };
264
+
265
+ function layerOf(entries) {
266
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
435
267
  }
436
- function rolesCondition(roles, aliases) {
437
- if (!(!roles || roles.length === 0))
438
- return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
268
+
269
+ function normalizeGroup(group) {
270
+ return group === void 0 || Array.isArray(group) ? group : [ group ];
439
271
  }
440
- function stripUndefined(obj) {
441
- const out = {};
442
- for (const [k, value] of Object.entries(obj))
443
- value !== void 0 && (out[k] = value);
444
- return out;
445
- }
446
- const RESERVED_CONDITION_VARS = [
447
- "fields",
448
- "actor",
449
- "assigned",
450
- "can",
451
- "row",
452
- "now",
453
- "effects",
454
- "activities",
455
- "subworkflows",
456
- "self",
457
- "stage",
458
- "parent",
459
- "ancestors",
460
- "allActivitiesDone",
461
- "anyActivityFailed"
462
- ], PREDICATE_CALLER_VARS = ["actor", "assigned", "can"];
463
- function knownList(ids) {
464
- return [...ids].map((s) => `"${s}"`).join(", ");
465
- }
466
- function checkDuplicates({
467
- names,
468
- what,
469
- issues
470
- }) {
471
- const seen = /* @__PURE__ */ new Set();
472
- for (const { name, path } of names)
473
- seen.has(name) && issues.push({ path, message: `duplicate ${what} "${name}" (already declared earlier)` }), seen.add(name);
474
- return seen;
475
- }
476
- function checkStages(def, issues) {
477
- const stageNames = checkDuplicates({
478
- names: def.stages.map((stage, i) => ({ name: stage.name, path: ["stages", i, "name"] })),
479
- what: "stage name",
480
- issues
481
- });
482
- for (const [i, stage] of def.stages.entries()) {
483
- const activityNames = checkDuplicates({
484
- names: (stage.activities ?? []).map((activity, j) => ({
485
- name: activity.name,
486
- path: ["stages", i, "activities", j, "name"]
487
- })),
488
- what: `activity name in stage "${stage.name}"`,
489
- issues
490
- });
491
- checkDuplicates({
492
- names: (stage.transitions ?? []).map((t, k) => ({
493
- name: t.name,
494
- path: ["stages", i, "transitions", k, "name"]
495
- })),
496
- what: `transition name in stage "${stage.name}"`,
497
- issues
498
- }), checkActivities({ def, i, activityNames, issues });
499
- }
500
- return stageNames;
501
- }
502
- function checkActivities({
503
- def,
504
- i,
505
- activityNames,
506
- issues
507
- }) {
508
- const stage = def.stages[i];
509
- for (const [j, activity] of (stage.activities ?? []).entries()) {
510
- const path = ["stages", i, "activities", j];
511
- checkDuplicates({
512
- names: (activity.actions ?? []).map((action, a) => ({
513
- name: action.name,
514
- path: [...path, "actions", a, "name"]
515
- })),
516
- what: `action name in activity "${activity.name}"`,
517
- issues
518
- }), checkStatusSetTargets({ activity, activityNames, path, issues });
519
- }
520
- }
521
- function checkStatusSetTargets({
522
- activity,
523
- activityNames,
524
- path,
525
- issues
526
- }) {
527
- checkStatusSetOps({ ops: activity.ops, activityNames, path: [...path, "ops"], issues });
528
- for (const [a, action] of (activity.actions ?? []).entries())
529
- checkStatusSetOps({
530
- ops: action.ops,
531
- activityNames,
532
- path: [...path, "actions", a, "ops"],
533
- issues
272
+
273
+ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
274
+ const activityFields = desugarFieldEntries({
275
+ entries: activity.fields,
276
+ path: [ ...path, "fields" ],
277
+ ctx: ctx
278
+ }), env = {
279
+ layers: [ {
280
+ scope: "activity",
281
+ entries: layerOf(activityFields)
282
+ }, ...stageEnv.layers ]
283
+ }, ops = desugarOps({
284
+ ops: activity.ops,
285
+ path: [ ...path, "ops" ],
286
+ env: env,
287
+ firingActivity: activity.name,
288
+ ctx: ctx
289
+ }), actions = (activity.actions ?? []).map((action, a) => desugarAction({
290
+ action: action,
291
+ path: [ ...path, "actions", a ],
292
+ env: env,
293
+ activityName: activity.name,
294
+ ctx: ctx
295
+ })), target = desugarTarget({
296
+ target: activity.target,
297
+ env: env,
298
+ path: [ ...path, "target" ],
299
+ ctx: ctx
534
300
  });
301
+ return {
302
+ ...stripUndefined({
303
+ name: activity.name,
304
+ title: activity.title,
305
+ description: activity.description,
306
+ groups: activity.groups,
307
+ group: normalizeGroup(activity.group),
308
+ kind: activity.kind,
309
+ filter: activity.filter,
310
+ requirements: activity.requirements,
311
+ completeWhen: activity.completeWhen,
312
+ failWhen: activity.failWhen,
313
+ effects: activity.effects,
314
+ subworkflows: activity.subworkflows
315
+ }),
316
+ activation: activity.activation ?? "manual",
317
+ ...target ? {
318
+ target: target
319
+ } : {},
320
+ ...activityFields ? {
321
+ fields: activityFields
322
+ } : {},
323
+ ...ops ? {
324
+ ops: ops
325
+ } : {},
326
+ ...actions.length > 0 ? {
327
+ actions: actions
328
+ } : {}
329
+ };
535
330
  }
536
- function checkStatusSetOps({
537
- ops,
538
- activityNames,
539
- path,
540
- issues
541
- }) {
542
- for (const [o, op] of (ops ?? []).entries())
543
- op.type !== "status.set" || activityNames.has(op.activity) || issues.push({
544
- path: [...path, o, "activity"],
545
- message: `status.set targets activity "${op.activity}" which is not declared in this stage. Known activities: ${knownList(activityNames)}`
331
+
332
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
333
+
334
+ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
335
+ if (target === void 0 || target.type === "url") return target;
336
+ const ref = typeof target.field == "string" ? {
337
+ field: target.field
338
+ } : target.field, field = resolveRef({
339
+ ref: ref,
340
+ env: env,
341
+ path: [ ...path, "field" ],
342
+ ctx: ctx
343
+ }) ?? fallbackRef(ref), entry = entryAt(env, field);
344
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
345
+ path: [ ...path, "field" ],
346
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
347
+ }), {
348
+ type: "field",
349
+ field: field
350
+ };
351
+ }
352
+
353
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
354
+ if ("type" in action) return desugarClaimAction({
355
+ action: action,
356
+ path: path,
357
+ env: env,
358
+ ctx: ctx
546
359
  });
360
+ const filter = andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
361
+ ops: action.ops,
362
+ path: [ ...path, "ops" ],
363
+ env: env,
364
+ firingActivity: activityName,
365
+ ctx: ctx
366
+ }) ?? [];
367
+ return action.status !== void 0 && ops.push({
368
+ type: "status.set",
369
+ activity: activityName,
370
+ status: action.status
371
+ }), {
372
+ ...stripUndefined({
373
+ name: action.name,
374
+ title: action.title,
375
+ description: action.description,
376
+ group: normalizeGroup(action.group),
377
+ params: action.params,
378
+ effects: action.effects
379
+ }),
380
+ ...filter ? {
381
+ filter: filter
382
+ } : {},
383
+ ...ops.length > 0 ? {
384
+ ops: ops
385
+ } : {}
386
+ };
547
387
  }
548
- function checkInitialStage({
549
- def,
550
- stageNames,
551
- issues
552
- }) {
553
- stageNames.has(def.initialStage) || issues.push({
554
- path: ["initialStage"],
555
- message: `initialStage "${def.initialStage}" is not a declared stage. Known stages: ${knownList(stageNames)}`
556
- });
557
- }
558
- function checkTransitionTargets({
559
- def,
560
- stageNames,
561
- issues
562
- }) {
563
- for (const [i, stage] of def.stages.entries())
564
- for (const [k, t] of (stage.transitions ?? []).entries())
565
- stageNames.has(t.to) || issues.push({
566
- path: ["stages", i, "transitions", k, "to"],
567
- message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(stageNames)}`
568
- });
569
- }
570
- function checkEffectNames(def, issues) {
571
- const sites = [];
572
- for (const [i, stage] of def.stages.entries()) {
573
- for (const [j, activity] of (stage.activities ?? []).entries()) {
574
- collectEffects({
575
- effects: activity.effects,
576
- path: ["stages", i, "activities", j, "effects"],
577
- sites
578
- });
579
- for (const [a, action] of (activity.actions ?? []).entries())
580
- collectEffects({
581
- effects: action.effects,
582
- path: ["stages", i, "activities", j, "actions", a, "effects"],
583
- sites
584
- });
585
- }
586
- for (const [k, t] of (stage.transitions ?? []).entries())
587
- collectEffects({ effects: t.effects, path: ["stages", i, "transitions", k, "effects"], sites });
588
- }
589
- checkDuplicates({
590
- names: sites,
591
- what: "effect name (registry key \u2014 unique per definition)",
592
- issues
593
- });
594
- }
595
- function collectEffects({
596
- effects,
597
- path,
598
- sites
599
- }) {
600
- for (const [e, effect] of (effects ?? []).entries())
601
- sites.push({ name: effect.name, path: [...path, e, "name"] });
602
- }
603
- function checkGuardNames(def, issues) {
604
- const sites = def.stages.flatMap(
605
- (stage, i) => (stage.guards ?? []).map((guard, g) => ({
606
- name: guard.name,
607
- path: ["stages", i, "guards", g, "name"]
608
- }))
609
- );
610
- checkDuplicates({
611
- names: sites,
612
- what: "guard name (the guard lake _id derives from it \u2014 unique per definition)",
613
- issues
614
- });
615
- }
616
- function fieldScopes(def) {
617
- const scopes = [
618
- { entries: def.fields, scope: "workflow", path: ["fields"], label: "workflow fields" }
619
- ];
620
- for (const [i, stage] of def.stages.entries()) {
621
- scopes.push({
622
- entries: stage.fields,
623
- scope: "stage",
624
- path: ["stages", i, "fields"],
625
- label: `stage "${stage.name}" fields`
388
+
389
+ function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
390
+ const ref = typeof action.field == "string" ? {
391
+ field: action.field
392
+ } : action.field, resolved = resolveRef({
393
+ ref: ref,
394
+ env: env,
395
+ path: [ ...path, "field" ],
396
+ ctx: ctx
626
397
  });
627
- for (const [j, activity] of (stage.activities ?? []).entries())
628
- scopes.push({
629
- entries: activity.fields,
630
- scope: "activity",
631
- path: ["stages", i, "activities", j, "fields"],
632
- label: `activity "${activity.name}" fields`
633
- });
634
- }
635
- return scopes;
636
- }
637
- function checkRequiredField(def, issues) {
638
- for (const { entries, scope, path, label } of fieldScopes(def))
639
- for (const [n, entry] of (entries ?? []).entries())
640
- if (entry.required === !0) {
641
- if (scope !== "workflow")
642
- issues.push({
643
- path: [...path, n, "required"],
644
- 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`
645
- });
646
- else if (entry.initialValue?.type !== "input") {
647
- const seed = entry.initialValue?.type ?? "working memory (no initialValue)";
648
- issues.push({
649
- path: [...path, n, "required"],
650
- message: `field entry "${entry.name}" has initialValue "${seed}" \u2014 \`required\` applies only to \`input\`-sourced entries (the caller fills them at start/spawn)`
651
- });
398
+ if (resolved) {
399
+ const entry = entryAt(env, resolved);
400
+ entry && entry.type !== "actor" && ctx.issues.push({
401
+ path: [ ...path, "field" ],
402
+ message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" — a claim pair needs an actor-valued entry (a "claim" or "actor" field declaration)`
403
+ }), checkShadowedClaimTarget({
404
+ actionName: action.name,
405
+ field: ref.field,
406
+ resolved: resolved,
407
+ env: env,
408
+ path: [ ...path, "field" ],
409
+ ctx: ctx
410
+ }), entry && ctx.claimedFields.add(entry);
411
+ }
412
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
413
+ type: "field.set",
414
+ target: resolved,
415
+ value: {
416
+ type: "actor"
652
417
  }
653
- }
654
- }
655
- function checkFieldEntryNames(def, issues) {
656
- for (const { entries, path, label } of fieldScopes(def))
657
- checkDuplicates({
658
- names: (entries ?? []).map((entry, n) => ({ name: entry.name, path: [...path, n, "name"] })),
659
- what: `field entry name in ${label}`,
660
- issues
661
- });
418
+ } ] : [];
419
+ return {
420
+ ...stripUndefined({
421
+ name: action.name,
422
+ title: action.title,
423
+ description: action.description,
424
+ group: normalizeGroup(action.group),
425
+ params: action.params,
426
+ effects: action.effects
427
+ }),
428
+ filter: filter,
429
+ ...ops.length > 0 ? {
430
+ ops: ops
431
+ } : {}
432
+ };
662
433
  }
663
- function checkPredicates(def, issues) {
664
- const reserved = new Set(RESERVED_CONDITION_VARS), names = Object.keys(def.predicates ?? {});
665
- for (const name of names)
666
- reserved.has(name) && issues.push({
667
- path: ["predicates", name],
668
- message: `predicate "${name}" shadows the built-in $${name} \u2014 pick another name`
669
- });
670
- for (const [name, groq2] of Object.entries(def.predicates ?? {}))
671
- checkPredicateBody({ name, groq: groq2, names, issues });
672
- }
673
- function checkPredicateBody({
674
- name,
675
- groq: groq2,
676
- names,
677
- issues
678
- }) {
679
- for (const caller of PREDICATE_CALLER_VARS)
680
- referencesVar(groq2, caller) && issues.push({
681
- path: ["predicates", name],
682
- message: `predicate "${name}" reads $${caller} \u2014 predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
434
+
435
+ function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
436
+ const nearest = env.layers.find(l => l.entries.has(field));
437
+ nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
438
+ path: path,
439
+ message: `claim action "${actionName}" targets "${field}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $fields — the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
683
440
  });
684
- for (const other of names)
685
- other === name || !referencesVar(groq2, other) || issues.push({
686
- path: ["predicates", name],
687
- message: `predicate "${name}" references predicate $${other} \u2014 predicates may read built-in vars only (no cross-references; compose at the condition site instead)`
688
- });
689
- }
690
- function referencesVar(groq2, name) {
691
- return GROQ_IDENTIFIER.test(name) ? new RegExp(`\\$${name}\\b`).test(groq2) : !1;
692
441
  }
693
- function checkCanOutsideActionFilters(def, issues) {
694
- for (const site of nonActionFilterConditionSites(def))
695
- referencesVar(site.groq, "can") && issues.push({
696
- path: site.path,
697
- message: `${site.label} reads $can \u2014 $can (the caller's grants) is bound only when an action fires, so it may appear in action filters only`
442
+
443
+ function checkUnclaimedClaimFields(ctx) {
444
+ for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
445
+ path: path,
446
+ message: `claim field "${name}" is never referenced by a claim action — you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw actor entry`
698
447
  });
699
448
  }
700
- function nonActionFilterConditionSites(def) {
701
- const sites = [];
702
- for (const [i, stage] of def.stages.entries()) {
703
- for (const [k, t] of (stage.transitions ?? []).entries())
704
- sites.push({
705
- groq: t.filter,
706
- path: ["stages", i, "transitions", k, "filter"],
707
- label: `transition "${t.name}" filter`
708
- }), collectBindingSites({
709
- effects: t.effects,
710
- path: ["stages", i, "transitions", k, "effects"],
711
- label: `transition "${t.name}"`,
712
- sites
713
- });
714
- for (const [j, activity] of (stage.activities ?? []).entries())
715
- collectActivityConditionSites({ activity, path: ["stages", i, "activities", j], sites });
716
- }
717
- return sites;
718
- }
719
- function collectActivityConditionSites({
720
- activity,
721
- path,
722
- sites
723
- }) {
724
- for (const field of ["filter", "completeWhen", "failWhen"]) {
725
- const groq2 = activity[field];
726
- groq2 !== void 0 && sites.push({ groq: groq2, path: [...path, field], label: `activity "${activity.name}".${field}` });
727
- }
728
- collectBindingSites({
729
- effects: activity.effects,
730
- path: [...path, "effects"],
731
- label: `activity "${activity.name}"`,
732
- sites
733
- });
734
- for (const [a, action] of (activity.actions ?? []).entries())
735
- collectBindingSites({
736
- effects: action.effects,
737
- path: [...path, "actions", a, "effects"],
738
- label: `action "${action.name}"`,
739
- sites
449
+
450
+ function desugarTransition({transition: transition, path: path, stageEnv: stageEnv, ctx: ctx}) {
451
+ const ops = desugarOps({
452
+ ops: transition.ops,
453
+ path: [ ...path, "ops" ],
454
+ env: stageEnv,
455
+ firingActivity: void 0,
456
+ ctx: ctx
740
457
  });
741
- collectSubworkflowSites({ activity, path, sites });
742
- }
743
- function collectSubworkflowSites({
744
- activity,
745
- path,
746
- sites
747
- }) {
748
- const sub = activity.subworkflows;
749
- if (sub === void 0) return;
750
- const subPath = [...path, "subworkflows"];
751
- sites.push({
752
- groq: sub.forEach,
753
- path: [...subPath, "forEach"],
754
- label: `activity "${activity.name}".subworkflows.forEach`
755
- });
756
- for (const [group, record] of [
757
- ["with", sub.with],
758
- ["context", sub.context]
759
- ])
760
- for (const [key, groq2] of Object.entries(record ?? {}))
761
- sites.push({
762
- groq: groq2,
763
- path: [...subPath, group, key],
764
- label: `activity "${activity.name}".subworkflows.${group} "${key}"`
765
- });
766
- }
767
- function collectBindingSites({
768
- effects,
769
- path,
770
- label,
771
- sites
772
- }) {
773
- for (const [e, effect] of (effects ?? []).entries())
774
- for (const [key, groq2] of Object.entries(effect.bindings ?? {}))
775
- sites.push({
776
- groq: groq2,
777
- path: [...path, e, "bindings", key],
778
- label: `${label} effect "${effect.name}" binding "${key}"`
779
- });
780
- }
781
- function checkAssigneesEntries(def, issues) {
782
- for (const { entries, path } of fieldScopes(def))
783
- (entries ?? []).filter((e) => e.type === "assignees").length <= 1 || issues.push({
784
- path,
785
- message: "at most one assignees-kind field entry per scope \u2014 the inbox reads it by kind"
458
+ return {
459
+ ...stripUndefined({
460
+ name: transition.name,
461
+ title: transition.title,
462
+ description: transition.description,
463
+ to: transition.to,
464
+ effects: transition.effects
465
+ }),
466
+ filter: transition.filter ?? "$allActivitiesDone",
467
+ ...ops ? {
468
+ ops: ops
469
+ } : {}
470
+ };
471
+ }
472
+
473
+ function desugarGuard(guard) {
474
+ const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
475
+ return {
476
+ ...rest,
477
+ match: {
478
+ ...matchRest,
479
+ ...idRefs !== void 0 ? {
480
+ idRefs: idRefs.map(printGuardRead)
481
+ } : {}
482
+ },
483
+ ...metadata !== void 0 ? {
484
+ metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
485
+ } : {}
486
+ };
487
+ }
488
+
489
+ function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
490
+ if (ops) return ops.map((op, i) => desugarOp({
491
+ op: op,
492
+ path: [ ...path, i ],
493
+ env: env,
494
+ firingActivity: firingActivity,
495
+ ctx: ctx
496
+ }));
497
+ }
498
+
499
+ function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
500
+ if (op.type === "status.set") {
501
+ const activity = op.activity ?? firingActivity;
502
+ return activity === void 0 && ctx.issues.push({
503
+ path: [ ...path, "activity" ],
504
+ message: "status.set outside an action must name its target activity"
505
+ }), {
506
+ type: "status.set",
507
+ activity: activity ?? "",
508
+ status: op.status
509
+ };
510
+ }
511
+ if (op.type === "audit") return desugarAuditOp({
512
+ op: op,
513
+ path: path,
514
+ env: env,
515
+ ctx: ctx
786
516
  });
517
+ const target = resolveRef({
518
+ ref: op.target,
519
+ env: env,
520
+ path: [ ...path, "target" ],
521
+ ctx: ctx
522
+ }) ?? fallbackRef(op.target);
523
+ return {
524
+ ...op,
525
+ target: target
526
+ };
787
527
  }
788
- function activityShape(activity) {
789
- return {
790
- hasActions: (activity.actions ?? []).length > 0,
791
- hasEffects: (activity.effects ?? []).length > 0,
792
- hasCompleteWhen: activity.completeWhen !== void 0,
793
- hasSubworkflows: activity.subworkflows !== void 0
794
- };
795
- }
796
- const KIND_SHAPE_RULES = {
797
- user: (s) => s.hasActions ? [] : ["needs at least one action for a person to fire"],
798
- service: (s) => s.hasEffects ? [] : ["needs at least one effect \u2014 the automated work it performs"],
799
- manual: (s) => [
800
- 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)",
801
- s.hasSubworkflows ? "is off-system work, not a fan-out \u2014 drop `subworkflows` or move it to its own activity" : void 0
802
- ].filter((m) => m !== void 0),
803
- receive: (s) => [
804
- s.hasCompleteWhen || s.hasSubworkflows ? void 0 : "needs a `completeWhen` (or `subworkflows`) condition to wait on",
805
- s.hasActions ? "has no actor, so it cannot carry actions" : void 0
806
- ].filter((m) => m !== void 0),
807
- script: (s) => [
808
- s.hasActions ? "runs inline with no actor, so it cannot carry actions" : void 0,
809
- s.hasCompleteWhen ? "resolves on activation, so it cannot carry a `completeWhen` (that is a `receive`/`service` wait)" : void 0,
810
- s.hasSubworkflows ? "resolves on activation, so it cannot fan out with `subworkflows`" : void 0
811
- ].filter((m) => m !== void 0)
528
+
529
+ const AUDIT_STAMPS = {
530
+ actor: {
531
+ type: "actor"
532
+ },
533
+ at: {
534
+ type: "now"
535
+ }
812
536
  };
813
- function checkActivityKinds(def, issues) {
814
- for (const [i, stage] of def.stages.entries())
815
- for (const [j, activity] of (stage.activities ?? []).entries()) {
816
- const path = ["stages", i, "activities", j];
817
- if (activity.target !== void 0 && activity.kind !== "manual" && issues.push({
818
- path: [...path, "target"],
819
- message: `activity "${activity.name}" has a \`target\` but is not \`kind: "manual"\` \u2014 a target is the deep-link for off-system manual work`
820
- }), activity.kind !== void 0)
821
- for (const fault of KIND_SHAPE_RULES[activity.kind](activityShape(activity)))
822
- issues.push({
823
- path: [...path, "kind"],
824
- message: `activity "${activity.name}" declares kind "${activity.kind}" but ${fault}`
825
- });
537
+
538
+ function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
539
+ const target = resolveRef({
540
+ ref: op.target,
541
+ env: env,
542
+ path: [ ...path, "target" ],
543
+ ctx: ctx
544
+ }) ?? fallbackRef(op.target);
545
+ if (op.value.type !== "object") return ctx.issues.push({
546
+ path: [ ...path, "value" ],
547
+ message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
548
+ }), {
549
+ type: "field.append",
550
+ target: target,
551
+ value: op.value
552
+ };
553
+ const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
554
+ ...op.value.fields
555
+ };
556
+ for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
557
+ if (stamp in fields) {
558
+ ctx.issues.push({
559
+ path: [ ...path, "value", "fields", stamp ],
560
+ message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
561
+ });
562
+ continue;
563
+ }
564
+ fields[stamp] = source;
826
565
  }
566
+ return {
567
+ type: "field.append",
568
+ target: target,
569
+ value: {
570
+ type: "object",
571
+ fields: fields
572
+ }
573
+ };
574
+ }
575
+
576
+ function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
577
+ const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
578
+ for (const layer of layers) if (layer.entries.has(ref.field)) return {
579
+ scope: layer.scope,
580
+ field: ref.field
581
+ };
582
+ const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
583
+ ctx.issues.push({
584
+ path: path,
585
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
586
+ });
587
+ }
588
+
589
+ function entryAt(env, ref) {
590
+ return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
591
+ }
592
+
593
+ function fallbackRef(ref) {
594
+ return {
595
+ scope: ref.scope ?? "workflow",
596
+ field: ref.field
597
+ };
827
598
  }
828
- function checkWorkflowInvariants(def) {
829
- const issues = [], stageNames = checkStages(def, issues);
830
- 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;
599
+
600
+ function rolesCondition(roles, aliases) {
601
+ if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
831
602
  }
603
+
604
+ function stripUndefined(obj) {
605
+ const out = {};
606
+ for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
607
+ return out;
608
+ }
609
+
832
610
  function defineWorkflow(definition) {
833
- const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow({ schema: AuthoringWorkflowSchema, input: definition, label }), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
834
- if (issues.length > 0) throw new Error(formatValidationError(label, issues));
835
- return stored;
611
+ const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow({
612
+ schema: AuthoringWorkflowSchema,
613
+ input: definition,
614
+ label: label
615
+ }), {definition: stored, issues: desugarIssues} = desugarWorkflow(parsed), issues = [ ...desugarIssues, ...checkWorkflowInvariants(stored) ];
616
+ if (issues.length > 0) throw new Error(formatValidationError(label, issues));
617
+ return stored;
618
+ }
619
+
620
+ function defineWorkflowConfig(config) {
621
+ return parseOrThrow({
622
+ schema: WorkflowConfigSchema,
623
+ input: config,
624
+ label: "defineWorkflowConfig"
625
+ });
836
626
  }
627
+
837
628
  function defineStage(stage) {
838
- return parseOrThrow({
839
- schema: AuthoringStageSchema,
840
- input: stage,
841
- label: labelFor("defineStage", stage)
842
- });
629
+ return parseOrThrow({
630
+ schema: AuthoringStageSchema,
631
+ input: stage,
632
+ label: labelFor("defineStage", stage)
633
+ });
843
634
  }
635
+
844
636
  function defineActivity(activity) {
845
- return parseOrThrow({
846
- schema: AuthoringActivitySchema,
847
- input: activity,
848
- label: labelFor("defineActivity", activity)
849
- });
637
+ return parseOrThrow({
638
+ schema: AuthoringActivitySchema,
639
+ input: activity,
640
+ label: labelFor("defineActivity", activity)
641
+ });
850
642
  }
643
+
851
644
  function defineAction(action) {
852
- return parseOrThrow({
853
- schema: AuthoringActionSchema,
854
- input: action,
855
- label: labelFor("defineAction", action)
856
- });
645
+ return parseOrThrow({
646
+ schema: AuthoringActionSchema,
647
+ input: action,
648
+ label: labelFor("defineAction", action)
649
+ });
857
650
  }
651
+
858
652
  function defineTransition(transition) {
859
- return parseOrThrow({
860
- schema: AuthoringTransitionSchema,
861
- input: transition,
862
- label: transitionLabel(transition)
863
- });
653
+ return parseOrThrow({
654
+ schema: AuthoringTransitionSchema,
655
+ input: transition,
656
+ label: transitionLabel(transition)
657
+ });
864
658
  }
659
+
865
660
  function defineField(entry) {
866
- return parseOrThrow({
867
- schema: AuthoringFieldEntrySchema,
868
- input: entry,
869
- label: labelFor("defineField", entry)
870
- });
661
+ return parseOrThrow({
662
+ schema: AuthoringFieldEntrySchema,
663
+ input: entry,
664
+ label: labelFor("defineField", entry)
665
+ });
871
666
  }
667
+
872
668
  function defineOp(op) {
873
- return parseOrThrow({ schema: AuthoringOpSchema, input: op, label: "defineOp" });
669
+ return parseOrThrow({
670
+ schema: AuthoringOpSchema,
671
+ input: op,
672
+ label: "defineOp"
673
+ });
674
+ }
675
+
676
+ function defineGroup(group) {
677
+ return parseOrThrow({
678
+ schema: GroupSchema,
679
+ input: group,
680
+ label: labelFor("defineGroup", group)
681
+ });
874
682
  }
683
+
875
684
  function defineGuard(guard) {
876
- return parseOrThrow({ schema: GuardSchema, input: guard, label: labelFor("defineGuard", guard) });
685
+ return parseOrThrow({
686
+ schema: AuthoringGuardSchema,
687
+ input: guard,
688
+ label: labelFor("defineGuard", guard)
689
+ });
877
690
  }
691
+
878
692
  function defineEffect(effect) {
879
- return parseOrThrow({
880
- schema: EffectSchema,
881
- input: effect,
882
- label: labelFor("defineEffect", effect)
883
- });
693
+ return parseOrThrow({
694
+ schema: EffectSchema,
695
+ input: effect,
696
+ label: labelFor("defineEffect", effect)
697
+ });
884
698
  }
699
+
885
700
  function transitionLabel(transition) {
886
- const { name, to } = transition;
887
- return typeof name == "string" ? `defineTransition("${name}")` : typeof to == "string" ? `defineTransition(\u2192 "${to}")` : "defineTransition";
701
+ const {name: name, to: to} = transition;
702
+ return typeof name == "string" ? `defineTransition("${name}")` : typeof to == "string" ? `defineTransition( "${to}")` : "defineTransition";
888
703
  }
704
+
889
705
  function defineEffectDescriptor(descriptor) {
890
- if (!descriptor.name) throw new Error("EffectDescriptor missing name");
891
- return descriptor;
892
- }
893
- function parseOrThrow({
894
- schema,
895
- input,
896
- label
897
- }) {
898
- const result = v.safeParse(schema, input);
899
- if (!result.success)
900
- throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
901
- return result.output;
902
- }
903
- function labelFor(fn, value) {
904
- if (value && typeof value == "object" && "name" in value) {
905
- const raw = value.name;
906
- if (typeof raw == "string") return `${fn}("${raw}")`;
907
- }
908
- return fn;
909
- }
910
- export {
911
- defineAction,
912
- defineActivity,
913
- defineEffect,
914
- defineEffectDescriptor,
915
- defineField,
916
- defineGuard,
917
- defineOp,
918
- defineStage,
919
- defineTransition,
920
- defineWorkflow,
921
- groq
922
- };
923
- //# sourceMappingURL=define.js.map
706
+ if (!descriptor.name) throw new Error("EffectDescriptor missing name");
707
+ return descriptor;
708
+ }
709
+
710
+ export { CONDITION_VARS, FILTER_SCOPE_VARS, GUARD_PREDICATE_VARS, RESERVED_CONDITION_VARS, defineAction, defineActivity, defineEffect, defineEffectDescriptor, defineField, defineGroup, defineGuard, defineOp, defineStage, defineTransition, defineWorkflow, defineWorkflowConfig, groq };