@sanity/workflow-engine 0.2.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 +405 -0
- package/dist/_chunks-cjs/schema.cjs.map +1 -0
- package/dist/_chunks-es/schema.js +390 -0
- package/dist/_chunks-es/schema.js.map +1 -0
- package/dist/define.cjs +483 -477
- 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 +480 -473
- package/dist/define.js.map +1 -1
- package/dist/index.cjs +1911 -1619
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11783 -6206
- package/dist/index.d.ts +11783 -6206
- package/dist/index.js +1908 -1615
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/define.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
-
var v = require("valibot");
|
|
3
|
+
var v = require("valibot"), schema = require("./_chunks-cjs/schema.cjs");
|
|
4
4
|
function _interopNamespaceCompat(e) {
|
|
5
5
|
if (e && typeof e == "object" && "default" in e) return e;
|
|
6
6
|
var n = /* @__PURE__ */ Object.create(null);
|
|
@@ -17,534 +17,540 @@ function _interopNamespaceCompat(e) {
|
|
|
17
17
|
}), n.default = e, Object.freeze(n);
|
|
18
18
|
}
|
|
19
19
|
var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
|
|
20
|
+
function groq(strings, ...values) {
|
|
21
|
+
return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
|
|
22
|
+
}
|
|
23
|
+
function serializeGroqValue(value) {
|
|
24
|
+
const serialized = JSON.stringify(value);
|
|
25
|
+
if (serialized === void 0)
|
|
26
|
+
throw new Error(
|
|
27
|
+
`groq tag cannot serialize ${typeof value} \u2014 interpolate JSON-representable values only`
|
|
28
|
+
);
|
|
29
|
+
return serialized;
|
|
30
|
+
}
|
|
31
|
+
function desugarWorkflow(authoring) {
|
|
32
|
+
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) => {
|
|
33
|
+
const path = ["stages", i], stageState = desugarStateEntries(stage.state, [...path, "state"], ctx), stageEnv = {
|
|
34
|
+
layers: [
|
|
35
|
+
{ scope: "stage", entries: layerOf(stageState) },
|
|
36
|
+
{ scope: "workflow", entries: workflowLayer }
|
|
37
|
+
]
|
|
38
|
+
}, tasks = (stage.tasks ?? []).map(
|
|
39
|
+
(task, j) => desugarTask(task, [...path, "tasks", j], stageEnv, ctx)
|
|
40
|
+
), transitions = (stage.transitions ?? []).map(
|
|
41
|
+
(transition, k) => desugarTransition(transition, [...path, "transitions", k], stageEnv, ctx)
|
|
42
|
+
);
|
|
43
|
+
return {
|
|
44
|
+
...stripUndefined({
|
|
45
|
+
name: stage.name,
|
|
46
|
+
title: stage.title,
|
|
47
|
+
description: stage.description,
|
|
48
|
+
guards: stage.guards
|
|
49
|
+
}),
|
|
50
|
+
...stageState ? { state: stageState } : {},
|
|
51
|
+
...tasks.length > 0 ? { tasks } : {},
|
|
52
|
+
...transitions.length > 0 ? { transitions } : {}
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
return checkUnclaimedClaimStates(ctx), { definition: {
|
|
56
|
+
...stripUndefined({
|
|
57
|
+
name: authoring.name,
|
|
58
|
+
version: authoring.version,
|
|
59
|
+
title: authoring.title,
|
|
60
|
+
description: authoring.description,
|
|
61
|
+
initialStage: authoring.initialStage,
|
|
62
|
+
predicates: authoring.predicates
|
|
63
|
+
}),
|
|
64
|
+
...workflowState ? { state: workflowState } : {},
|
|
65
|
+
stages
|
|
66
|
+
}, issues: ctx.issues };
|
|
67
|
+
}
|
|
68
|
+
function desugarStateEntries(entries, path, ctx) {
|
|
69
|
+
return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => {
|
|
70
|
+
if (entry.type !== "claim") return entry;
|
|
71
|
+
const desugared = {
|
|
72
|
+
...stripUndefined({ name: entry.name, title: entry.title, description: entry.description }),
|
|
73
|
+
type: "value.actor",
|
|
74
|
+
source: { type: "write" }
|
|
75
|
+
};
|
|
76
|
+
return ctx.claimStates.set(desugared, { name: entry.name, path: [...path, i] }), desugared;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function layerOf(entries) {
|
|
80
|
+
return new Map((entries ?? []).map((entry) => [entry.name, entry]));
|
|
81
|
+
}
|
|
82
|
+
function desugarTask(task, path, stageEnv, ctx) {
|
|
83
|
+
const taskState = desugarStateEntries(task.state, [...path, "state"], ctx), env = {
|
|
84
|
+
layers: [{ scope: "task", entries: layerOf(taskState) }, ...stageEnv.layers]
|
|
85
|
+
}, ops = desugarOps(task.ops, [...path, "ops"], env, task.name, ctx), actions = (task.actions ?? []).map(
|
|
86
|
+
(action, a) => desugarAction(action, [...path, "actions", a], env, task.name, ctx)
|
|
87
|
+
);
|
|
88
|
+
return {
|
|
89
|
+
...stripUndefined({
|
|
90
|
+
name: task.name,
|
|
91
|
+
title: task.title,
|
|
92
|
+
description: task.description,
|
|
93
|
+
filter: task.filter,
|
|
94
|
+
completeWhen: task.completeWhen,
|
|
95
|
+
failWhen: task.failWhen,
|
|
96
|
+
effects: task.effects,
|
|
97
|
+
subworkflows: task.subworkflows
|
|
98
|
+
}),
|
|
99
|
+
activation: task.activation ?? "manual",
|
|
100
|
+
...taskState ? { state: taskState } : {},
|
|
101
|
+
...ops ? { ops } : {},
|
|
102
|
+
...actions.length > 0 ? { actions } : {}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function desugarAction(action, path, env, taskName, ctx) {
|
|
106
|
+
if ("type" in action)
|
|
107
|
+
return desugarClaimAction(action, path, env, ctx);
|
|
108
|
+
const filter = composeFilter([rolesCondition(action.roles), action.filter]), ops = desugarOps(action.ops, [...path, "ops"], env, taskName, ctx) ?? [];
|
|
109
|
+
return action.status !== void 0 && ops.push({ type: "status.set", task: taskName, status: action.status }), {
|
|
110
|
+
...stripUndefined({
|
|
111
|
+
name: action.name,
|
|
112
|
+
title: action.title,
|
|
113
|
+
description: action.description,
|
|
114
|
+
params: action.params,
|
|
115
|
+
effects: action.effects
|
|
116
|
+
}),
|
|
117
|
+
...filter ? { filter } : {},
|
|
118
|
+
...ops.length > 0 ? { ops } : {}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function desugarClaimAction(action, path, env, ctx) {
|
|
122
|
+
const ref = typeof action.state == "string" ? { state: action.state } : action.state, resolved = resolveRef(ref, env, [...path, "state"], ctx);
|
|
123
|
+
if (resolved) {
|
|
124
|
+
const entry = entryAt(env, resolved);
|
|
125
|
+
entry && entry.type !== "value.actor" && ctx.issues.push({
|
|
126
|
+
path: [...path, "state"],
|
|
127
|
+
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)`
|
|
128
|
+
}), checkShadowedClaimTarget(action.name, ref.state, resolved, env, [...path, "state"], ctx), entry && ctx.claimedStates.add(entry);
|
|
129
|
+
}
|
|
130
|
+
const noSteal = `!defined($state.${ref.state})`, filter = composeFilter([noSteal, rolesCondition(action.roles), action.filter]), ops = resolved ? [{ type: "state.set", target: resolved, value: { type: "actor" } }] : [];
|
|
131
|
+
return {
|
|
132
|
+
...stripUndefined({
|
|
133
|
+
name: action.name,
|
|
134
|
+
title: action.title,
|
|
135
|
+
description: action.description,
|
|
136
|
+
params: action.params,
|
|
137
|
+
effects: action.effects
|
|
138
|
+
}),
|
|
139
|
+
filter,
|
|
140
|
+
...ops.length > 0 ? { ops } : {}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function checkShadowedClaimTarget(actionName, state, resolved, env, path, ctx) {
|
|
144
|
+
const nearest = env.layers.find((l) => l.entries.has(state));
|
|
145
|
+
nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
|
|
146
|
+
path,
|
|
147
|
+
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`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function checkUnclaimedClaimStates(ctx) {
|
|
151
|
+
for (const [entry, { name, path }] of ctx.claimStates)
|
|
152
|
+
ctx.claimedStates.has(entry) || ctx.issues.push({
|
|
153
|
+
path,
|
|
154
|
+
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`
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function desugarTransition(transition, path, stageEnv, ctx) {
|
|
158
|
+
const ops = desugarOps(transition.ops, [...path, "ops"], stageEnv, void 0, ctx);
|
|
159
|
+
return {
|
|
160
|
+
...stripUndefined({
|
|
161
|
+
name: transition.name,
|
|
162
|
+
title: transition.title,
|
|
163
|
+
description: transition.description,
|
|
164
|
+
to: transition.to,
|
|
165
|
+
effects: transition.effects
|
|
166
|
+
}),
|
|
167
|
+
filter: transition.filter ?? "$allTasksDone",
|
|
168
|
+
...ops ? { ops } : {}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function desugarOps(ops, path, env, firingTask, ctx) {
|
|
172
|
+
if (ops)
|
|
173
|
+
return ops.map((op, i) => desugarOp(op, [...path, i], env, firingTask, ctx));
|
|
174
|
+
}
|
|
175
|
+
function desugarOp(op, path, env, firingTask, ctx) {
|
|
176
|
+
if (op.type === "status.set") {
|
|
177
|
+
const task = op.task ?? firingTask;
|
|
178
|
+
return task === void 0 && ctx.issues.push({
|
|
179
|
+
path: [...path, "task"],
|
|
180
|
+
message: "status.set outside an action must name its target task"
|
|
181
|
+
}), { type: "status.set", task: task ?? "", status: op.status };
|
|
182
|
+
}
|
|
183
|
+
if (op.type === "audit") return desugarAuditOp(op, path, env, ctx);
|
|
184
|
+
const target = resolveRef(op.target, env, [...path, "target"], ctx) ?? fallbackRef(op.target);
|
|
185
|
+
return { ...op, target };
|
|
186
|
+
}
|
|
187
|
+
const AUDIT_STAMPS = { actor: { type: "actor" }, at: { type: "now" } };
|
|
188
|
+
function desugarAuditOp(op, path, env, ctx) {
|
|
189
|
+
const target = resolveRef(op.target, env, [...path, "target"], ctx) ?? fallbackRef(op.target);
|
|
190
|
+
if (op.value.type !== "object")
|
|
191
|
+
return ctx.issues.push({
|
|
192
|
+
path: [...path, "value"],
|
|
193
|
+
message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
|
|
194
|
+
}), { type: "state.append", target, value: op.value };
|
|
195
|
+
const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = { ...op.value.fields };
|
|
196
|
+
for (const [stamp, source] of [
|
|
197
|
+
[actorField, AUDIT_STAMPS.actor],
|
|
198
|
+
[atField, AUDIT_STAMPS.at]
|
|
199
|
+
]) {
|
|
200
|
+
if (stamp in fields) {
|
|
201
|
+
ctx.issues.push({
|
|
202
|
+
path: [...path, "value", "fields", stamp],
|
|
203
|
+
message: `audit stamp field "${stamp}" collides with an authored domain field \u2014 rename yours or remap the stamp via stampFields`
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
fields[stamp] = source;
|
|
208
|
+
}
|
|
209
|
+
return { type: "state.append", target, value: { type: "object", fields } };
|
|
210
|
+
}
|
|
211
|
+
function resolveRef(ref, env, path, ctx) {
|
|
212
|
+
const layers = ref.scope === void 0 ? env.layers : env.layers.filter((l) => l.scope === ref.scope);
|
|
213
|
+
for (const layer of layers)
|
|
214
|
+
if (layer.entries.has(ref.state)) return { scope: layer.scope, state: ref.state };
|
|
215
|
+
const reachable = env.layers.flatMap((l) => [...l.entries.keys()].map((n) => `${l.scope}:${n}`));
|
|
216
|
+
ctx.issues.push({
|
|
217
|
+
path,
|
|
218
|
+
message: `state reference "${ref.state}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function entryAt(env, ref) {
|
|
222
|
+
return env.layers.find((l) => l.scope === ref.scope)?.entries.get(ref.state);
|
|
223
|
+
}
|
|
224
|
+
function fallbackRef(ref) {
|
|
225
|
+
return { scope: ref.scope ?? "workflow", state: ref.state };
|
|
226
|
+
}
|
|
227
|
+
function rolesCondition(roles) {
|
|
228
|
+
if (!(!roles || roles.length === 0))
|
|
229
|
+
return groq`count($actor.roles[@ in ${roles}]) > 0`;
|
|
230
|
+
}
|
|
231
|
+
function composeFilter(parts) {
|
|
232
|
+
const present = parts.filter((p) => p !== void 0);
|
|
233
|
+
if (present.length !== 0)
|
|
234
|
+
return present.length === 1 ? present[0] : present.map((p) => `(${p})`).join(" && ");
|
|
235
|
+
}
|
|
236
|
+
function stripUndefined(obj) {
|
|
237
|
+
const out = {};
|
|
238
|
+
for (const [k, value] of Object.entries(obj))
|
|
239
|
+
value !== void 0 && (out[k] = value);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
const RESERVED_CONDITION_VARS = [
|
|
243
|
+
"state",
|
|
244
|
+
"actor",
|
|
245
|
+
"assigned",
|
|
246
|
+
"can",
|
|
247
|
+
"row",
|
|
248
|
+
"now",
|
|
249
|
+
"effects",
|
|
250
|
+
"tasks",
|
|
251
|
+
"subworkflows",
|
|
252
|
+
"self",
|
|
253
|
+
"stage",
|
|
254
|
+
"parent",
|
|
255
|
+
"ancestors",
|
|
256
|
+
"allTasksDone",
|
|
257
|
+
"anyTaskFailed"
|
|
258
|
+
], PREDICATE_CALLER_VARS = ["actor", "assigned", "can"];
|
|
20
259
|
function knownList(ids) {
|
|
21
260
|
return [...ids].map((s) => `"${s}"`).join(", ");
|
|
22
261
|
}
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
for (const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
262
|
+
function checkDuplicates(names, what, issues) {
|
|
263
|
+
const seen = /* @__PURE__ */ new Set();
|
|
264
|
+
for (const { name, path } of names)
|
|
265
|
+
seen.has(name) && issues.push({ path, message: `duplicate ${what} "${name}" (already declared earlier)` }), seen.add(name);
|
|
266
|
+
return seen;
|
|
267
|
+
}
|
|
268
|
+
function checkStages(def, issues) {
|
|
269
|
+
const stageNames = checkDuplicates(
|
|
270
|
+
def.stages.map((stage, i) => ({ name: stage.name, path: ["stages", i, "name"] })),
|
|
271
|
+
"stage name",
|
|
272
|
+
issues
|
|
273
|
+
);
|
|
274
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
275
|
+
const taskNames = checkDuplicates(
|
|
276
|
+
(stage.tasks ?? []).map((task, j) => ({
|
|
277
|
+
name: task.name,
|
|
278
|
+
path: ["stages", i, "tasks", j, "name"]
|
|
279
|
+
})),
|
|
280
|
+
`task name in stage "${stage.name}"`,
|
|
281
|
+
issues
|
|
282
|
+
);
|
|
283
|
+
checkDuplicates(
|
|
284
|
+
(stage.transitions ?? []).map((t, k) => ({
|
|
285
|
+
name: t.name,
|
|
286
|
+
path: ["stages", i, "transitions", k, "name"]
|
|
287
|
+
})),
|
|
288
|
+
`transition name in stage "${stage.name}"`,
|
|
289
|
+
issues
|
|
290
|
+
), checkTasks(def, i, taskNames, issues);
|
|
291
|
+
}
|
|
292
|
+
return stageNames;
|
|
293
|
+
}
|
|
294
|
+
function checkTasks(def, i, taskNames, issues) {
|
|
295
|
+
const stage = def.stages[i];
|
|
296
|
+
for (const [j, task] of (stage.tasks ?? []).entries()) {
|
|
297
|
+
const path = ["stages", i, "tasks", j];
|
|
298
|
+
checkDuplicates(
|
|
299
|
+
(task.actions ?? []).map((action, a) => ({
|
|
300
|
+
name: action.name,
|
|
301
|
+
path: [...path, "actions", a, "name"]
|
|
302
|
+
})),
|
|
303
|
+
`action name in task "${task.name}"`,
|
|
304
|
+
issues
|
|
305
|
+
), checkStatusSetTargets(task, taskNames, path, issues);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function checkStatusSetTargets(task, taskNames, path, issues) {
|
|
309
|
+
checkStatusSetOps(task.ops, taskNames, [...path, "ops"], issues);
|
|
310
|
+
for (const [a, action] of (task.actions ?? []).entries())
|
|
311
|
+
checkStatusSetOps(action.ops, taskNames, [...path, "actions", a, "ops"], issues);
|
|
312
|
+
}
|
|
313
|
+
function checkStatusSetOps(ops, taskNames, path, issues) {
|
|
314
|
+
for (const [o, op] of (ops ?? []).entries())
|
|
315
|
+
op.type !== "status.set" || taskNames.has(op.task) || issues.push({
|
|
316
|
+
path: [...path, o, "task"],
|
|
317
|
+
message: `status.set targets task "${op.task}" which is not declared in this stage. Known tasks: ${knownList(taskNames)}`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function checkInitialStage(def, stageNames, issues) {
|
|
321
|
+
stageNames.has(def.initialStage) || issues.push({
|
|
322
|
+
path: ["initialStage"],
|
|
323
|
+
message: `initialStage "${def.initialStage}" is not a declared stage. Known stages: ${knownList(stageNames)}`
|
|
47
324
|
});
|
|
48
325
|
}
|
|
49
|
-
function checkTransitionTargets(def,
|
|
326
|
+
function checkTransitionTargets(def, stageNames, issues) {
|
|
50
327
|
for (const [i, stage] of def.stages.entries())
|
|
51
328
|
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
52
|
-
|
|
329
|
+
stageNames.has(t.to) || issues.push({
|
|
53
330
|
path: ["stages", i, "transitions", k, "to"],
|
|
54
|
-
message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(
|
|
331
|
+
message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(stageNames)}`
|
|
55
332
|
});
|
|
56
333
|
}
|
|
57
|
-
function
|
|
58
|
-
const
|
|
59
|
-
for (const [i, p] of (def.predicates ?? []).entries())
|
|
60
|
-
predicateIds.has(p.id) && issues.push({
|
|
61
|
-
path: ["predicates", i, "id"],
|
|
62
|
-
message: `duplicate predicate id "${p.id}"`
|
|
63
|
-
}), predicateIds.add(p.id);
|
|
64
|
-
return predicateIds;
|
|
65
|
-
}
|
|
66
|
-
function checkFilterRefs(def, predicateIds, issues) {
|
|
67
|
-
const visit = (g, path) => {
|
|
68
|
-
g === void 0 || typeof g == "string" || predicateIds.has(g.ref) || issues.push({
|
|
69
|
-
path: [...path, "ref"],
|
|
70
|
-
message: `Filter references predicate "${g.ref}" which is not declared. Known predicates: ${knownList(predicateIds) || "(none)"}`
|
|
71
|
-
});
|
|
72
|
-
};
|
|
334
|
+
function checkEffectNames(def, issues) {
|
|
335
|
+
const sites = [];
|
|
73
336
|
for (const [i, stage] of def.stages.entries()) {
|
|
74
|
-
visit(stage.completion, ["stages", i, "completion"]);
|
|
75
337
|
for (const [j, task] of (stage.tasks ?? []).entries()) {
|
|
76
|
-
|
|
338
|
+
collectEffects(task.effects, ["stages", i, "tasks", j, "effects"], sites);
|
|
77
339
|
for (const [a, action] of (task.actions ?? []).entries())
|
|
78
|
-
|
|
340
|
+
collectEffects(action.effects, ["stages", i, "tasks", j, "actions", a, "effects"], sites);
|
|
79
341
|
}
|
|
80
342
|
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
81
|
-
|
|
343
|
+
collectEffects(t.effects, ["stages", i, "transitions", k, "effects"], sites);
|
|
82
344
|
}
|
|
345
|
+
checkDuplicates(sites, "effect name (registry key \u2014 unique per definition)", issues);
|
|
83
346
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
options,
|
|
100
|
-
`Invalid option: expected one of ${options.map((o) => `"${o}"`).join("|")}`
|
|
347
|
+
function collectEffects(effects, path, sites) {
|
|
348
|
+
for (const [e, effect] of (effects ?? []).entries())
|
|
349
|
+
sites.push({ name: effect.name, path: [...path, e, "name"] });
|
|
350
|
+
}
|
|
351
|
+
function checkGuardNames(def, issues) {
|
|
352
|
+
const sites = def.stages.flatMap(
|
|
353
|
+
(stage, i) => (stage.guards ?? []).map((guard, g) => ({
|
|
354
|
+
name: guard.name,
|
|
355
|
+
path: ["stages", i, "guards", g, "name"]
|
|
356
|
+
}))
|
|
357
|
+
);
|
|
358
|
+
checkDuplicates(
|
|
359
|
+
sites,
|
|
360
|
+
"guard name (the guard lake _id derives from it \u2014 unique per definition)",
|
|
361
|
+
issues
|
|
101
362
|
);
|
|
102
363
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
364
|
+
function stateScopes(def) {
|
|
365
|
+
const scopes = [{ entries: def.state, path: ["state"], label: "workflow state" }];
|
|
366
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
367
|
+
scopes.push({
|
|
368
|
+
entries: stage.state,
|
|
369
|
+
path: ["stages", i, "state"],
|
|
370
|
+
label: `stage "${stage.name}" state`
|
|
371
|
+
});
|
|
372
|
+
for (const [j, task] of (stage.tasks ?? []).entries())
|
|
373
|
+
scopes.push({
|
|
374
|
+
entries: task.state,
|
|
375
|
+
path: ["stages", i, "tasks", j, "state"],
|
|
376
|
+
label: `task "${task.name}" state`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return scopes;
|
|
380
|
+
}
|
|
381
|
+
function checkStateEntryNames(def, issues) {
|
|
382
|
+
for (const { entries, path, label } of stateScopes(def))
|
|
383
|
+
checkDuplicates(
|
|
384
|
+
(entries ?? []).map((entry, n) => ({ name: entry.name, path: [...path, n, "name"] })),
|
|
385
|
+
`state entry name in ${label}`,
|
|
386
|
+
issues
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
function checkPredicates(def, issues) {
|
|
390
|
+
const reserved = new Set(RESERVED_CONDITION_VARS), names = Object.keys(def.predicates ?? {});
|
|
391
|
+
for (const name of names)
|
|
392
|
+
reserved.has(name) && issues.push({
|
|
393
|
+
path: ["predicates", name],
|
|
394
|
+
message: `predicate "${name}" shadows the built-in $${name} \u2014 pick another name`
|
|
395
|
+
});
|
|
396
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {}))
|
|
397
|
+
checkPredicateBody(name, groq2, names, issues);
|
|
398
|
+
}
|
|
399
|
+
function checkPredicateBody(name, groq2, names, issues) {
|
|
400
|
+
for (const caller of PREDICATE_CALLER_VARS)
|
|
401
|
+
referencesVar(groq2, caller) && issues.push({
|
|
402
|
+
path: ["predicates", name],
|
|
403
|
+
message: `predicate "${name}" reads $${caller} \u2014 predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
|
|
404
|
+
});
|
|
405
|
+
for (const other of names)
|
|
406
|
+
other === name || !referencesVar(groq2, other) || issues.push({
|
|
407
|
+
path: ["predicates", name],
|
|
408
|
+
message: `predicate "${name}" references predicate $${other} \u2014 predicates may read built-in vars only (no cross-references; compose at the condition site instead)`
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function referencesVar(groq2, name) {
|
|
412
|
+
return schema.GROQ_IDENTIFIER.test(name) ? new RegExp(`\\$${name}\\b`).test(groq2) : !1;
|
|
413
|
+
}
|
|
414
|
+
function checkCanOutsideActionFilters(def, issues) {
|
|
415
|
+
for (const site of nonActionFilterConditionSites(def))
|
|
416
|
+
referencesVar(site.groq, "can") && issues.push({
|
|
417
|
+
path: site.path,
|
|
418
|
+
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`
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function nonActionFilterConditionSites(def) {
|
|
422
|
+
const sites = [];
|
|
423
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
424
|
+
for (const [k, t] of (stage.transitions ?? []).entries())
|
|
425
|
+
sites.push({
|
|
426
|
+
groq: t.filter,
|
|
427
|
+
path: ["stages", i, "transitions", k, "filter"],
|
|
428
|
+
label: `transition "${t.name}" filter`
|
|
429
|
+
}), collectBindingSites(
|
|
430
|
+
t.effects,
|
|
431
|
+
["stages", i, "transitions", k, "effects"],
|
|
432
|
+
`transition "${t.name}"`,
|
|
433
|
+
sites
|
|
434
|
+
);
|
|
435
|
+
for (const [j, task] of (stage.tasks ?? []).entries())
|
|
436
|
+
collectTaskConditionSites(task, ["stages", i, "tasks", j], sites);
|
|
437
|
+
}
|
|
438
|
+
return sites;
|
|
439
|
+
}
|
|
440
|
+
function collectTaskConditionSites(task, path, sites) {
|
|
441
|
+
for (const field of ["filter", "completeWhen", "failWhen"]) {
|
|
442
|
+
const groq2 = task[field];
|
|
443
|
+
groq2 !== void 0 && sites.push({ groq: groq2, path: [...path, field], label: `task "${task.name}".${field}` });
|
|
444
|
+
}
|
|
445
|
+
collectBindingSites(task.effects, [...path, "effects"], `task "${task.name}"`, sites);
|
|
446
|
+
for (const [a, action] of (task.actions ?? []).entries())
|
|
447
|
+
collectBindingSites(
|
|
448
|
+
action.effects,
|
|
449
|
+
[...path, "actions", a, "effects"],
|
|
450
|
+
`action "${action.name}"`,
|
|
451
|
+
sites
|
|
452
|
+
);
|
|
453
|
+
collectSubworkflowSites(task, path, sites);
|
|
454
|
+
}
|
|
455
|
+
function collectSubworkflowSites(task, path, sites) {
|
|
456
|
+
const sub = task.subworkflows;
|
|
457
|
+
if (sub === void 0) return;
|
|
458
|
+
const subPath = [...path, "subworkflows"];
|
|
459
|
+
sites.push({
|
|
460
|
+
groq: sub.forEach,
|
|
461
|
+
path: [...subPath, "forEach"],
|
|
462
|
+
label: `task "${task.name}".subworkflows.forEach`
|
|
463
|
+
});
|
|
464
|
+
for (const [group, record] of [
|
|
465
|
+
["with", sub.with],
|
|
466
|
+
["context", sub.context]
|
|
163
467
|
])
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
191
|
-
]), StateSlotSourceSchema = v__namespace.union([
|
|
192
|
-
v__namespace.strictObject({ kind: v__namespace.literal("init") }),
|
|
193
|
-
v__namespace.strictObject({
|
|
194
|
-
kind: v__namespace.literal("query"),
|
|
195
|
-
query: NonEmpty,
|
|
196
|
-
resource: v__namespace.optional(v__namespace.union([v__namespace.literal("workflow"), v__namespace.literal("subject"), GdrSchema]))
|
|
197
|
-
}),
|
|
198
|
-
v__namespace.strictObject({ kind: v__namespace.literal("write") }),
|
|
199
|
-
/**
|
|
200
|
-
* Pre-populate the slot at resolution time with a constant value.
|
|
201
|
-
* Useful for stage- or task-scope slots that should start non-empty
|
|
202
|
-
* (e.g. a default headline, a starting tally).
|
|
203
|
-
*/
|
|
204
|
-
v__namespace.strictObject({ kind: v__namespace.literal("literal"), value: v__namespace.unknown() }),
|
|
205
|
-
/**
|
|
206
|
-
* Pre-populate by reading another already-resolved slot. `scope:
|
|
207
|
-
* "workflow"` reads from the workflow-scope `state[]` (resolved at
|
|
208
|
-
* startInstance). `scope: "stage"` reads from an earlier slot in
|
|
209
|
-
* this same stage entry's `state[]` (sequential resolution).
|
|
210
|
-
* Optional `path` selects a nested field (e.g. `path: "id"` on a
|
|
211
|
-
* doc.ref slot).
|
|
212
|
-
*/
|
|
213
|
-
v__namespace.strictObject({
|
|
214
|
-
kind: v__namespace.literal("stateRead"),
|
|
215
|
-
scope: v__namespace.union([v__namespace.literal("workflow"), v__namespace.literal("stage")]),
|
|
216
|
-
slotId: NonEmpty,
|
|
217
|
-
path: v__namespace.optional(v__namespace.string())
|
|
218
|
-
})
|
|
219
|
-
]), StateSlotKindSchema = picklist([
|
|
220
|
-
"workflow.state.doc.ref",
|
|
221
|
-
"workflow.state.doc.refs",
|
|
222
|
-
// Release reference. A workflow declares this slot to say "I target a
|
|
223
|
-
// Content Release"; the runtime fills it with a specific release at
|
|
224
|
-
// start time, and the engine auto-derives the instance's read
|
|
225
|
-
// perspective from it.
|
|
226
|
-
"workflow.state.release.ref",
|
|
227
|
-
"workflow.state.query",
|
|
228
|
-
"workflow.state.value.string",
|
|
229
|
-
"workflow.state.value.url",
|
|
230
|
-
"workflow.state.value.number",
|
|
231
|
-
"workflow.state.value.boolean",
|
|
232
|
-
"workflow.state.value.dateTime",
|
|
233
|
-
"workflow.state.value.actor",
|
|
234
|
-
"workflow.state.checklist",
|
|
235
|
-
"workflow.state.notes",
|
|
236
|
-
"workflow.state.assignees"
|
|
237
|
-
]), StateSlotSchema = v__namespace.strictObject({
|
|
238
|
-
type: StateSlotKindSchema,
|
|
239
|
-
id: NonEmpty,
|
|
240
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
241
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
242
|
-
source: StateSlotSourceSchema
|
|
243
|
-
}), FilterSchema = v__namespace.union([
|
|
244
|
-
// Inline GROQ — only reserved params allowed at runtime.
|
|
245
|
-
NonEmpty,
|
|
246
|
-
// Reference to a named predicate, optionally with named args.
|
|
247
|
-
v__namespace.strictObject({
|
|
248
|
-
ref: NonEmpty,
|
|
249
|
-
args: v__namespace.optional(v__namespace.record(v__namespace.string(), v__namespace.unknown()))
|
|
250
|
-
})
|
|
251
|
-
]), PredicateParamSchema = v__namespace.strictObject({
|
|
252
|
-
id: NonEmpty,
|
|
253
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
254
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
255
|
-
type: picklist(["string", "number", "boolean", "string[]", "reference"]),
|
|
256
|
-
enum: v__namespace.optional(v__namespace.array(v__namespace.string()))
|
|
257
|
-
}), PredicateSchema = v__namespace.strictObject({
|
|
258
|
-
type: v__namespace.optional(v__namespace.literal("workflow.predicate")),
|
|
259
|
-
id: NonEmpty,
|
|
260
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
261
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
262
|
-
groq: NonEmpty,
|
|
263
|
-
params: v__namespace.optional(v__namespace.array(PredicateParamSchema))
|
|
264
|
-
}), EffectSchema = v__namespace.strictObject({
|
|
265
|
-
type: v__namespace.optional(v__namespace.literal("workflow.effect")),
|
|
266
|
-
id: NonEmpty,
|
|
267
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
268
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
269
|
-
// Bindings resolve typed Source values at commit time, producing
|
|
270
|
-
// concrete JSON in the queued effect's `params`. NO GROQ.
|
|
271
|
-
bindings: v__namespace.optional(v__namespace.record(v__namespace.string(), SourceSchema)),
|
|
272
|
-
input: v__namespace.optional(v__namespace.record(v__namespace.string(), v__namespace.unknown()))
|
|
273
|
-
}), TerminalTaskStatus = picklist(["done", "skipped", "failed"]), PermissionSchema = picklist(DOCUMENT_VALUE_PERMISSIONS), ActionParamSchema = v__namespace.strictObject({
|
|
274
|
-
type: v__namespace.optional(v__namespace.literal("workflow.action.param")),
|
|
275
|
-
id: NonEmpty,
|
|
276
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
277
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
278
|
-
paramType: picklist([
|
|
279
|
-
"string",
|
|
280
|
-
"number",
|
|
281
|
-
"boolean",
|
|
282
|
-
"url",
|
|
283
|
-
"dateTime",
|
|
284
|
-
"actor",
|
|
285
|
-
"doc.ref",
|
|
286
|
-
"doc.refs",
|
|
287
|
-
"json"
|
|
288
|
-
]),
|
|
289
|
-
required: v__namespace.optional(v__namespace.boolean())
|
|
290
|
-
}), ActionSchema = v__namespace.strictObject({
|
|
291
|
-
type: v__namespace.optional(v__namespace.literal("workflow.action")),
|
|
292
|
-
id: NonEmpty,
|
|
293
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
294
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
295
|
-
filter: v__namespace.optional(FilterSchema),
|
|
296
|
-
/** Caller-supplied params, validated before ops/effects run. */
|
|
297
|
-
params: v__namespace.optional(v__namespace.array(ActionParamSchema)),
|
|
298
|
-
/**
|
|
299
|
-
* Status the task flips to when this action fires. Omit to leave
|
|
300
|
-
* the task status unchanged — used by intermediate `claim` /
|
|
301
|
-
* `assign` / partial-update actions where the task should stay
|
|
302
|
-
* active so a later action can fire on it. Declared values must
|
|
303
|
-
* be terminal (`done` / `skipped` / `failed`); the engine never
|
|
304
|
-
* mid-flip back to `active` outside of the `workflow.op.status.set`
|
|
305
|
-
* op explicit path.
|
|
306
|
-
*/
|
|
307
|
-
setStatus: v__namespace.optional(TerminalTaskStatus),
|
|
308
|
-
/**
|
|
309
|
-
* Inline state-mutation primitives executed during the action commit.
|
|
310
|
-
* Each op runs against the in-memory mutation (deterministic, no
|
|
311
|
-
* external I/O). Resolved Sources turn into concrete JSON before the
|
|
312
|
-
* op applies. Logged on `history[]` as `workflow.history.opApplied`
|
|
313
|
-
* and surfaced on the `ActionResult.ranOps`.
|
|
314
|
-
*/
|
|
315
|
-
ops: v__namespace.optional(v__namespace.array(OpSchema)),
|
|
316
|
-
effects: v__namespace.optional(v__namespace.array(EffectSchema)),
|
|
317
|
-
/**
|
|
318
|
-
* Role gating — OR-match against `Actor.roles[]`. If omitted, no role
|
|
319
|
-
* gate. `"*"` in Actor.roles always matches (bench / all-access).
|
|
320
|
-
*/
|
|
321
|
-
roles: v__namespace.optional(v__namespace.array(NonEmpty)),
|
|
322
|
-
/**
|
|
323
|
-
* When true, the actor must appear in the task's `assignees[]` (as a
|
|
324
|
-
* user-kind assignee matching by id) for the action to be allowed.
|
|
325
|
-
*/
|
|
326
|
-
requireAssignment: v__namespace.optional(v__namespace.boolean()),
|
|
327
|
-
/**
|
|
328
|
-
* Which permission on the workflow.instance document the actor must
|
|
329
|
-
* hold (per supplied `grants`). Defaults to "update". Only checked
|
|
330
|
-
* when grants are supplied at evaluate/fireAction time.
|
|
331
|
-
*/
|
|
332
|
-
requiredPermission: v__namespace.optional(PermissionSchema)
|
|
333
|
-
}), CompletionPolicySchema = v__namespace.union([
|
|
334
|
-
v__namespace.strictObject({ kind: v__namespace.literal("all") }),
|
|
335
|
-
v__namespace.strictObject({ kind: v__namespace.literal("any") }),
|
|
336
|
-
v__namespace.strictObject({
|
|
337
|
-
kind: v__namespace.literal("count"),
|
|
338
|
-
n: PositiveInt
|
|
339
|
-
})
|
|
340
|
-
]), LogicalDefinitionRefSchema = v__namespace.strictObject({
|
|
341
|
-
workflowId: NonEmpty,
|
|
342
|
-
version: v__namespace.optional(v__namespace.union([PositiveInt, v__namespace.literal("latest")]))
|
|
343
|
-
}), SpawnForEachSchema = v__namespace.strictObject({
|
|
344
|
-
groq: NonEmpty,
|
|
345
|
-
as: v__namespace.strictObject({
|
|
346
|
-
initialState: v__namespace.optional(
|
|
347
|
-
v__namespace.array(
|
|
348
|
-
v__namespace.strictObject({
|
|
349
|
-
type: StateSlotKindSchema,
|
|
350
|
-
id: NonEmpty,
|
|
351
|
-
value: SourceSchema
|
|
352
|
-
})
|
|
353
|
-
)
|
|
354
|
-
),
|
|
355
|
-
effectsContext: v__namespace.optional(v__namespace.record(v__namespace.string(), SourceSchema))
|
|
356
|
-
})
|
|
357
|
-
}), SpawnSchema = v__namespace.strictObject({
|
|
358
|
-
type: v__namespace.optional(v__namespace.literal("workflow.spawn")),
|
|
359
|
-
id: v__namespace.optional(v__namespace.string()),
|
|
360
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
361
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
362
|
-
definitionRef: LogicalDefinitionRefSchema,
|
|
363
|
-
forEach: SpawnForEachSchema,
|
|
364
|
-
completionPolicy: v__namespace.optional(CompletionPolicySchema)
|
|
365
|
-
}), AssigneeSchema = v__namespace.variant("kind", [
|
|
366
|
-
v__namespace.strictObject({ kind: v__namespace.literal("user"), id: NonEmpty }),
|
|
367
|
-
v__namespace.strictObject({ kind: v__namespace.literal("role"), role: NonEmpty })
|
|
368
|
-
]), TaskSchema = v__namespace.strictObject({
|
|
369
|
-
type: v__namespace.optional(v__namespace.literal("workflow.task")),
|
|
370
|
-
id: NonEmpty,
|
|
371
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
372
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
373
|
-
assignees: v__namespace.optional(v__namespace.array(AssigneeSchema)),
|
|
374
|
-
filter: v__namespace.optional(FilterSchema),
|
|
375
|
-
invokeOn: v__namespace.optional(picklist(["stageEnter", "manual"])),
|
|
376
|
-
/**
|
|
377
|
-
* Auto-completion predicate — same shape as `transition.filter`. When
|
|
378
|
-
* truthy at activation or on any subsequent cascade, the engine
|
|
379
|
-
* flips the task to `done` with a `{ kind: "system", id: "engine.completeWhen" }`
|
|
380
|
-
* actor. A full filter, not just a wall-clock deadline.
|
|
381
|
-
*/
|
|
382
|
-
completeWhen: v__namespace.optional(FilterSchema),
|
|
383
|
-
/**
|
|
384
|
-
* Auto-failure predicate — symmetric to `completeWhen`. Same shape,
|
|
385
|
-
* same evaluation cadence (activation + every cascade), but flips
|
|
386
|
-
* the task to `failed` (not `done`) with a `{ kind: "system",
|
|
387
|
-
* id: "engine.failWhen" }` actor. When both `completeWhen` and
|
|
388
|
-
* `failWhen` resolve truthy on the same evaluation, `failWhen`
|
|
389
|
-
* wins — failure is the more notable signal and should not be
|
|
390
|
-
* silently swallowed.
|
|
391
|
-
*/
|
|
392
|
-
failWhen: v__namespace.optional(FilterSchema),
|
|
393
|
-
effects: v__namespace.optional(v__namespace.array(EffectSchema)),
|
|
394
|
-
actions: v__namespace.optional(v__namespace.array(ActionSchema)),
|
|
395
|
-
spawns: v__namespace.optional(SpawnSchema),
|
|
396
|
-
contentRefs: v__namespace.optional(v__namespace.array(v__namespace.unknown())),
|
|
397
|
-
/** Task-scoped state slots. Resolved at task activation time. */
|
|
398
|
-
state: v__namespace.optional(v__namespace.array(StateSlotSchema))
|
|
399
|
-
}), TransitionSchema = v__namespace.strictObject({
|
|
400
|
-
type: v__namespace.optional(v__namespace.literal("workflow.transition")),
|
|
401
|
-
id: NonEmpty,
|
|
402
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
403
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
404
|
-
to: NonEmpty,
|
|
405
|
-
on: v__namespace.optional(v__namespace.string()),
|
|
406
|
-
filter: v__namespace.optional(FilterSchema),
|
|
407
|
-
effects: v__namespace.optional(v__namespace.array(EffectSchema))
|
|
408
|
-
}), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardMatchSchema = v__namespace.strictObject({
|
|
409
|
-
/** Subject `_type`(s); empty matches any type. */
|
|
410
|
-
types: v__namespace.optional(v__namespace.array(NonEmpty)),
|
|
411
|
-
/** Target docs as `Source`s resolved at deploy to bare ids + the resource. */
|
|
412
|
-
idRefs: v__namespace.optional(v__namespace.array(SourceSchema)),
|
|
413
|
-
/** Glob id patterns (bare, resource-local). */
|
|
414
|
-
idPatterns: v__namespace.optional(v__namespace.array(NonEmpty)),
|
|
415
|
-
actions: v__namespace.pipe(
|
|
416
|
-
v__namespace.array(GuardActionSchema),
|
|
417
|
-
v__namespace.minLength(1, "a guard must match at least one action")
|
|
418
|
-
)
|
|
419
|
-
}), GuardSchema = v__namespace.strictObject({
|
|
420
|
-
name: v__namespace.optional(v__namespace.string()),
|
|
421
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
422
|
-
match: GuardMatchSchema,
|
|
423
|
-
/**
|
|
424
|
-
* Lake GROQ predicate. Reads `document.before`/`document.after`, `mutation`,
|
|
425
|
-
* `guard`, and `identity()`. Bare ids/fields only — no GDRs. Omitted or
|
|
426
|
-
* empty means UNCONDITIONAL DENY.
|
|
427
|
-
*/
|
|
428
|
-
predicate: v__namespace.optional(v__namespace.string()),
|
|
429
|
-
/**
|
|
430
|
-
* Projected workflow state the predicate reads as `guard.metadata.*`. Values
|
|
431
|
-
* are `Source`s resolved at deploy and re-synced by the engine. Copied data,
|
|
432
|
-
* never a cross-resource pointer.
|
|
433
|
-
*/
|
|
434
|
-
metadata: v__namespace.optional(v__namespace.record(NonEmpty, SourceSchema))
|
|
435
|
-
}), StageKindSchema = picklist(["initial", "intermediate", "terminal", "waiting"]), StageSchema = v__namespace.strictObject({
|
|
436
|
-
type: v__namespace.optional(v__namespace.literal("workflow.stage")),
|
|
437
|
-
id: NonEmpty,
|
|
438
|
-
title: v__namespace.optional(v__namespace.string()),
|
|
439
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
440
|
-
kind: v__namespace.optional(StageKindSchema),
|
|
441
|
-
tasks: v__namespace.optional(v__namespace.array(TaskSchema)),
|
|
442
|
-
transitions: v__namespace.optional(v__namespace.array(TransitionSchema)),
|
|
443
|
-
completion: v__namespace.optional(FilterSchema),
|
|
444
|
-
onEnter: v__namespace.optional(v__namespace.array(EffectSchema)),
|
|
445
|
-
onExit: v__namespace.optional(v__namespace.array(EffectSchema)),
|
|
446
|
-
/**
|
|
447
|
-
* Lake mutation guards active while this stage holds. Each compiles to a
|
|
448
|
-
* `temp.system.guard` doc deployed on stage entry and retracted on exit.
|
|
449
|
-
* Plural because one stage's intent may span resources (one guard each).
|
|
450
|
-
*/
|
|
451
|
-
guards: v__namespace.optional(v__namespace.array(GuardSchema)),
|
|
452
|
-
contentRefs: v__namespace.optional(v__namespace.array(v__namespace.unknown())),
|
|
453
|
-
/** Stage-scoped state slots. Resolved at stage entry and persisted on
|
|
454
|
-
* the StageEntry. */
|
|
455
|
-
state: v__namespace.optional(v__namespace.array(StateSlotSchema))
|
|
456
|
-
}), WorkflowDefinitionSchema = v__namespace.strictObject({
|
|
457
|
-
workflowId: NonEmpty,
|
|
458
|
-
version: PositiveInt,
|
|
459
|
-
title: NonEmpty,
|
|
460
|
-
description: v__namespace.optional(v__namespace.string()),
|
|
461
|
-
initialStageId: NonEmpty,
|
|
462
|
-
/**
|
|
463
|
-
* Workflow-level state slots. Persist for the instance lifetime.
|
|
464
|
-
* Every stage's filters/ops can read them via
|
|
465
|
-
* `*[_id == $self][0].state[key == "<key>"][0].value`. Init-sourced
|
|
466
|
-
* slots are supplied at `startInstance` via `initialState`.
|
|
467
|
-
*/
|
|
468
|
-
state: v__namespace.optional(v__namespace.array(StateSlotSchema)),
|
|
469
|
-
stages: v__namespace.pipe(v__namespace.array(StageSchema), v__namespace.minLength(1, "must declare at least one stage")),
|
|
470
|
-
predicates: v__namespace.optional(v__namespace.array(PredicateSchema))
|
|
471
|
-
});
|
|
472
|
-
function formatValidationError(label, issues) {
|
|
473
|
-
const lines = issues.map((issue) => ` - ${issue.path.length === 0 ? "(root)" : formatPath(issue.path)}: ${issue.message}`);
|
|
474
|
-
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):
|
|
475
|
-
${lines.join(`
|
|
476
|
-
`)}`;
|
|
477
|
-
}
|
|
478
|
-
function issuesFromValibot(issues) {
|
|
479
|
-
return issues.map((issue) => ({
|
|
480
|
-
path: issue.path ? issue.path.map((item) => item.key) : [],
|
|
481
|
-
message: issue.message
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
function formatPath(path) {
|
|
485
|
-
let out = "";
|
|
486
|
-
for (const seg of path)
|
|
487
|
-
typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
|
|
488
|
-
return out;
|
|
468
|
+
for (const [key, groq2] of Object.entries(record ?? {}))
|
|
469
|
+
sites.push({
|
|
470
|
+
groq: groq2,
|
|
471
|
+
path: [...subPath, group, key],
|
|
472
|
+
label: `task "${task.name}".subworkflows.${group} "${key}"`
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
function collectBindingSites(effects, path, label, sites) {
|
|
476
|
+
for (const [e, effect] of (effects ?? []).entries())
|
|
477
|
+
for (const [key, groq2] of Object.entries(effect.bindings ?? {}))
|
|
478
|
+
sites.push({
|
|
479
|
+
groq: groq2,
|
|
480
|
+
path: [...path, e, "bindings", key],
|
|
481
|
+
label: `${label} effect "${effect.name}" binding "${key}"`
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function checkAssigneesEntries(def, issues) {
|
|
485
|
+
for (const { entries, path } of stateScopes(def))
|
|
486
|
+
(entries ?? []).filter((e) => e.type === "assignees").length <= 1 || issues.push({
|
|
487
|
+
path,
|
|
488
|
+
message: "at most one assignees-kind state entry per scope \u2014 the inbox reads it by kind"
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function checkWorkflowInvariants(def) {
|
|
492
|
+
const issues = [], stageNames = checkStages(def, issues);
|
|
493
|
+
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;
|
|
489
494
|
}
|
|
490
495
|
function defineWorkflow(definition) {
|
|
491
|
-
const
|
|
492
|
-
if (issues.length > 0) throw new Error(formatValidationError(label, issues));
|
|
493
|
-
return
|
|
496
|
+
const label = labelFor("defineWorkflow", definition), parsed = parseOrThrow(schema.AuthoringWorkflowSchema, definition, label), { definition: stored, issues: desugarIssues } = desugarWorkflow(parsed), issues = [...desugarIssues, ...checkWorkflowInvariants(stored)];
|
|
497
|
+
if (issues.length > 0) throw new Error(schema.formatValidationError(label, issues));
|
|
498
|
+
return stored;
|
|
494
499
|
}
|
|
495
500
|
function defineStage(stage) {
|
|
496
|
-
return parseOrThrow(
|
|
501
|
+
return parseOrThrow(schema.AuthoringStageSchema, stage, labelFor("defineStage", stage));
|
|
497
502
|
}
|
|
498
503
|
function defineTask(task) {
|
|
499
|
-
return parseOrThrow(
|
|
504
|
+
return parseOrThrow(schema.AuthoringTaskSchema, task, labelFor("defineTask", task));
|
|
500
505
|
}
|
|
501
506
|
function defineAction(action) {
|
|
502
|
-
return parseOrThrow(
|
|
507
|
+
return parseOrThrow(schema.AuthoringActionSchema, action, labelFor("defineAction", action));
|
|
503
508
|
}
|
|
504
509
|
function defineTransition(transition) {
|
|
505
|
-
return parseOrThrow(
|
|
506
|
-
TransitionSchema,
|
|
507
|
-
transition,
|
|
508
|
-
typeof transition.id == "string" ? `defineTransition("${transition.id}")` : typeof transition.to == "string" ? `defineTransition(\u2192 "${transition.to}")` : "defineTransition"
|
|
509
|
-
);
|
|
510
|
+
return parseOrThrow(schema.AuthoringTransitionSchema, transition, transitionLabel(transition));
|
|
510
511
|
}
|
|
511
|
-
function
|
|
512
|
-
return parseOrThrow(
|
|
512
|
+
function defineState(entry) {
|
|
513
|
+
return parseOrThrow(schema.AuthoringStateEntrySchema, entry, labelFor("defineState", entry));
|
|
513
514
|
}
|
|
514
|
-
function
|
|
515
|
-
return parseOrThrow(
|
|
515
|
+
function defineOp(op) {
|
|
516
|
+
return parseOrThrow(schema.AuthoringOpSchema, op, "defineOp");
|
|
516
517
|
}
|
|
517
|
-
function
|
|
518
|
-
return parseOrThrow(
|
|
518
|
+
function defineGuard(guard) {
|
|
519
|
+
return parseOrThrow(schema.GuardSchema, guard, labelFor("defineGuard", guard));
|
|
519
520
|
}
|
|
520
521
|
function defineEffect(effect) {
|
|
521
|
-
return parseOrThrow(EffectSchema, effect, labelFor("defineEffect", effect
|
|
522
|
+
return parseOrThrow(schema.EffectSchema, effect, labelFor("defineEffect", effect));
|
|
523
|
+
}
|
|
524
|
+
function transitionLabel(transition) {
|
|
525
|
+
const { name, to } = transition;
|
|
526
|
+
return typeof name == "string" ? `defineTransition("${name}")` : typeof to == "string" ? `defineTransition(\u2192 "${to}")` : "defineTransition";
|
|
522
527
|
}
|
|
523
528
|
function defineEffectDescriptor(descriptor) {
|
|
524
529
|
if (!descriptor.name) throw new Error("EffectDescriptor missing name");
|
|
525
530
|
return descriptor;
|
|
526
531
|
}
|
|
527
|
-
function parseOrThrow(schema, input, label) {
|
|
528
|
-
const result = v__namespace.safeParse(schema, input);
|
|
532
|
+
function parseOrThrow(schema$1, input, label) {
|
|
533
|
+
const result = v__namespace.safeParse(schema$1, input);
|
|
529
534
|
if (!result.success)
|
|
530
|
-
throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
|
|
535
|
+
throw new Error(schema.formatValidationError(label, schema.issuesFromValibot(result.issues)));
|
|
531
536
|
return result.output;
|
|
532
537
|
}
|
|
533
|
-
function labelFor(fn, value
|
|
534
|
-
if (value && typeof value == "object" &&
|
|
535
|
-
const raw = value
|
|
538
|
+
function labelFor(fn, value) {
|
|
539
|
+
if (value && typeof value == "object" && "name" in value) {
|
|
540
|
+
const raw = value.name;
|
|
536
541
|
if (typeof raw == "string") return `${fn}("${raw}")`;
|
|
537
542
|
}
|
|
538
543
|
return fn;
|
|
539
544
|
}
|
|
540
545
|
exports.defineAction = defineAction;
|
|
541
|
-
exports.defineAssignee = defineAssignee;
|
|
542
546
|
exports.defineEffect = defineEffect;
|
|
543
547
|
exports.defineEffectDescriptor = defineEffectDescriptor;
|
|
544
|
-
exports.
|
|
545
|
-
exports.
|
|
548
|
+
exports.defineGuard = defineGuard;
|
|
549
|
+
exports.defineOp = defineOp;
|
|
546
550
|
exports.defineStage = defineStage;
|
|
551
|
+
exports.defineState = defineState;
|
|
547
552
|
exports.defineTask = defineTask;
|
|
548
553
|
exports.defineTransition = defineTransition;
|
|
549
554
|
exports.defineWorkflow = defineWorkflow;
|
|
555
|
+
exports.groq = groq;
|
|
550
556
|
//# sourceMappingURL=define.cjs.map
|