@ema.co/mcp-toolkit 2026.2.13 → 2026.2.19
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.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/.context/public/guides/ema-user-guide.md +12 -16
- package/.context/public/guides/mcp-tools-guide.md +203 -334
- package/dist/mcp/domain/loop-detection.js +97 -0
- package/dist/mcp/domain/structural-rules.js +4 -5
- package/dist/mcp/domain/validation-rules.js +5 -5
- package/dist/mcp/domain/workflow-graph.js +3 -5
- package/dist/mcp/guidance.js +9 -29
- package/dist/mcp/handlers/feedback/index.js +1 -1
- package/dist/mcp/handlers/persona/index.js +237 -8
- package/dist/mcp/handlers/persona/schema.js +27 -0
- package/dist/mcp/handlers/reference/index.js +6 -4
- package/dist/mcp/handlers/workflow/index.js +15 -19
- package/dist/mcp/handlers/workflow/validation.js +1 -1
- package/dist/mcp/knowledge-types.js +7 -0
- package/dist/mcp/knowledge.js +61 -800
- package/dist/mcp/resources.js +217 -1
- package/dist/mcp/server.js +195 -2152
- package/dist/mcp/tools.js +2 -3
- package/dist/sdk/generated/agent-catalog.js +615 -0
- package/dist/sdk/generated/widget-catalog.js +60 -0
- package/docs/README.md +17 -9
- package/package.json +1 -1
- package/.context/public/guides/dashboard-operations.md +0 -349
- package/.context/public/guides/email-patterns.md +0 -125
- package/.context/public/guides/workflow-builder-patterns.md +0 -708
- package/dist/mcp/domain/intent-architect.js +0 -914
- package/dist/mcp/domain/quality-gates.js +0 -110
- package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
- package/dist/mcp/domain/workflow-intent.js +0 -1806
- package/dist/mcp/domain/workflow-merge.js +0 -449
- package/dist/mcp/domain/workflow-tracer.js +0 -648
- package/dist/mcp/domain/workflow-transformer.js +0 -742
- package/dist/mcp/handlers/persona/intent.js +0 -141
- package/dist/mcp/handlers/workflow/analyze.js +0 -119
- package/dist/mcp/handlers/workflow/compare.js +0 -70
- package/dist/mcp/handlers/workflow/generate.js +0 -384
- package/dist/mcp/handlers-consolidated.js +0 -333
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow Merge Module
|
|
3
|
-
*
|
|
4
|
-
* Provides workflow comparison, merging, and validation utilities for
|
|
5
|
-
* brownfield workflow updates. Extracted from server.ts for better
|
|
6
|
-
* testability and separation of concerns.
|
|
7
|
-
*
|
|
8
|
-
* @module sdk/workflow-merge
|
|
9
|
-
*/
|
|
10
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
-
// Internal Helpers
|
|
12
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
-
/**
|
|
14
|
-
* Extract a clean enum name from the nested structure.
|
|
15
|
-
* Handles: {name: {name: {name: "foo"}}} or {name: {name: "foo"}} or "foo"
|
|
16
|
-
*/
|
|
17
|
-
function extractEnumName(enumNameObj) {
|
|
18
|
-
if (typeof enumNameObj === "string")
|
|
19
|
-
return enumNameObj;
|
|
20
|
-
if (!enumNameObj || typeof enumNameObj !== "object")
|
|
21
|
-
return "";
|
|
22
|
-
const obj = enumNameObj;
|
|
23
|
-
if (obj.name)
|
|
24
|
-
return extractEnumName(obj.name);
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Extract node action type from the action structure.
|
|
29
|
-
* Handles: {name: {namespaces: [...], name: "type"}} or {name: {name: "type"}}
|
|
30
|
-
*/
|
|
31
|
-
function extractActionType(action) {
|
|
32
|
-
if (!action || typeof action !== "object")
|
|
33
|
-
return "";
|
|
34
|
-
const obj = action;
|
|
35
|
-
// Try action.name.name (nested structure)
|
|
36
|
-
if (obj.name && typeof obj.name === "object") {
|
|
37
|
-
const nameObj = obj.name;
|
|
38
|
-
if (typeof nameObj.name === "string")
|
|
39
|
-
return nameObj.name;
|
|
40
|
-
}
|
|
41
|
-
// Try direct name
|
|
42
|
-
if (typeof obj.name === "string")
|
|
43
|
-
return obj.name;
|
|
44
|
-
return "";
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Parse a workflow into normalized node structures.
|
|
48
|
-
* @internal Used by compareWorkflows and mergeWorkflows
|
|
49
|
-
*/
|
|
50
|
-
function parseWorkflowNodes(workflow) {
|
|
51
|
-
const nodes = new Map();
|
|
52
|
-
if (!workflow)
|
|
53
|
-
return nodes;
|
|
54
|
-
const actions = workflow.actions;
|
|
55
|
-
if (!actions)
|
|
56
|
-
return nodes;
|
|
57
|
-
for (const action of actions) {
|
|
58
|
-
const name = String(action.name ?? "");
|
|
59
|
-
if (!name)
|
|
60
|
-
continue;
|
|
61
|
-
nodes.set(name, {
|
|
62
|
-
name,
|
|
63
|
-
actionType: extractActionType(action.action ?? action.actionType),
|
|
64
|
-
inputs: action.inputs ?? {},
|
|
65
|
-
runIf: action.runIf,
|
|
66
|
-
typeArguments: action.typeArguments,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
return nodes;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Parse a workflow's enum types into normalized structures.
|
|
73
|
-
* @internal Used by compareWorkflows and mergeWorkflows
|
|
74
|
-
*/
|
|
75
|
-
function parseWorkflowEnums(workflow) {
|
|
76
|
-
const enums = new Map();
|
|
77
|
-
if (!workflow)
|
|
78
|
-
return enums;
|
|
79
|
-
const enumTypes = workflow.enumTypes;
|
|
80
|
-
if (!enumTypes)
|
|
81
|
-
return enums;
|
|
82
|
-
for (const enumType of enumTypes) {
|
|
83
|
-
const fullName = enumType.name;
|
|
84
|
-
const name = extractEnumName(fullName);
|
|
85
|
-
if (!name)
|
|
86
|
-
continue;
|
|
87
|
-
const options = enumType.options ?? [];
|
|
88
|
-
enums.set(name, {
|
|
89
|
-
name,
|
|
90
|
-
fullName,
|
|
91
|
-
options,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return enums;
|
|
95
|
-
}
|
|
96
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
-
// Public API
|
|
98
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
-
/**
|
|
100
|
-
* Compare two workflows and produce a diff.
|
|
101
|
-
*
|
|
102
|
-
* @param existing - The existing workflow to compare against
|
|
103
|
-
* @param incoming - The incoming workflow with changes
|
|
104
|
-
* @returns A diff object describing all changes between the workflows
|
|
105
|
-
*/
|
|
106
|
-
export function compareWorkflows(existing, incoming) {
|
|
107
|
-
// Guard against null/undefined inputs
|
|
108
|
-
if (!existing || typeof existing !== "object") {
|
|
109
|
-
existing = {};
|
|
110
|
-
}
|
|
111
|
-
if (!incoming || typeof incoming !== "object") {
|
|
112
|
-
incoming = {};
|
|
113
|
-
}
|
|
114
|
-
const existingNodes = parseWorkflowNodes(existing);
|
|
115
|
-
const incomingNodes = parseWorkflowNodes(incoming);
|
|
116
|
-
const existingEnums = parseWorkflowEnums(existing);
|
|
117
|
-
const incomingEnums = parseWorkflowEnums(incoming);
|
|
118
|
-
// Node comparison
|
|
119
|
-
const nodesAdded = [];
|
|
120
|
-
const nodesRemoved = [];
|
|
121
|
-
const nodesModified = [];
|
|
122
|
-
const nodesUnchanged = [];
|
|
123
|
-
const conflicts = [];
|
|
124
|
-
// Check existing nodes
|
|
125
|
-
for (const [name, existingNode] of existingNodes) {
|
|
126
|
-
const incomingNode = incomingNodes.get(name);
|
|
127
|
-
if (!incomingNode) {
|
|
128
|
-
nodesRemoved.push(name);
|
|
129
|
-
}
|
|
130
|
-
else if (existingNode.actionType !== incomingNode.actionType) {
|
|
131
|
-
// Type mismatch - this is a conflict
|
|
132
|
-
conflicts.push({
|
|
133
|
-
type: "node_type_mismatch",
|
|
134
|
-
node: name,
|
|
135
|
-
message: `Node "${name}" has different types: existing="${existingNode.actionType}", incoming="${incomingNode.actionType}"`,
|
|
136
|
-
});
|
|
137
|
-
nodesModified.push(name);
|
|
138
|
-
}
|
|
139
|
-
else if (JSON.stringify(existingNode) !== JSON.stringify(incomingNode)) {
|
|
140
|
-
nodesModified.push(name);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
nodesUnchanged.push(name);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// Check for new nodes
|
|
147
|
-
for (const [name] of incomingNodes) {
|
|
148
|
-
if (!existingNodes.has(name)) {
|
|
149
|
-
nodesAdded.push(name);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// Enum comparison
|
|
153
|
-
const enumsAdded = [];
|
|
154
|
-
const enumsRemoved = [];
|
|
155
|
-
const enumsModified = [];
|
|
156
|
-
const enumsUnchanged = [];
|
|
157
|
-
for (const [name, existingEnum] of existingEnums) {
|
|
158
|
-
const incomingEnum = incomingEnums.get(name);
|
|
159
|
-
if (!incomingEnum) {
|
|
160
|
-
enumsRemoved.push(name);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
// Compare options
|
|
164
|
-
const existingOpts = existingEnum.options.map((o) => o.name).sort().join(",");
|
|
165
|
-
const incomingOpts = incomingEnum.options.map((o) => o.name).sort().join(",");
|
|
166
|
-
if (existingOpts !== incomingOpts) {
|
|
167
|
-
enumsModified.push(name);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
enumsUnchanged.push(name);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
for (const [name] of incomingEnums) {
|
|
175
|
-
if (!existingEnums.has(name)) {
|
|
176
|
-
enumsAdded.push(name);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
// Results comparison
|
|
180
|
-
const existingResults = Object.keys(existing.results ?? {});
|
|
181
|
-
const incomingResults = Object.keys(incoming.results ?? {});
|
|
182
|
-
const resultsAdded = incomingResults.filter((r) => !existingResults.includes(r));
|
|
183
|
-
const resultsRemoved = existingResults.filter((r) => !incomingResults.includes(r));
|
|
184
|
-
// Structural analysis
|
|
185
|
-
const hasHitlNodes = [...incomingNodes.values()].some((n) => n.actionType.includes("hitl") || n.actionType.includes("general_hitl")) || nodesAdded.some((name) => name.includes("hitl"));
|
|
186
|
-
const hasCategorizerChanges = enumsAdded.length > 0 || enumsModified.length > 0;
|
|
187
|
-
// Check for broken references (nodes referencing removed nodes)
|
|
188
|
-
for (const [name, node] of incomingNodes) {
|
|
189
|
-
const inputs = node.inputs;
|
|
190
|
-
for (const [inputKey, binding] of Object.entries(inputs)) {
|
|
191
|
-
const actionOutput = binding?.actionOutput;
|
|
192
|
-
if (actionOutput) {
|
|
193
|
-
const refNodeName = String(actionOutput.actionName ?? "");
|
|
194
|
-
if (refNodeName && !incomingNodes.has(refNodeName)) {
|
|
195
|
-
conflicts.push({
|
|
196
|
-
type: "reference_broken",
|
|
197
|
-
node: name,
|
|
198
|
-
message: `Node "${name}" references non-existent node "${refNodeName}" in input "${inputKey}"`,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Check runIf references
|
|
204
|
-
if (node.runIf) {
|
|
205
|
-
const lhs = node.runIf.lhs;
|
|
206
|
-
const actionOutput = lhs?.actionOutput;
|
|
207
|
-
if (actionOutput) {
|
|
208
|
-
const refNodeName = String(actionOutput.actionName ?? "");
|
|
209
|
-
if (refNodeName && !incomingNodes.has(refNodeName)) {
|
|
210
|
-
conflicts.push({
|
|
211
|
-
type: "reference_broken",
|
|
212
|
-
node: name,
|
|
213
|
-
message: `Node "${name}" runIf references non-existent node "${refNodeName}"`,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
nodesAdded,
|
|
221
|
-
nodesRemoved,
|
|
222
|
-
nodesModified,
|
|
223
|
-
nodesUnchanged,
|
|
224
|
-
enumsAdded,
|
|
225
|
-
enumsRemoved,
|
|
226
|
-
enumsModified,
|
|
227
|
-
enumsUnchanged,
|
|
228
|
-
resultsAdded,
|
|
229
|
-
resultsRemoved,
|
|
230
|
-
hasHitlNodes,
|
|
231
|
-
hasCategorizerChanges,
|
|
232
|
-
conflicts,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Merge an incoming workflow into an existing one intelligently.
|
|
237
|
-
*
|
|
238
|
-
* Strategy:
|
|
239
|
-
* - Preserve existing workflowName (required by backend)
|
|
240
|
-
* - For nodes: add new nodes, update modified nodes, keep unchanged nodes
|
|
241
|
-
* - For enums: merge enum options (add new options, keep existing)
|
|
242
|
-
* - For results: merge result mappings
|
|
243
|
-
* - Detect conflicts and route to Autobuilder if HITL involved
|
|
244
|
-
*
|
|
245
|
-
* @param existing - The existing workflow
|
|
246
|
-
* @param incoming - The incoming workflow with changes
|
|
247
|
-
* @param options - Merge options
|
|
248
|
-
* @returns The merge result including the merged workflow and any conflicts
|
|
249
|
-
*/
|
|
250
|
-
export function mergeWorkflows(existing, incoming, options = {}) {
|
|
251
|
-
// Guard against null/undefined inputs
|
|
252
|
-
if (!existing || typeof existing !== "object") {
|
|
253
|
-
existing = {};
|
|
254
|
-
}
|
|
255
|
-
if (!incoming || typeof incoming !== "object") {
|
|
256
|
-
incoming = {};
|
|
257
|
-
}
|
|
258
|
-
const diff = compareWorkflows(existing, incoming);
|
|
259
|
-
const warnings = [];
|
|
260
|
-
const errors = [];
|
|
261
|
-
// Check for fatal conflicts
|
|
262
|
-
const fatalConflicts = diff.conflicts.filter((c) => c.type === "node_type_mismatch");
|
|
263
|
-
if (fatalConflicts.length > 0 && !options.forceReplace) {
|
|
264
|
-
return {
|
|
265
|
-
success: false,
|
|
266
|
-
mergedWorkflow: existing,
|
|
267
|
-
diff,
|
|
268
|
-
warnings,
|
|
269
|
-
errors: fatalConflicts.map((c) => c.message),
|
|
270
|
-
requiresAutobuilder: true,
|
|
271
|
-
description: `Cannot merge: ${fatalConflicts.length} node type mismatch(es)`,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
// Start with a deep copy of the incoming workflow
|
|
275
|
-
const merged = JSON.parse(JSON.stringify(incoming));
|
|
276
|
-
// Preserve existing workflowName (critical for backend validation)
|
|
277
|
-
if (existing.workflowName) {
|
|
278
|
-
merged.workflowName = JSON.parse(JSON.stringify(existing.workflowName));
|
|
279
|
-
}
|
|
280
|
-
// Get parsed structures
|
|
281
|
-
const existingNodes = parseWorkflowNodes(existing);
|
|
282
|
-
const existingEnums = parseWorkflowEnums(existing);
|
|
283
|
-
const incomingEnums = parseWorkflowEnums(incoming);
|
|
284
|
-
// === ENUM MERGING ===
|
|
285
|
-
// Merge enum types: keep existing structure but add new options
|
|
286
|
-
const mergedEnumTypes = [];
|
|
287
|
-
const processedEnums = new Set();
|
|
288
|
-
// Process existing enums first
|
|
289
|
-
for (const [name, existingEnum] of existingEnums) {
|
|
290
|
-
const incomingEnum = incomingEnums.get(name);
|
|
291
|
-
processedEnums.add(name);
|
|
292
|
-
if (incomingEnum) {
|
|
293
|
-
// Merge options: keep all existing, add any new from incoming
|
|
294
|
-
const existingOptNames = new Set(existingEnum.options.map((o) => o.name));
|
|
295
|
-
const mergedOptions = [...existingEnum.options];
|
|
296
|
-
for (const opt of incomingEnum.options) {
|
|
297
|
-
if (!existingOptNames.has(opt.name)) {
|
|
298
|
-
mergedOptions.push(opt);
|
|
299
|
-
warnings.push(`Added new option "${opt.name}" to enum "${name}"`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
mergedEnumTypes.push({
|
|
303
|
-
name: existingEnum.fullName, // Use existing nested structure
|
|
304
|
-
options: mergedOptions,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
// Keep existing enum as-is
|
|
309
|
-
mergedEnumTypes.push({
|
|
310
|
-
name: existingEnum.fullName,
|
|
311
|
-
options: existingEnum.options,
|
|
312
|
-
});
|
|
313
|
-
warnings.push(`Kept existing enum "${name}" (not in incoming workflow)`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
// Add new enums from incoming
|
|
317
|
-
for (const [name, incomingEnum] of incomingEnums) {
|
|
318
|
-
if (!processedEnums.has(name)) {
|
|
319
|
-
mergedEnumTypes.push({
|
|
320
|
-
name: incomingEnum.fullName,
|
|
321
|
-
options: incomingEnum.options,
|
|
322
|
-
});
|
|
323
|
-
warnings.push(`Added new enum "${name}"`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
merged.enumTypes = mergedEnumTypes;
|
|
327
|
-
// === NODE MERGING ===
|
|
328
|
-
// If preserveExistingNodes is true, keep existing nodes that aren't in incoming
|
|
329
|
-
if (options.preserveExistingNodes && diff.nodesRemoved.length > 0) {
|
|
330
|
-
const existingActions = existing.actions;
|
|
331
|
-
const mergedActions = merged.actions;
|
|
332
|
-
if (existingActions && mergedActions) {
|
|
333
|
-
for (const nodeName of diff.nodesRemoved) {
|
|
334
|
-
const existingAction = existingActions.find((a) => String(a.name) === nodeName);
|
|
335
|
-
if (existingAction) {
|
|
336
|
-
mergedActions.push(JSON.parse(JSON.stringify(existingAction)));
|
|
337
|
-
warnings.push(`Preserved existing node "${nodeName}" (not in incoming workflow)`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
// === RESULTS MERGING ===
|
|
343
|
-
// Merge results: combine both existing and incoming
|
|
344
|
-
const existingResults = existing.results ?? {};
|
|
345
|
-
const incomingResults = incoming.results ?? {};
|
|
346
|
-
const mergedResults = {
|
|
347
|
-
...existingResults,
|
|
348
|
-
...incomingResults, // Incoming takes precedence
|
|
349
|
-
};
|
|
350
|
-
merged.results = mergedResults;
|
|
351
|
-
// Check for broken references after merge
|
|
352
|
-
const brokenRefs = diff.conflicts.filter((c) => c.type === "reference_broken");
|
|
353
|
-
if (brokenRefs.length > 0) {
|
|
354
|
-
errors.push(...brokenRefs.map((c) => c.message));
|
|
355
|
-
}
|
|
356
|
-
// Generate description
|
|
357
|
-
const descParts = [];
|
|
358
|
-
if (diff.nodesAdded.length > 0)
|
|
359
|
-
descParts.push(`+${diff.nodesAdded.length} nodes`);
|
|
360
|
-
if (diff.nodesRemoved.length > 0)
|
|
361
|
-
descParts.push(`-${diff.nodesRemoved.length} nodes`);
|
|
362
|
-
if (diff.nodesModified.length > 0)
|
|
363
|
-
descParts.push(`~${diff.nodesModified.length} modified`);
|
|
364
|
-
if (diff.enumsAdded.length > 0)
|
|
365
|
-
descParts.push(`+${diff.enumsAdded.length} enums`);
|
|
366
|
-
if (diff.enumsModified.length > 0)
|
|
367
|
-
descParts.push(`~${diff.enumsModified.length} enums`);
|
|
368
|
-
const description = descParts.length > 0
|
|
369
|
-
? `Brownfield merge: ${descParts.join(", ")}`
|
|
370
|
-
: "No changes detected";
|
|
371
|
-
return {
|
|
372
|
-
success: errors.length === 0,
|
|
373
|
-
mergedWorkflow: merged,
|
|
374
|
-
diff,
|
|
375
|
-
warnings,
|
|
376
|
-
errors,
|
|
377
|
-
requiresAutobuilder: diff.hasHitlNodes,
|
|
378
|
-
description,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Validate a merged workflow for deployment readiness.
|
|
383
|
-
*
|
|
384
|
-
* @param merged - The merged workflow to validate
|
|
385
|
-
* @returns Validation result with any issues found
|
|
386
|
-
*/
|
|
387
|
-
export function validateMergedWorkflow(merged) {
|
|
388
|
-
const issues = [];
|
|
389
|
-
// Guard against null/undefined input
|
|
390
|
-
if (!merged || typeof merged !== "object") {
|
|
391
|
-
return { valid: false, issues: ["Workflow is null or undefined"] };
|
|
392
|
-
}
|
|
393
|
-
const nodes = parseWorkflowNodes(merged);
|
|
394
|
-
const enums = parseWorkflowEnums(merged);
|
|
395
|
-
// Check 1: All referenced nodes exist
|
|
396
|
-
for (const [name, node] of nodes) {
|
|
397
|
-
// Check input references
|
|
398
|
-
for (const [inputKey, binding] of Object.entries(node.inputs)) {
|
|
399
|
-
const actionOutput = binding?.actionOutput;
|
|
400
|
-
if (actionOutput) {
|
|
401
|
-
const refNodeName = String(actionOutput.actionName ?? "");
|
|
402
|
-
if (refNodeName && !nodes.has(refNodeName) && refNodeName !== "trigger") {
|
|
403
|
-
// Special case: "trigger" is always valid
|
|
404
|
-
issues.push(`Node "${name}" references non-existent node "${refNodeName}" in input "${inputKey}"`);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
// Check runIf references
|
|
409
|
-
if (node.runIf) {
|
|
410
|
-
const lhs = node.runIf.lhs;
|
|
411
|
-
const actionOutput = lhs?.actionOutput;
|
|
412
|
-
if (actionOutput) {
|
|
413
|
-
const refNodeName = String(actionOutput.actionName ?? "");
|
|
414
|
-
if (refNodeName && !nodes.has(refNodeName) && refNodeName !== "trigger") {
|
|
415
|
-
issues.push(`Node "${name}" runIf references non-existent node "${refNodeName}"`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// Check typeArguments enum references
|
|
420
|
-
if (node.typeArguments) {
|
|
421
|
-
const categories = node.typeArguments.categories;
|
|
422
|
-
const enumType = categories?.enumType;
|
|
423
|
-
if (enumType) {
|
|
424
|
-
const enumName = extractEnumName(enumType.name);
|
|
425
|
-
if (enumName && !enums.has(enumName)) {
|
|
426
|
-
issues.push(`Node "${name}" references non-existent enum "${enumName}"`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
// Check 2: At least one trigger node exists
|
|
432
|
-
const hasTrigger = [...nodes.values()].some((n) => n.actionType.includes("trigger") || n.name === "trigger");
|
|
433
|
-
if (!hasTrigger && nodes.size > 0) {
|
|
434
|
-
issues.push("Workflow has no trigger node");
|
|
435
|
-
}
|
|
436
|
-
// Check 3: Results reference existing nodes
|
|
437
|
-
const results = merged.results;
|
|
438
|
-
if (results) {
|
|
439
|
-
for (const [resultKey, mapping] of Object.entries(results)) {
|
|
440
|
-
if (mapping.actionName && !nodes.has(mapping.actionName)) {
|
|
441
|
-
issues.push(`Result "${resultKey}" references non-existent node "${mapping.actionName}"`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return {
|
|
446
|
-
valid: issues.length === 0,
|
|
447
|
-
issues,
|
|
448
|
-
};
|
|
449
|
-
}
|