@sanity/workflow-engine 0.3.0 → 0.4.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/_chunks-cjs/schema.cjs +291 -317
- package/dist/_chunks-cjs/schema.cjs.map +1 -1
- package/dist/_chunks-es/schema.js +291 -317
- package/dist/_chunks-es/schema.js.map +1 -1
- package/dist/define.cjs +480 -74
- package/dist/define.cjs.map +1 -1
- package/dist/define.d.cts +7404 -4450
- package/dist/define.d.ts +7404 -4450
- package/dist/define.js +482 -76
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +1650 -1521
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10557 -5214
- package/dist/index.d.ts +10557 -5214
- package/dist/index.js +1651 -1522
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/define.js
CHANGED
|
@@ -1,107 +1,512 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
-
import { formatValidationError, issuesFromValibot,
|
|
2
|
+
import { GROQ_IDENTIFIER, formatValidationError, issuesFromValibot, AuthoringWorkflowSchema, AuthoringStageSchema, AuthoringTaskSchema, AuthoringActionSchema, AuthoringTransitionSchema, AuthoringStateEntrySchema, AuthoringOpSchema, GuardSchema, EffectSchema } from "./_chunks-es/schema.js";
|
|
3
|
+
function groq(strings, ...values) {
|
|
4
|
+
return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
|
|
5
|
+
}
|
|
6
|
+
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;
|
|
13
|
+
}
|
|
14
|
+
function desugarWorkflow(authoring) {
|
|
15
|
+
const ctx = { issues: [], claimStates: /* @__PURE__ */ new Map(), claimedStates: /* @__PURE__ */ new Set() }, workflowState = desugarStateEntries(authoring.state, ["state"], ctx), workflowLayer = layerOf(workflowState), stages = authoring.stages.map((stage, i) => {
|
|
16
|
+
const path = ["stages", i], stageState = desugarStateEntries(stage.state, [...path, "state"], ctx), stageEnv = {
|
|
17
|
+
layers: [
|
|
18
|
+
{ scope: "stage", entries: layerOf(stageState) },
|
|
19
|
+
{ scope: "workflow", entries: workflowLayer }
|
|
20
|
+
]
|
|
21
|
+
}, tasks = (stage.tasks ?? []).map(
|
|
22
|
+
(task, j) => desugarTask(task, [...path, "tasks", j], stageEnv, ctx)
|
|
23
|
+
), transitions = (stage.transitions ?? []).map(
|
|
24
|
+
(transition, k) => desugarTransition(transition, [...path, "transitions", k], stageEnv, ctx)
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
...stripUndefined({
|
|
28
|
+
name: stage.name,
|
|
29
|
+
title: stage.title,
|
|
30
|
+
description: stage.description,
|
|
31
|
+
guards: stage.guards
|
|
32
|
+
}),
|
|
33
|
+
...stageState ? { state: stageState } : {},
|
|
34
|
+
...tasks.length > 0 ? { tasks } : {},
|
|
35
|
+
...transitions.length > 0 ? { transitions } : {}
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return checkUnclaimedClaimStates(ctx), { definition: {
|
|
39
|
+
...stripUndefined({
|
|
40
|
+
name: authoring.name,
|
|
41
|
+
version: authoring.version,
|
|
42
|
+
title: authoring.title,
|
|
43
|
+
description: authoring.description,
|
|
44
|
+
initialStage: authoring.initialStage,
|
|
45
|
+
predicates: authoring.predicates
|
|
46
|
+
}),
|
|
47
|
+
...workflowState ? { state: workflowState } : {},
|
|
48
|
+
stages
|
|
49
|
+
}, issues: ctx.issues };
|
|
50
|
+
}
|
|
51
|
+
function desugarStateEntries(entries, path, ctx) {
|
|
52
|
+
return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => {
|
|
53
|
+
if (entry.type !== "claim") return entry;
|
|
54
|
+
const desugared = {
|
|
55
|
+
...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
|
|
56
|
+
type: "value.actor",
|
|
57
|
+
source: { type: "write" }
|
|
58
|
+
};
|
|
59
|
+
return ctx.claimStates.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function layerOf(entries) {
|
|
63
|
+
return new Map((entries ?? []).map((entry) => [entry.name, entry]));
|
|
64
|
+
}
|
|
65
|
+
function desugarTask(task, path, stageEnv, ctx) {
|
|
66
|
+
const taskState = desugarStateEntries(task.state, [...path, "state"], ctx), env = {
|
|
67
|
+
layers: [{ scope: "task", entries: layerOf(taskState) }, ...stageEnv.layers]
|
|
68
|
+
}, ops = desugarOps(task.ops, [...path, "ops"], env, task.name, ctx), actions = (task.actions ?? []).map(
|
|
69
|
+
(action, a) => desugarAction(action, [...path, "actions", a], env, task.name, ctx)
|
|
70
|
+
);
|
|
71
|
+
return {
|
|
72
|
+
...stripUndefined({
|
|
73
|
+
name: task.name,
|
|
74
|
+
title: task.title,
|
|
75
|
+
description: task.description,
|
|
76
|
+
filter: task.filter,
|
|
77
|
+
completeWhen: task.completeWhen,
|
|
78
|
+
failWhen: task.failWhen,
|
|
79
|
+
effects: task.effects,
|
|
80
|
+
subworkflows: task.subworkflows
|
|
81
|
+
}),
|
|
82
|
+
activation: task.activation ?? "manual",
|
|
83
|
+
...taskState ? { state: taskState } : {},
|
|
84
|
+
...ops ? { ops } : {},
|
|
85
|
+
...actions.length > 0 ? { actions } : {}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function desugarAction(action, path, env, taskName, ctx) {
|
|
89
|
+
if ("type" in action)
|
|
90
|
+
return desugarClaimAction(action, path, env, ctx);
|
|
91
|
+
const filter = composeFilter([rolesCondition(action.roles), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
|
|
92
|
+
return action.status !== void 0 && ops.push({ type: "status.set", task: taskName, status: action.status }), {
|
|
93
|
+
...stripUndefined({
|
|
94
|
+
name: action.name,
|
|
95
|
+
title: action.title,
|
|
96
|
+
description: action.description,
|
|
97
|
+
params: action.params,
|
|
98
|
+
effects: action.effects
|
|
99
|
+
}),
|
|
100
|
+
...filter ? { filter } : {},
|
|
101
|
+
...ops.length > 0 ? { ops } : {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function desugarClaimAction(action, path, env, ctx) {
|
|
105
|
+
const ref = typeof action.state == "string" ? { state: action.state } : action.state, resolved = resolveRef(ref, env, [...path, "state"], ctx);
|
|
106
|
+
if (resolved) {
|
|
107
|
+
const entry = entryAt(env, resolved);
|
|
108
|
+
entry && entry.type !== "value.actor" && ctx.issues.push({
|
|
109
|
+
path: [...path, "state"],
|
|
110
|
+
message: `claim action "${action.name}" references "${resolved.state}" of kind "${entry.type}" \u2014 a claim pair needs an actor-valued entry (a "claim" or "value.actor" state declaration)`
|
|
111
|
+
}), checkShadowedClaimTarget(action.name, ref.state, resolved, env, [...path, "state"], ctx), entry && ctx.claimedStates.add(entry);
|
|
112
|
+
}
|
|
113
|
+
const noSteal = `!defined($state.${ref.state})`, filter = composeFilter([noSteal, rolesCondition(action.roles), action.filter]), ops = resolved ? [{ type: "state.set", target: resolved, value: { type: "actor" } }] : [];
|
|
114
|
+
return {
|
|
115
|
+
...stripUndefined({
|
|
116
|
+
name: action.name,
|
|
117
|
+
title: action.title,
|
|
118
|
+
description: action.description,
|
|
119
|
+
params: action.params,
|
|
120
|
+
effects: action.effects
|
|
121
|
+
}),
|
|
122
|
+
filter,
|
|
123
|
+
...ops.length > 0 ? { ops } : {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function checkShadowedClaimTarget(actionName, state, resolved, env, path, ctx) {
|
|
127
|
+
const nearest = env.layers.find((l) => l.entries.has(state));
|
|
128
|
+
nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
|
|
129
|
+
path,
|
|
130
|
+
message: `claim action "${actionName}" targets "${state}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $state \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`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function checkUnclaimedClaimStates(ctx) {
|
|
134
|
+
for (const [entry, { name, path }] of ctx.claimStates)
|
|
135
|
+
ctx.claimedStates.has(entry) || ctx.issues.push({
|
|
136
|
+
path,
|
|
137
|
+
message: `claim state "${name}" is never referenced by a claim action \u2014 you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", state: "${name}" }) or use a raw value.actor entry`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function desugarTransition(transition, path, stageEnv, ctx) {
|
|
141
|
+
const ops = desugarOps(transition.ops, [...path, "ops"], stageEnv, void 0, ctx);
|
|
142
|
+
return {
|
|
143
|
+
...stripUndefined({
|
|
144
|
+
name: transition.name,
|
|
145
|
+
title: transition.title,
|
|
146
|
+
description: transition.description,
|
|
147
|
+
to: transition.to,
|
|
148
|
+
effects: transition.effects
|
|
149
|
+
}),
|
|
150
|
+
filter: transition.filter ?? "$allTasksDone",
|
|
151
|
+
...ops ? { ops } : {}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function desugarOps(ops, path, env, firingTask, ctx) {
|
|
155
|
+
if (ops)
|
|
156
|
+
return ops.map((op, i) => desugarOp(op, [...path, i], env, firingTask, ctx));
|
|
157
|
+
}
|
|
158
|
+
function desugarOp(op, path, env, firingTask, ctx) {
|
|
159
|
+
if (op.type === "status.set") {
|
|
160
|
+
const task = op.task ?? firingTask;
|
|
161
|
+
return task === void 0 && ctx.issues.push({
|
|
162
|
+
path: [...path, "task"],
|
|
163
|
+
message: "status.set outside an action must name its target task"
|
|
164
|
+
}), { type: "status.set", task: task ?? "", status: op.status };
|
|
165
|
+
}
|
|
166
|
+
if (op.type === "audit") return desugarAuditOp(op, path, env, ctx);
|
|
167
|
+
const target = resolveRef(op.target, env, [...path, "target"], ctx) ?? fallbackRef(op.target);
|
|
168
|
+
return { ...op, target };
|
|
169
|
+
}
|
|
170
|
+
const AUDIT_STAMPS = { actor: { type: "actor" }, at: { type: "now" } };
|
|
171
|
+
function desugarAuditOp(op, path, env, ctx) {
|
|
172
|
+
const target = resolveRef(op.target, env, [...path, "target"], ctx) ?? fallbackRef(op.target);
|
|
173
|
+
if (op.value.type !== "object")
|
|
174
|
+
return ctx.issues.push({
|
|
175
|
+
path: [...path, "value"],
|
|
176
|
+
message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
|
|
177
|
+
}), { type: "state.append", target, value: op.value };
|
|
178
|
+
const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = { ...op.value.fields };
|
|
179
|
+
for (const [stamp, source] of [
|
|
180
|
+
[actorField, AUDIT_STAMPS.actor],
|
|
181
|
+
[atField, AUDIT_STAMPS.at]
|
|
182
|
+
]) {
|
|
183
|
+
if (stamp in fields) {
|
|
184
|
+
ctx.issues.push({
|
|
185
|
+
path: [...path, "value", "fields", stamp],
|
|
186
|
+
message: `audit stamp field "${stamp}" collides with an authored domain field \u2014 rename yours or remap the stamp via stampFields`
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
fields[stamp] = source;
|
|
191
|
+
}
|
|
192
|
+
return { type: "state.append", target, value: { type: "object", fields } };
|
|
193
|
+
}
|
|
194
|
+
function resolveRef(ref, env, path, ctx) {
|
|
195
|
+
const layers = ref.scope === void 0 ? env.layers : env.layers.filter((l) => l.scope === ref.scope);
|
|
196
|
+
for (const layer of layers)
|
|
197
|
+
if (layer.entries.has(ref.state)) return { scope: layer.scope, state: ref.state };
|
|
198
|
+
const reachable = env.layers.flatMap((l) => [...l.entries.keys()].map((n) => `${l.scope}:${n}`));
|
|
199
|
+
ctx.issues.push({
|
|
200
|
+
path,
|
|
201
|
+
message: `state reference "${ref.state}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function entryAt(env, ref) {
|
|
205
|
+
return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.state);
|
|
206
|
+
}
|
|
207
|
+
function fallbackRef(ref) {
|
|
208
|
+
return { scope: ref.scope ?? "workflow", state: ref.state };
|
|
209
|
+
}
|
|
210
|
+
function rolesCondition(roles) {
|
|
211
|
+
if (!(!roles || roles.length === 0))
|
|
212
|
+
return groq`count($actor.roles[@ in ${roles}]) > 0`;
|
|
213
|
+
}
|
|
214
|
+
function composeFilter(parts) {
|
|
215
|
+
const present = parts.filter((p) => p !== void 0);
|
|
216
|
+
if (present.length !== 0)
|
|
217
|
+
return present.length === 1 ? present[0] : present.map((p) => `(${p})`).join(" && ");
|
|
218
|
+
}
|
|
219
|
+
function stripUndefined(obj) {
|
|
220
|
+
const out = {};
|
|
221
|
+
for (const [k, value] of Object.entries(obj))
|
|
222
|
+
value !== void 0 && (out[k] = value);
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
const RESERVED_CONDITION_VARS = [
|
|
226
|
+
"state",
|
|
227
|
+
"actor",
|
|
228
|
+
"assigned",
|
|
229
|
+
"can",
|
|
230
|
+
"row",
|
|
231
|
+
"now",
|
|
232
|
+
"effects",
|
|
233
|
+
"tasks",
|
|
234
|
+
"subworkflows",
|
|
235
|
+
"self",
|
|
236
|
+
"stage",
|
|
237
|
+
"parent",
|
|
238
|
+
"ancestors",
|
|
239
|
+
"allTasksDone",
|
|
240
|
+
"anyTaskFailed"
|
|
241
|
+
], PREDICATE_CALLER_VARS = ["actor", "assigned", "can"];
|
|
3
242
|
function knownList(ids) {
|
|
4
243
|
return [...ids].map((s) => `"${s}"`).join(", ");
|
|
5
244
|
}
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
for (const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
245
|
+
function checkDuplicates(names, what, issues) {
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
for (const { name, path } of names)
|
|
248
|
+
seen.has(name) && issues.push({ path, message: `duplicate ${what} "${name}" (already declared earlier)` }), seen.add(name);
|
|
249
|
+
return seen;
|
|
250
|
+
}
|
|
251
|
+
function checkStages(def, issues) {
|
|
252
|
+
const stageNames = checkDuplicates(
|
|
253
|
+
def.stages.map((stage, i) => ({ name: stage.name, path: ["stages", i, "name"] })),
|
|
254
|
+
"stage name",
|
|
255
|
+
issues
|
|
256
|
+
);
|
|
257
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
258
|
+
const taskNames = checkDuplicates(
|
|
259
|
+
(stage.tasks ?? []).map((task, j) => ({
|
|
260
|
+
name: task.name,
|
|
261
|
+
path: ["stages", i, "tasks", j, "name"]
|
|
262
|
+
})),
|
|
263
|
+
`task name in stage "${stage.name}"`,
|
|
264
|
+
issues
|
|
265
|
+
);
|
|
266
|
+
checkDuplicates(
|
|
267
|
+
(stage.transitions ?? []).map((t, k) => ({
|
|
268
|
+
name: t.name,
|
|
269
|
+
path: ["stages", i, "transitions", k, "name"]
|
|
270
|
+
})),
|
|
271
|
+
`transition name in stage "${stage.name}"`,
|
|
272
|
+
issues
|
|
273
|
+
), checkTasks(def, i, taskNames, issues);
|
|
274
|
+
}
|
|
275
|
+
return stageNames;
|
|
276
|
+
}
|
|
277
|
+
function checkTasks(def, i, taskNames, issues) {
|
|
278
|
+
const stage = def.stages[i];
|
|
279
|
+
for (const [j, task] of (stage.tasks ?? []).entries()) {
|
|
280
|
+
const path = ["stages", i, "tasks", j];
|
|
281
|
+
checkDuplicates(
|
|
282
|
+
(task.actions ?? []).map((action, a) => ({
|
|
283
|
+
name: action.name,
|
|
284
|
+
path: [...path, "actions", a, "name"]
|
|
285
|
+
})),
|
|
286
|
+
`action name in task "${task.name}"`,
|
|
287
|
+
issues
|
|
288
|
+
), checkStatusSetTargets(task, taskNames, path, issues);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function checkStatusSetTargets(task, taskNames, path, issues) {
|
|
292
|
+
checkStatusSetOps(task.ops, taskNames, [...path, "ops"], issues);
|
|
293
|
+
for (const [a, action] of (task.actions ?? []).entries())
|
|
294
|
+
checkStatusSetOps(action.ops, taskNames, [...path, "actions", a, "ops"], issues);
|
|
295
|
+
}
|
|
296
|
+
function checkStatusSetOps(ops, taskNames, path, issues) {
|
|
297
|
+
for (const [o, op] of (ops ?? []).entries())
|
|
298
|
+
op.type !== "status.set" || taskNames.has(op.task) || issues.push({
|
|
299
|
+
path: [...path, o, "task"],
|
|
300
|
+
message: `status.set targets task "${op.task}" which is not declared in this stage. Known tasks: ${knownList(taskNames)}`
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function checkInitialStage(def, stageNames, issues) {
|
|
304
|
+
stageNames.has(def.initialStage) || issues.push({
|
|
305
|
+
path: ["initialStage"],
|
|
306
|
+
message: `initialStage "${def.initialStage}" is not a declared stage. Known stages: ${knownList(stageNames)}`
|
|
30
307
|
});
|
|
31
308
|
}
|
|
32
|
-
function checkTransitionTargets(def,
|
|
309
|
+
function checkTransitionTargets(def, stageNames, issues) {
|
|
33
310
|
for (const [i, stage] of def.stages.entries())
|
|
34
311
|
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
35
|
-
|
|
312
|
+
stageNames.has(t.to) || issues.push({
|
|
36
313
|
path: ["stages", i, "transitions", k, "to"],
|
|
37
|
-
message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(
|
|
314
|
+
message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(stageNames)}`
|
|
38
315
|
});
|
|
39
316
|
}
|
|
40
|
-
function
|
|
41
|
-
const
|
|
42
|
-
for (const [i, p] of (def.predicates ?? []).entries())
|
|
43
|
-
predicateIds.has(p.id) && issues.push({
|
|
44
|
-
path: ["predicates", i, "id"],
|
|
45
|
-
message: `duplicate predicate id "${p.id}"`
|
|
46
|
-
}), predicateIds.add(p.id);
|
|
47
|
-
return predicateIds;
|
|
48
|
-
}
|
|
49
|
-
function checkFilterRefs(def, predicateIds, issues) {
|
|
50
|
-
const visit = (g, path) => {
|
|
51
|
-
g === void 0 || typeof g == "string" || predicateIds.has(g.ref) || issues.push({
|
|
52
|
-
path: [...path, "ref"],
|
|
53
|
-
message: `Filter references predicate "${g.ref}" which is not declared. Known predicates: ${knownList(predicateIds) || "(none)"}`
|
|
54
|
-
});
|
|
55
|
-
};
|
|
317
|
+
function checkEffectNames(def, issues) {
|
|
318
|
+
const sites = [];
|
|
56
319
|
for (const [i, stage] of def.stages.entries()) {
|
|
57
|
-
visit(stage.completion, ["stages", i, "completion"]);
|
|
58
320
|
for (const [j, task] of (stage.tasks ?? []).entries()) {
|
|
59
|
-
|
|
321
|
+
collectEffects(task.effects, ["stages", i, "tasks", j, "effects"], sites);
|
|
60
322
|
for (const [a, action] of (task.actions ?? []).entries())
|
|
61
|
-
|
|
323
|
+
collectEffects(action.effects, ["stages", i, "tasks", j, "actions", a, "effects"], sites);
|
|
62
324
|
}
|
|
63
325
|
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
64
|
-
|
|
326
|
+
collectEffects(t.effects, ["stages", i, "transitions", k, "effects"], sites);
|
|
327
|
+
}
|
|
328
|
+
checkDuplicates(sites, "effect name (registry key \u2014 unique per definition)", issues);
|
|
329
|
+
}
|
|
330
|
+
function collectEffects(effects, path, sites) {
|
|
331
|
+
for (const [e, effect] of (effects ?? []).entries())
|
|
332
|
+
sites.push({ name: effect.name, path: [...path, e, "name"] });
|
|
333
|
+
}
|
|
334
|
+
function checkGuardNames(def, issues) {
|
|
335
|
+
const sites = def.stages.flatMap(
|
|
336
|
+
(stage, i) => (stage.guards ?? []).map((guard, g) => ({
|
|
337
|
+
name: guard.name,
|
|
338
|
+
path: ["stages", i, "guards", g, "name"]
|
|
339
|
+
}))
|
|
340
|
+
);
|
|
341
|
+
checkDuplicates(
|
|
342
|
+
sites,
|
|
343
|
+
"guard name (the guard lake _id derives from it \u2014 unique per definition)",
|
|
344
|
+
issues
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
function stateScopes(def) {
|
|
348
|
+
const scopes = [{ entries: def.state, path: ["state"], label: "workflow state" }];
|
|
349
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
350
|
+
scopes.push({
|
|
351
|
+
entries: stage.state,
|
|
352
|
+
path: ["stages", i, "state"],
|
|
353
|
+
label: `stage "${stage.name}" state`
|
|
354
|
+
});
|
|
355
|
+
for (const [j, task] of (stage.tasks ?? []).entries())
|
|
356
|
+
scopes.push({
|
|
357
|
+
entries: task.state,
|
|
358
|
+
path: ["stages", i, "tasks", j, "state"],
|
|
359
|
+
label: `task "${task.name}" state`
|
|
360
|
+
});
|
|
65
361
|
}
|
|
362
|
+
return scopes;
|
|
363
|
+
}
|
|
364
|
+
function checkStateEntryNames(def, issues) {
|
|
365
|
+
for (const { entries, path, label } of stateScopes(def))
|
|
366
|
+
checkDuplicates(
|
|
367
|
+
(entries ?? []).map((entry, n) => ({ name: entry.name, path: [...path, n, "name"] })),
|
|
368
|
+
`state entry name in ${label}`,
|
|
369
|
+
issues
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
function checkPredicates(def, issues) {
|
|
373
|
+
const reserved = new Set(RESERVED_CONDITION_VARS), names = Object.keys(def.predicates ?? {});
|
|
374
|
+
for (const name of names)
|
|
375
|
+
reserved.has(name) && issues.push({
|
|
376
|
+
path: ["predicates", name],
|
|
377
|
+
message: `predicate "${name}" shadows the built-in $${name} \u2014 pick another name`
|
|
378
|
+
});
|
|
379
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {}))
|
|
380
|
+
checkPredicateBody(name, groq2, names, issues);
|
|
381
|
+
}
|
|
382
|
+
function checkPredicateBody(name, groq2, names, issues) {
|
|
383
|
+
for (const caller of PREDICATE_CALLER_VARS)
|
|
384
|
+
referencesVar(groq2, caller) && issues.push({
|
|
385
|
+
path: ["predicates", name],
|
|
386
|
+
message: `predicate "${name}" reads $${caller} \u2014 predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
|
|
387
|
+
});
|
|
388
|
+
for (const other of names)
|
|
389
|
+
other === name || !referencesVar(groq2, other) || issues.push({
|
|
390
|
+
path: ["predicates", name],
|
|
391
|
+
message: `predicate "${name}" references predicate $${other} \u2014 predicates may read built-in vars only (no cross-references; compose at the condition site instead)`
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function referencesVar(groq2, name) {
|
|
395
|
+
return GROQ_IDENTIFIER.test(name) ? new RegExp(`\\$${name}\\b`).test(groq2) : !1;
|
|
396
|
+
}
|
|
397
|
+
function checkCanOutsideActionFilters(def, issues) {
|
|
398
|
+
for (const site of nonActionFilterConditionSites(def))
|
|
399
|
+
referencesVar(site.groq, "can") && issues.push({
|
|
400
|
+
path: site.path,
|
|
401
|
+
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`
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function nonActionFilterConditionSites(def) {
|
|
405
|
+
const sites = [];
|
|
406
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
407
|
+
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
408
|
+
sites.push({
|
|
409
|
+
groq: t.filter,
|
|
410
|
+
path: ["stages", i, "transitions", k, "filter"],
|
|
411
|
+
label: `transition "${t.name}" filter`
|
|
412
|
+
}), collectBindingSites(
|
|
413
|
+
t.effects,
|
|
414
|
+
["stages", i, "transitions", k, "effects"],
|
|
415
|
+
`transition "${t.name}"`,
|
|
416
|
+
sites
|
|
417
|
+
);
|
|
418
|
+
for (const [j, task] of (stage.tasks ?? []).entries())
|
|
419
|
+
collectTaskConditionSites(task, ["stages", i, "tasks", j], sites);
|
|
420
|
+
}
|
|
421
|
+
return sites;
|
|
422
|
+
}
|
|
423
|
+
function collectTaskConditionSites(task, path, sites) {
|
|
424
|
+
for (const field of ["filter", "completeWhen", "failWhen"]) {
|
|
425
|
+
const groq2 = task[field];
|
|
426
|
+
groq2 !== void 0 && sites.push({ groq: groq2, path: [...path, field], label: `task "${task.name}".${field}` });
|
|
427
|
+
}
|
|
428
|
+
collectBindingSites(task.effects, [...path, "effects"], `task "${task.name}"`, sites);
|
|
429
|
+
for (const [a, action] of (task.actions ?? []).entries())
|
|
430
|
+
collectBindingSites(
|
|
431
|
+
action.effects,
|
|
432
|
+
[...path, "actions", a, "effects"],
|
|
433
|
+
`action "${action.name}"`,
|
|
434
|
+
sites
|
|
435
|
+
);
|
|
436
|
+
collectSubworkflowSites(task, path, sites);
|
|
437
|
+
}
|
|
438
|
+
function collectSubworkflowSites(task, path, sites) {
|
|
439
|
+
const sub = task.subworkflows;
|
|
440
|
+
if (sub === void 0) return;
|
|
441
|
+
const subPath = [...path, "subworkflows"];
|
|
442
|
+
sites.push({
|
|
443
|
+
groq: sub.forEach,
|
|
444
|
+
path: [...subPath, "forEach"],
|
|
445
|
+
label: `task "${task.name}".subworkflows.forEach`
|
|
446
|
+
});
|
|
447
|
+
for (const [group, record] of [
|
|
448
|
+
["with", sub.with],
|
|
449
|
+
["context", sub.context]
|
|
450
|
+
])
|
|
451
|
+
for (const [key, groq2] of Object.entries(record ?? {}))
|
|
452
|
+
sites.push({
|
|
453
|
+
groq: groq2,
|
|
454
|
+
path: [...subPath, group, key],
|
|
455
|
+
label: `task "${task.name}".subworkflows.${group} "${key}"`
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function collectBindingSites(effects, path, label, sites) {
|
|
459
|
+
for (const [e, effect] of (effects ?? []).entries())
|
|
460
|
+
for (const [key, groq2] of Object.entries(effect.bindings ?? {}))
|
|
461
|
+
sites.push({
|
|
462
|
+
groq: groq2,
|
|
463
|
+
path: [...path, e, "bindings", key],
|
|
464
|
+
label: `${label} effect "${effect.name}" binding "${key}"`
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function checkAssigneesEntries(def, issues) {
|
|
468
|
+
for (const { entries, path } of stateScopes(def))
|
|
469
|
+
(entries ?? []).filter((e) => e.type === "assignees").length <= 1 || issues.push({
|
|
470
|
+
path,
|
|
471
|
+
message: "at most one assignees-kind state entry per scope \u2014 the inbox reads it by kind"
|
|
472
|
+
});
|
|
66
473
|
}
|
|
67
474
|
function checkWorkflowInvariants(def) {
|
|
68
|
-
const issues = [],
|
|
69
|
-
checkInitialStage(def,
|
|
70
|
-
const predicateIds = collectPredicateIds(def, issues);
|
|
71
|
-
return checkFilterRefs(def, predicateIds, issues), issues;
|
|
475
|
+
const issues = [], stageNames = checkStages(def, issues);
|
|
476
|
+
return checkInitialStage(def, stageNames, issues), checkTransitionTargets(def, stageNames, issues), checkEffectNames(def, issues), checkGuardNames(def, issues), checkStateEntryNames(def, issues), checkPredicates(def, issues), checkCanOutsideActionFilters(def, issues), checkAssigneesEntries(def, issues), issues;
|
|
72
477
|
}
|
|
73
478
|
function defineWorkflow(definition) {
|
|
74
|
-
const
|
|
479
|
+
const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow(AuthoringWorkflowSchema, definition, label), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
|
|
75
480
|
if (issues.length > 0) throw new Error(formatValidationError(label, issues));
|
|
76
|
-
return
|
|
481
|
+
return stored;
|
|
77
482
|
}
|
|
78
483
|
function defineStage(stage) {
|
|
79
|
-
return parseOrThrow(
|
|
484
|
+
return parseOrThrow(AuthoringStageSchema, stage, labelFor("defineStage", stage));
|
|
80
485
|
}
|
|
81
486
|
function defineTask(task) {
|
|
82
|
-
return parseOrThrow(
|
|
487
|
+
return parseOrThrow(AuthoringTaskSchema, task, labelFor("defineTask", task));
|
|
83
488
|
}
|
|
84
489
|
function defineAction(action) {
|
|
85
|
-
return parseOrThrow(
|
|
490
|
+
return parseOrThrow(AuthoringActionSchema, action, labelFor("defineAction", action));
|
|
86
491
|
}
|
|
87
492
|
function defineTransition(transition) {
|
|
88
|
-
return parseOrThrow(
|
|
493
|
+
return parseOrThrow(AuthoringTransitionSchema, transition, transitionLabel(transition));
|
|
89
494
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
return typeof id == "string" ? `defineTransition("${id}")` : typeof to == "string" ? `defineTransition(\u2192 "${to}")` : "defineTransition";
|
|
93
|
-
}
|
|
94
|
-
function defineFilter(filter) {
|
|
95
|
-
return parseOrThrow(FilterSchema, filter, "defineFilter");
|
|
495
|
+
function defineState(entry) {
|
|
496
|
+
return parseOrThrow(AuthoringStateEntrySchema, entry, labelFor("defineState", entry));
|
|
96
497
|
}
|
|
97
|
-
function
|
|
98
|
-
return parseOrThrow(
|
|
498
|
+
function defineOp(op) {
|
|
499
|
+
return parseOrThrow(AuthoringOpSchema, op, "defineOp");
|
|
99
500
|
}
|
|
100
|
-
function
|
|
101
|
-
return parseOrThrow(
|
|
501
|
+
function defineGuard(guard) {
|
|
502
|
+
return parseOrThrow(GuardSchema, guard, labelFor("defineGuard", guard));
|
|
102
503
|
}
|
|
103
504
|
function defineEffect(effect) {
|
|
104
|
-
return parseOrThrow(EffectSchema, effect, labelFor("defineEffect", effect
|
|
505
|
+
return parseOrThrow(EffectSchema, effect, labelFor("defineEffect", effect));
|
|
506
|
+
}
|
|
507
|
+
function transitionLabel(transition) {
|
|
508
|
+
const { name, to } = transition;
|
|
509
|
+
return typeof name == "string" ? `defineTransition("${name}")` : typeof to == "string" ? `defineTransition(\u2192 "${to}")` : "defineTransition";
|
|
105
510
|
}
|
|
106
511
|
function defineEffectDescriptor(descriptor) {
|
|
107
512
|
if (!descriptor.name) throw new Error("EffectDescriptor missing name");
|
|
@@ -113,23 +518,24 @@ function parseOrThrow(schema, input, label) {
|
|
|
113
518
|
throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
|
|
114
519
|
return result.output;
|
|
115
520
|
}
|
|
116
|
-
function labelFor(fn, value
|
|
117
|
-
if (value && typeof value == "object" &&
|
|
118
|
-
const raw = value
|
|
521
|
+
function labelFor(fn, value) {
|
|
522
|
+
if (value && typeof value == "object" && "name" in value) {
|
|
523
|
+
const raw = value.name;
|
|
119
524
|
if (typeof raw == "string") return `${fn}("${raw}")`;
|
|
120
525
|
}
|
|
121
526
|
return fn;
|
|
122
527
|
}
|
|
123
528
|
export {
|
|
124
529
|
defineAction,
|
|
125
|
-
defineAssignee,
|
|
126
530
|
defineEffect,
|
|
127
531
|
defineEffectDescriptor,
|
|
128
|
-
|
|
129
|
-
|
|
532
|
+
defineGuard,
|
|
533
|
+
defineOp,
|
|
130
534
|
defineStage,
|
|
535
|
+
defineState,
|
|
131
536
|
defineTask,
|
|
132
537
|
defineTransition,
|
|
133
|
-
defineWorkflow
|
|
538
|
+
defineWorkflow,
|
|
539
|
+
groq
|
|
134
540
|
};
|
|
135
541
|
//# sourceMappingURL=define.js.map
|