@duckflux/core 0.6.8
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/cel/index.js +12010 -0
- package/dist/engine/index.js +27937 -0
- package/dist/eventhub/index.js +151 -0
- package/dist/index.js +28044 -0
- package/dist/parser/index.js +26765 -0
- package/package.json +48 -0
- package/src/cel/index.ts +156 -0
- package/src/engine/control.ts +169 -0
- package/src/engine/engine.ts +127 -0
- package/src/engine/errors.ts +90 -0
- package/src/engine/index.ts +8 -0
- package/src/engine/output.ts +109 -0
- package/src/engine/sequential.ts +379 -0
- package/src/engine/state.ts +185 -0
- package/src/engine/timeout.ts +43 -0
- package/src/engine/wait.ts +102 -0
- package/src/eventhub/index.ts +24 -0
- package/src/eventhub/memory.ts +106 -0
- package/src/eventhub/types.ts +17 -0
- package/src/index.ts +51 -0
- package/src/model/index.ts +183 -0
- package/src/parser/index.ts +4 -0
- package/src/parser/parser.ts +13 -0
- package/src/parser/schema/duckflux.schema.json +573 -0
- package/src/parser/schema.ts +26 -0
- package/src/parser/validate.ts +541 -0
- package/src/parser/validate_inputs.ts +187 -0
- package/src/participant/emit.ts +63 -0
- package/src/participant/exec.ts +158 -0
- package/src/participant/http.ts +45 -0
- package/src/participant/index.ts +61 -0
- package/src/participant/mcp.ts +8 -0
- package/src/participant/workflow.ts +73 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { validateCelExpression } from "../cel/index";
|
|
5
|
+
import type { ValidationError, ValidationResult, Workflow } from "../model/index";
|
|
6
|
+
|
|
7
|
+
const RESERVED_NAMES = new Set(["workflow", "execution", "input", "output", "env", "loop", "event"]);
|
|
8
|
+
const BUILTIN_ONERROR = new Set(["fail", "skip", "retry"]);
|
|
9
|
+
|
|
10
|
+
function isInlineParticipant(step: unknown): step is Record<string, unknown> & { type: string } {
|
|
11
|
+
return typeof step === "object" && step !== null && "type" in step;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isWaitStep(step: unknown): step is { wait: Record<string, unknown> } {
|
|
15
|
+
return typeof step === "object" && step !== null && "wait" in step && Object.keys(step).length === 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectParticipantReferences(
|
|
19
|
+
flow: unknown[],
|
|
20
|
+
refs: Array<{ name: string; path: string }>,
|
|
21
|
+
inlineNames: Array<{ name: string; path: string }>,
|
|
22
|
+
basePath = "flow",
|
|
23
|
+
): void {
|
|
24
|
+
for (const [index, step] of flow.entries()) {
|
|
25
|
+
const stepPath = `${basePath}[${index}]`;
|
|
26
|
+
|
|
27
|
+
if (typeof step === "string") {
|
|
28
|
+
refs.push({ name: step, path: stepPath });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!step || typeof step !== "object") {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Inline participant
|
|
37
|
+
if (isInlineParticipant(step)) {
|
|
38
|
+
if (step.as && typeof step.as === "string") {
|
|
39
|
+
inlineNames.push({ name: step.as, path: stepPath });
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Wait step
|
|
45
|
+
if (isWaitStep(step)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const obj = step as Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
if (obj.parallel) {
|
|
52
|
+
collectParticipantReferences(obj.parallel as unknown[], refs, inlineNames, `${stepPath}.parallel`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Set step — skip, not a participant reference
|
|
57
|
+
if ("set" in obj && Object.keys(obj).length === 1) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (obj.loop) {
|
|
62
|
+
const loopDef = obj.loop as Record<string, unknown>;
|
|
63
|
+
collectParticipantReferences(
|
|
64
|
+
(loopDef.steps ?? []) as unknown[],
|
|
65
|
+
refs,
|
|
66
|
+
inlineNames,
|
|
67
|
+
`${stepPath}.loop.steps`,
|
|
68
|
+
);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (obj.if) {
|
|
73
|
+
const ifDef = obj.if as Record<string, unknown>;
|
|
74
|
+
collectParticipantReferences(
|
|
75
|
+
(ifDef.then ?? []) as unknown[],
|
|
76
|
+
refs,
|
|
77
|
+
inlineNames,
|
|
78
|
+
`${stepPath}.if.then`,
|
|
79
|
+
);
|
|
80
|
+
if (ifDef.else) {
|
|
81
|
+
collectParticipantReferences(
|
|
82
|
+
ifDef.else as unknown[],
|
|
83
|
+
refs,
|
|
84
|
+
inlineNames,
|
|
85
|
+
`${stepPath}.if.else`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Participant override
|
|
92
|
+
const keys = Object.keys(obj);
|
|
93
|
+
if (keys.length === 1) {
|
|
94
|
+
refs.push({ name: keys[0], path: `${stepPath}.${keys[0]}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateCel(expr: string, errors: ValidationError[], path: string): void {
|
|
100
|
+
const validation = validateCelExpression(expr);
|
|
101
|
+
if (!validation.valid) {
|
|
102
|
+
errors.push({ path, message: validation.error ?? "invalid CEL expression" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateFlowCel(flow: unknown[], errors: ValidationError[], basePath = "flow"): void {
|
|
107
|
+
for (const [index, step] of flow.entries()) {
|
|
108
|
+
const stepPath = `${basePath}[${index}]`;
|
|
109
|
+
|
|
110
|
+
if (!step || typeof step !== "object") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const obj = step as Record<string, unknown>;
|
|
115
|
+
|
|
116
|
+
// Wait step CEL
|
|
117
|
+
if (isWaitStep(step)) {
|
|
118
|
+
const waitDef = obj.wait as Record<string, unknown>;
|
|
119
|
+
if (waitDef.match && typeof waitDef.match === "string") {
|
|
120
|
+
validateCel(waitDef.match, errors, `${stepPath}.wait.match`);
|
|
121
|
+
}
|
|
122
|
+
if (waitDef.until && typeof waitDef.until === "string") {
|
|
123
|
+
validateCel(waitDef.until, errors, `${stepPath}.wait.until`);
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Inline participant CEL
|
|
129
|
+
if (isInlineParticipant(step)) {
|
|
130
|
+
if (obj.when && typeof obj.when === "string") {
|
|
131
|
+
validateCel(obj.when, errors, `${stepPath}.when`);
|
|
132
|
+
}
|
|
133
|
+
if (typeof obj.input === "string") {
|
|
134
|
+
validateCel(obj.input, errors, `${stepPath}.input`);
|
|
135
|
+
}
|
|
136
|
+
if (obj.input && typeof obj.input === "object") {
|
|
137
|
+
for (const [key, value] of Object.entries(obj.input as Record<string, unknown>)) {
|
|
138
|
+
if (typeof value === "string") {
|
|
139
|
+
validateCel(value, errors, `${stepPath}.input.${key}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Emit payload CEL
|
|
144
|
+
if (obj.type === "emit" && obj.payload && typeof obj.payload === "string") {
|
|
145
|
+
validateCel(obj.payload, errors, `${stepPath}.payload`);
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Set step CEL
|
|
151
|
+
if ("set" in obj && Object.keys(obj).length === 1) {
|
|
152
|
+
const setDef = obj.set as Record<string, string>;
|
|
153
|
+
for (const [key, expr] of Object.entries(setDef)) {
|
|
154
|
+
if (RESERVED_NAMES.has(key)) {
|
|
155
|
+
errors.push({
|
|
156
|
+
path: `${stepPath}.set.${key}`,
|
|
157
|
+
message: `set key '${key}' uses a reserved name`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (typeof expr === "string") {
|
|
161
|
+
validateCel(expr, errors, `${stepPath}.set.${key}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (obj.loop) {
|
|
168
|
+
const loopDef = obj.loop as Record<string, unknown>;
|
|
169
|
+
if (loopDef.until && typeof loopDef.until === "string") {
|
|
170
|
+
validateCel(loopDef.until, errors, `${stepPath}.loop.until`);
|
|
171
|
+
}
|
|
172
|
+
validateFlowCel(
|
|
173
|
+
(loopDef.steps ?? []) as unknown[],
|
|
174
|
+
errors,
|
|
175
|
+
`${stepPath}.loop.steps`,
|
|
176
|
+
);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (obj.parallel) {
|
|
181
|
+
validateFlowCel(obj.parallel as unknown[], errors, `${stepPath}.parallel`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (obj.if) {
|
|
186
|
+
const ifDef = obj.if as Record<string, unknown>;
|
|
187
|
+
if (ifDef.condition && typeof ifDef.condition === "string") {
|
|
188
|
+
validateCel(ifDef.condition, errors, `${stepPath}.if.condition`);
|
|
189
|
+
}
|
|
190
|
+
validateFlowCel((ifDef.then ?? []) as unknown[], errors, `${stepPath}.if.then`);
|
|
191
|
+
if (ifDef.else) {
|
|
192
|
+
validateFlowCel(ifDef.else as unknown[], errors, `${stepPath}.if.else`);
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Participant override
|
|
198
|
+
const keys = Object.keys(obj);
|
|
199
|
+
if (keys.length !== 1) continue;
|
|
200
|
+
|
|
201
|
+
const participantName = keys[0];
|
|
202
|
+
const override = obj[participantName];
|
|
203
|
+
if (!override || typeof override !== "object") continue;
|
|
204
|
+
|
|
205
|
+
const ov = override as Record<string, unknown>;
|
|
206
|
+
if (ov.when && typeof ov.when === "string") {
|
|
207
|
+
validateCel(ov.when, errors, `${stepPath}.${participantName}.when`);
|
|
208
|
+
}
|
|
209
|
+
if (typeof ov.input === "string") {
|
|
210
|
+
validateCel(ov.input, errors, `${stepPath}.${participantName}.input`);
|
|
211
|
+
}
|
|
212
|
+
if (ov.input && typeof ov.input === "object") {
|
|
213
|
+
for (const [key, value] of Object.entries(ov.input as Record<string, unknown>)) {
|
|
214
|
+
if (typeof value === "string") {
|
|
215
|
+
validateCel(value, errors, `${stepPath}.${participantName}.input.${key}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateLoopConstraints(flow: unknown[], errors: ValidationError[], basePath = "flow"): void {
|
|
223
|
+
for (const [index, step] of flow.entries()) {
|
|
224
|
+
const stepPath = `${basePath}[${index}]`;
|
|
225
|
+
|
|
226
|
+
if (!step || typeof step !== "object") continue;
|
|
227
|
+
|
|
228
|
+
const obj = step as Record<string, unknown>;
|
|
229
|
+
|
|
230
|
+
if (obj.loop) {
|
|
231
|
+
const loopDef = obj.loop as Record<string, unknown>;
|
|
232
|
+
if (loopDef.until == null && loopDef.max == null) {
|
|
233
|
+
errors.push({
|
|
234
|
+
path: `${stepPath}.loop`,
|
|
235
|
+
message: "loop must define at least one of 'until' or 'max'",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Validate loop.as doesn't conflict with reserved names
|
|
239
|
+
if (loopDef.as && typeof loopDef.as === "string" && RESERVED_NAMES.has(loopDef.as)) {
|
|
240
|
+
errors.push({
|
|
241
|
+
path: `${stepPath}.loop.as`,
|
|
242
|
+
message: `loop.as '${loopDef.as}' conflicts with reserved name`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
validateLoopConstraints(
|
|
246
|
+
(loopDef.steps ?? []) as unknown[],
|
|
247
|
+
errors,
|
|
248
|
+
`${stepPath}.loop.steps`,
|
|
249
|
+
);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (obj.parallel) {
|
|
254
|
+
validateLoopConstraints(obj.parallel as unknown[], errors, `${stepPath}.parallel`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (obj.if) {
|
|
259
|
+
const ifDef = obj.if as Record<string, unknown>;
|
|
260
|
+
validateLoopConstraints((ifDef.then ?? []) as unknown[], errors, `${stepPath}.if.then`);
|
|
261
|
+
if (ifDef.else) {
|
|
262
|
+
validateLoopConstraints(ifDef.else as unknown[], errors, `${stepPath}.if.else`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function validateWaitSteps(flow: unknown[], errors: ValidationError[], basePath = "flow"): void {
|
|
269
|
+
for (const [index, step] of flow.entries()) {
|
|
270
|
+
const stepPath = `${basePath}[${index}]`;
|
|
271
|
+
|
|
272
|
+
if (!step || typeof step !== "object") continue;
|
|
273
|
+
|
|
274
|
+
if (isWaitStep(step)) {
|
|
275
|
+
const waitDef = (step as { wait: Record<string, unknown> }).wait;
|
|
276
|
+
|
|
277
|
+
// Validate wait modes
|
|
278
|
+
const hasEvent = !!waitDef.event;
|
|
279
|
+
const hasUntil = !!waitDef.until;
|
|
280
|
+
|
|
281
|
+
if (hasEvent && hasUntil) {
|
|
282
|
+
errors.push({
|
|
283
|
+
path: `${stepPath}.wait`,
|
|
284
|
+
message: "wait step cannot have both 'event' and 'until'",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Event mode: event required
|
|
289
|
+
if (waitDef.match && !hasEvent) {
|
|
290
|
+
errors.push({
|
|
291
|
+
path: `${stepPath}.wait.match`,
|
|
292
|
+
message: "wait.match requires wait.event",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Polling mode: until required for poll
|
|
297
|
+
if (waitDef.poll && !hasUntil) {
|
|
298
|
+
errors.push({
|
|
299
|
+
path: `${stepPath}.wait.poll`,
|
|
300
|
+
message: "wait.poll requires wait.until",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// onTimeout validation
|
|
305
|
+
if (waitDef.onTimeout && typeof waitDef.onTimeout === "string") {
|
|
306
|
+
if (waitDef.onTimeout !== "fail" && waitDef.onTimeout !== "skip") {
|
|
307
|
+
// Could be a participant name - validated elsewhere
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const obj = step as Record<string, unknown>;
|
|
314
|
+
if (obj.loop) {
|
|
315
|
+
validateWaitSteps(
|
|
316
|
+
((obj.loop as Record<string, unknown>).steps ?? []) as unknown[],
|
|
317
|
+
errors,
|
|
318
|
+
`${stepPath}.loop.steps`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (obj.parallel) {
|
|
322
|
+
validateWaitSteps(obj.parallel as unknown[], errors, `${stepPath}.parallel`);
|
|
323
|
+
}
|
|
324
|
+
if (obj.if) {
|
|
325
|
+
const ifDef = obj.if as Record<string, unknown>;
|
|
326
|
+
validateWaitSteps((ifDef.then ?? []) as unknown[], errors, `${stepPath}.if.then`);
|
|
327
|
+
if (ifDef.else) {
|
|
328
|
+
validateWaitSteps(ifDef.else as unknown[], errors, `${stepPath}.if.else`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function validateInlineParticipantFields(
|
|
335
|
+
flow: unknown[],
|
|
336
|
+
errors: ValidationError[],
|
|
337
|
+
basePath = "flow",
|
|
338
|
+
): void {
|
|
339
|
+
for (const [index, step] of flow.entries()) {
|
|
340
|
+
const stepPath = `${basePath}[${index}]`;
|
|
341
|
+
|
|
342
|
+
if (!step || typeof step !== "object") continue;
|
|
343
|
+
|
|
344
|
+
if (isInlineParticipant(step)) {
|
|
345
|
+
validateSingleParticipantRequiredFields(
|
|
346
|
+
step as { type: string; [key: string]: unknown },
|
|
347
|
+
stepPath,
|
|
348
|
+
errors,
|
|
349
|
+
);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const obj = step as Record<string, unknown>;
|
|
354
|
+
if (obj.loop) {
|
|
355
|
+
const loopDef = obj.loop as Record<string, unknown>;
|
|
356
|
+
validateInlineParticipantFields(
|
|
357
|
+
(loopDef.steps ?? []) as unknown[],
|
|
358
|
+
errors,
|
|
359
|
+
`${stepPath}.loop.steps`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (obj.parallel) {
|
|
363
|
+
validateInlineParticipantFields(obj.parallel as unknown[], errors, `${stepPath}.parallel`);
|
|
364
|
+
}
|
|
365
|
+
if (obj.if) {
|
|
366
|
+
const ifDef = obj.if as Record<string, unknown>;
|
|
367
|
+
validateInlineParticipantFields((ifDef.then ?? []) as unknown[], errors, `${stepPath}.if.then`);
|
|
368
|
+
if (ifDef.else) {
|
|
369
|
+
validateInlineParticipantFields(ifDef.else as unknown[], errors, `${stepPath}.if.else`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function validateParticipantRequiredFields(
|
|
376
|
+
participants: Record<string, { type: string; [key: string]: unknown }>,
|
|
377
|
+
errors: ValidationError[],
|
|
378
|
+
): void {
|
|
379
|
+
for (const [name, p] of Object.entries(participants)) {
|
|
380
|
+
validateSingleParticipantRequiredFields(p, `participants.${name}`, errors);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validateSingleParticipantRequiredFields(
|
|
385
|
+
p: { type: string; [key: string]: unknown },
|
|
386
|
+
path: string,
|
|
387
|
+
errors: ValidationError[],
|
|
388
|
+
): void {
|
|
389
|
+
switch (p.type) {
|
|
390
|
+
case "exec":
|
|
391
|
+
if (!p.run) {
|
|
392
|
+
errors.push({ path: `${path}.run`, message: "exec participant requires 'run' field" });
|
|
393
|
+
}
|
|
394
|
+
break;
|
|
395
|
+
case "http":
|
|
396
|
+
if (!p.url) {
|
|
397
|
+
errors.push({ path: `${path}.url`, message: "http participant requires 'url' field" });
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
case "workflow":
|
|
401
|
+
if (!p.path) {
|
|
402
|
+
errors.push({ path: `${path}.path`, message: "workflow participant requires 'path' field" });
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
case "emit":
|
|
406
|
+
if (!p.event) {
|
|
407
|
+
errors.push({ path: `${path}.event`, message: "emit participant requires 'event' field" });
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function validateEmitParticipants(
|
|
414
|
+
participants: Record<string, { type: string; event?: string }>,
|
|
415
|
+
errors: ValidationError[],
|
|
416
|
+
): void {
|
|
417
|
+
for (const [name, p] of Object.entries(participants)) {
|
|
418
|
+
if (p.type === "emit" && !p.event) {
|
|
419
|
+
errors.push({
|
|
420
|
+
path: `participants.${name}.event`,
|
|
421
|
+
message: "emit participant requires 'event' field",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function validateSemantic(workflow: Workflow, basePath: string): Promise<ValidationResult> {
|
|
428
|
+
const errors: ValidationError[] = [];
|
|
429
|
+
const participants = workflow.participants ?? {};
|
|
430
|
+
const participantNames = new Set(Object.keys(participants));
|
|
431
|
+
|
|
432
|
+
// Validate reserved names
|
|
433
|
+
for (const name of participantNames) {
|
|
434
|
+
if (RESERVED_NAMES.has(name)) {
|
|
435
|
+
errors.push({ path: `participants.${name}`, message: "participant name is reserved" });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate flow is non-empty
|
|
440
|
+
if (!workflow.flow || workflow.flow.length === 0) {
|
|
441
|
+
errors.push({ path: "flow", message: "flow must contain at least one step" });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Collect references and inline names
|
|
445
|
+
const refs: Array<{ name: string; path: string }> = [];
|
|
446
|
+
const inlineNames: Array<{ name: string; path: string }> = [];
|
|
447
|
+
collectParticipantReferences(workflow.flow ?? [], refs, inlineNames);
|
|
448
|
+
|
|
449
|
+
// Validate participant references exist
|
|
450
|
+
for (const ref of refs) {
|
|
451
|
+
if (!participantNames.has(ref.name)) {
|
|
452
|
+
errors.push({ path: ref.path, message: `participant '${ref.name}' does not exist` });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Validate inline `as` uniqueness
|
|
457
|
+
const seenInlineNames = new Set<string>();
|
|
458
|
+
for (const inline of inlineNames) {
|
|
459
|
+
if (participantNames.has(inline.name)) {
|
|
460
|
+
errors.push({
|
|
461
|
+
path: inline.path,
|
|
462
|
+
message: `inline participant 'as: ${inline.name}' conflicts with top-level participant`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (RESERVED_NAMES.has(inline.name)) {
|
|
466
|
+
errors.push({
|
|
467
|
+
path: inline.path,
|
|
468
|
+
message: `inline participant 'as: ${inline.name}' uses a reserved name`,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (seenInlineNames.has(inline.name)) {
|
|
472
|
+
errors.push({
|
|
473
|
+
path: inline.path,
|
|
474
|
+
message: `inline participant 'as: ${inline.name}' is not unique`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
seenInlineNames.add(inline.name);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Validate inline participants cannot be reused as string references (§3.1)
|
|
481
|
+
for (const ref of refs) {
|
|
482
|
+
if (seenInlineNames.has(ref.name)) {
|
|
483
|
+
errors.push({
|
|
484
|
+
path: ref.path,
|
|
485
|
+
message: `participant reference '${ref.name}' refers to an inline participant, which cannot be reused`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Validate onError references
|
|
491
|
+
const defaultsOnError = workflow.defaults?.onError;
|
|
492
|
+
if (defaultsOnError && !BUILTIN_ONERROR.has(defaultsOnError) && !participantNames.has(defaultsOnError)) {
|
|
493
|
+
errors.push({
|
|
494
|
+
path: "defaults.onError",
|
|
495
|
+
message: `onError fallback '${defaultsOnError}' does not reference an existing participant`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
for (const [name, participant] of Object.entries(participants)) {
|
|
500
|
+
if (participant.onError && !BUILTIN_ONERROR.has(participant.onError) && !participantNames.has(participant.onError)) {
|
|
501
|
+
errors.push({
|
|
502
|
+
path: `participants.${name}.onError`,
|
|
503
|
+
message: `onError fallback '${participant.onError}' does not reference an existing participant`,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Validate type-specific required fields (top-level participants)
|
|
509
|
+
validateParticipantRequiredFields(participants, errors);
|
|
510
|
+
|
|
511
|
+
// Validate inline participant required fields in flow
|
|
512
|
+
validateInlineParticipantFields(workflow.flow ?? [], errors);
|
|
513
|
+
|
|
514
|
+
// Validate emit participants
|
|
515
|
+
validateEmitParticipants(participants as Record<string, { type: string; event?: string }>, errors);
|
|
516
|
+
|
|
517
|
+
// Validate loop, wait, CEL
|
|
518
|
+
validateLoopConstraints(workflow.flow ?? [], errors);
|
|
519
|
+
validateWaitSteps(workflow.flow ?? [], errors);
|
|
520
|
+
validateFlowCel(workflow.flow ?? [], errors);
|
|
521
|
+
|
|
522
|
+
// Validate sub-workflow paths
|
|
523
|
+
for (const [name, participant] of Object.entries(participants)) {
|
|
524
|
+
if (participant.type !== "workflow") continue;
|
|
525
|
+
|
|
526
|
+
const resolvedPath = resolve(basePath, (participant as { path: string }).path);
|
|
527
|
+
const exists = await access(resolvedPath, constants.F_OK).then(
|
|
528
|
+
() => true,
|
|
529
|
+
() => false,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
if (!exists) {
|
|
533
|
+
errors.push({
|
|
534
|
+
path: `participants.${name}.path`,
|
|
535
|
+
message: `sub-workflow path does not exist: ${resolvedPath}`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { valid: errors.length === 0, errors };
|
|
541
|
+
}
|