@ema.co/mcp-toolkit 2026.1.27 → 2026.1.28-2
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/dist/mcp/handlers/data/index.js +3 -0
- package/dist/mcp/handlers/persona/create.js +16 -0
- package/dist/mcp/handlers/persona/list.js +9 -4
- package/dist/mcp/handlers/persona/update.js +24 -2
- package/dist/mcp/handlers/workflow/deploy.js +20 -2
- package/dist/mcp/handlers/workflow/generate.js +39 -2
- package/dist/mcp/handlers/workflow/index.js +8 -3
- package/dist/mcp/handlers/workflow/modify.js +34 -7
- package/dist/mcp/handlers/workflow/validate.js +85 -0
- package/dist/mcp/handlers/workflow/validation.js +160 -0
- package/dist/mcp/resources.js +286 -4
- package/dist/mcp/server.js +16 -3
- package/dist/mcp/tools.js +32 -11
- package/dist/sdk/client.js +36 -9
- package/dist/sdk/ema-client.js +32 -4
- package/dist/sdk/index.js +3 -1
- package/dist/sdk/knowledge.js +5 -5
- package/dist/sdk/structural-rules.js +498 -0
- package/dist/sdk/workflow-generator.js +2 -1
- package/dist/sdk/workflow-intent.js +28 -96
- package/dist/sdk/workflow-path-enumerator.js +278 -0
- package/dist/sdk/workflow-static-validator.js +291 -0
- package/dist/sdk/workflow-validation-types.js +7 -0
- package/docs/README.md +14 -0
- package/docs/go-validator-analysis.md +323 -0
- package/docs/rule-format-specification.md +346 -0
- package/docs/validation-contract.md +397 -0
- package/docs/validation-error-format.md +326 -0
- package/package.json +1 -1
- package/dist/mcp/workflow-operations.js +0 -100
- package/dist/sdk/workflow-fixer.js +0 -48
- package/docs/dashboard-operations.md +0 -281
- package/docs/ema-user-guide.md +0 -1201
- package/docs/email-patterns.md +0 -120
- package/docs/mcp-tools-guide.md +0 -575
|
@@ -129,6 +129,38 @@ export const STRUCTURAL_INVARIANTS = [
|
|
|
129
129
|
fix: "Use entity_extraction to extract email_address, then connect to to_email",
|
|
130
130
|
severity: "critical",
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
id: "no_redundant_classifiers",
|
|
134
|
+
name: "No Redundant Classifiers",
|
|
135
|
+
rule: "Multiple categorizers analyzing the same input source cause overlapping/conflicting routing decisions.",
|
|
136
|
+
violation: "Two chat_categorizer nodes both receive trigger.chat_conversation",
|
|
137
|
+
fix: "Consolidate into single classifier, or chain sequentially with clear precedence (first categorizer → second categorizer)",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "all_inputs_exist",
|
|
142
|
+
name: "All Input Sources Must Exist",
|
|
143
|
+
rule: "When a node expects input from another node, that source node must exist in the workflow.",
|
|
144
|
+
violation: "Node 'respond' expects data from 'search_old' but that node doesn't exist",
|
|
145
|
+
fix: "Add the missing source node or update the input binding to reference an existing node",
|
|
146
|
+
severity: "critical",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "gated_dependencies_match",
|
|
150
|
+
name: "Gated Dependencies Need Matching Gates",
|
|
151
|
+
rule: "If a node depends on a gated source (has runIf), the dependent node should also be gated with a matching condition, or the dependency may not provide data.",
|
|
152
|
+
violation: "Node 'respond' depends on gated 'search_billing' (only runs when category='billing') but has no gate itself",
|
|
153
|
+
fix: "Add matching runIf condition to the dependent node, or restructure so dependency is always available",
|
|
154
|
+
severity: "warning",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "no_orphaned_outputs",
|
|
158
|
+
name: "No Orphaned Outputs",
|
|
159
|
+
rule: "Node outputs that aren't consumed by any downstream node or mapped to WORKFLOW_OUTPUT are wasted computation.",
|
|
160
|
+
violation: "Node 'search_backup' output is never used",
|
|
161
|
+
fix: "Connect the output to a downstream node, map to WORKFLOW_OUTPUT, or remove the node if unused",
|
|
162
|
+
severity: "warning",
|
|
163
|
+
},
|
|
132
164
|
];
|
|
133
165
|
export const EXECUTION_RULES = [
|
|
134
166
|
{
|
|
@@ -199,6 +231,24 @@ export const COMMON_STRUCTURAL_MISTAKES = [
|
|
|
199
231
|
cause: "Nodes created but never connected, or connections removed without removing node",
|
|
200
232
|
fix: "Either connect the node or remove it. Use analyzeWorkflow to detect orphans.",
|
|
201
233
|
},
|
|
234
|
+
{
|
|
235
|
+
id: "redundant_classifiers",
|
|
236
|
+
symptom: "Workflow has multiple categorizers that analyze the same input",
|
|
237
|
+
cause: "Two or more chat_categorizer nodes both receive the same conversation input",
|
|
238
|
+
fix: "Consolidate into single classifier, or chain sequentially (first categorizer → second categorizer with different categories)",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "missing_input_source",
|
|
242
|
+
symptom: "Node fails with 'missing input' error",
|
|
243
|
+
cause: "Input binding references a node that doesn't exist in the workflow",
|
|
244
|
+
fix: "Add the missing source node or update the input binding to reference an existing node",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "gated_dependency_mismatch",
|
|
248
|
+
symptom: "Node doesn't receive expected data even when source node exists",
|
|
249
|
+
cause: "Source node is gated (has runIf) but dependent node has no matching gate",
|
|
250
|
+
fix: "Add matching runIf condition to dependent node, or restructure so dependency is always available",
|
|
251
|
+
},
|
|
202
252
|
{
|
|
203
253
|
id: "search_no_results",
|
|
204
254
|
symptom: "Response says 'I don't have information' even when KB has data",
|
|
@@ -276,6 +326,448 @@ After generating/modifying a workflow, verify:
|
|
|
276
326
|
- [ ] Email recipients come from entity_extraction
|
|
277
327
|
- [ ] No circular dependencies
|
|
278
328
|
`;
|
|
329
|
+
export const GRAPH_ANALYSIS_RULES = [
|
|
330
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
331
|
+
// Loop Detection
|
|
332
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
333
|
+
{
|
|
334
|
+
id: "circular_loop",
|
|
335
|
+
name: "Circular Loop Detection",
|
|
336
|
+
definition: "A cycle in the directed graph of nodes (A → B → ... → A). Circular loops cause infinite execution.",
|
|
337
|
+
symptoms: [
|
|
338
|
+
"Presence of directed cycles in node connections",
|
|
339
|
+
"Edges that lead back to nodes earlier in the path without a runIf/condition or state-change guard",
|
|
340
|
+
"A path that repeatedly produces the same trigger event",
|
|
341
|
+
],
|
|
342
|
+
examplePattern: "adjacency: trigger → A, A → B, B → A (cycle). Or node A has an output that is connected to trigger.user_query or trigger.chat_conversation without a gating condition.",
|
|
343
|
+
concreteExample: {
|
|
344
|
+
snippet: `{
|
|
345
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
346
|
+
"A": {
|
|
347
|
+
"action_name": "action_A",
|
|
348
|
+
"incoming_edges": [
|
|
349
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" },
|
|
350
|
+
{ "source_node_id": "B", "source_output": "feedback", "target_input": "query" }
|
|
351
|
+
]
|
|
352
|
+
},
|
|
353
|
+
"B": {
|
|
354
|
+
"action_name": "action_B",
|
|
355
|
+
"incoming_edges": [
|
|
356
|
+
{ "source_node_id": "A", "source_output": "result", "target_input": "input" }
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}`,
|
|
360
|
+
whyThisMatches: "A → B → A forms a directed cycle. Also re-entry if A/B feed fields that the trigger listens to (e.g., sending to trigger.user_query) — flag as high risk when no runIf/state-change guard.",
|
|
361
|
+
quickRemediation: "Break the cycle with a runIf guard, state transition, or remove re-entry edge.",
|
|
362
|
+
},
|
|
363
|
+
detectionAlgorithm: "Build adjacency list from incoming_edges: for each edge (source_node_id → target_node_id). Run depth-first search (DFS) or Tarjan's SCC to find cycles. If any SCC has size > 1 or a self-loop, report a cycle.",
|
|
364
|
+
detectionSteps: [
|
|
365
|
+
"Create directed graph G from node connections",
|
|
366
|
+
"Run cycle detection (DFS or Tarjan's SCC algorithm)",
|
|
367
|
+
"List cycles with node sequences",
|
|
368
|
+
"For each cycle, look for nodes in the cycle with gating conditions (runIf, trigger_when)",
|
|
369
|
+
"If cycle has no gating conditions, mark as 'potential infinite loop' (high risk)",
|
|
370
|
+
],
|
|
371
|
+
remediation: "Add explicit runIf/trigger_when conditions, counters, or idempotency checks. Break the cycle by adding a terminal node or require user confirmation before re-trigger.",
|
|
372
|
+
severity: "critical",
|
|
373
|
+
thresholds: [
|
|
374
|
+
{
|
|
375
|
+
description: "Cycle severity",
|
|
376
|
+
value: "Cycles with no gating → high; cycles with explicit counters or state updates → medium/low",
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
381
|
+
// Multiple Responder Detection
|
|
382
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
383
|
+
{
|
|
384
|
+
id: "multiple_responders",
|
|
385
|
+
name: "Multiple Responder Detection",
|
|
386
|
+
definition: "Two or more responder nodes (call_llm, external_action_caller, response sinks) can be activated for the same incoming trigger, producing duplicated user-visible outputs.",
|
|
387
|
+
symptoms: [
|
|
388
|
+
"Multiple nodes with the same trigger_when category or overlapping conditions",
|
|
389
|
+
"Multiple call_llm or external_action_caller nodes subscribing to the same category or to an ungated default",
|
|
390
|
+
"Same node type present in parallel paths that both end up connected to the Workflow Output sink",
|
|
391
|
+
],
|
|
392
|
+
examplePattern: "chat_categorizer → category::Cancel Booking → external_action_caller A; chat_categorizer → category::Cancel Booking → external_action_caller B; Both A and B outputs connect to WORKFLOW_OUTPUT.",
|
|
393
|
+
concreteExample: {
|
|
394
|
+
snippet: `{
|
|
395
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
396
|
+
"categorizer": {
|
|
397
|
+
"action_name": "chat_categorizer",
|
|
398
|
+
"incoming_edges": [
|
|
399
|
+
{ "source_node_id": "trigger", "source_output": "chat_conversation", "target_input": "conversation" }
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
"responder_1": {
|
|
403
|
+
"action_name": "call_llm",
|
|
404
|
+
"incoming_edges": [
|
|
405
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" },
|
|
406
|
+
{ "source_node_id": "categorizer", "source_output": "category::Billing", "target_input": "trigger_when" }
|
|
407
|
+
]
|
|
408
|
+
},
|
|
409
|
+
"responder_2": {
|
|
410
|
+
"action_name": "call_llm",
|
|
411
|
+
"incoming_edges": [
|
|
412
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" },
|
|
413
|
+
{ "source_node_id": "categorizer", "source_output": "category::Billing", "target_input": "trigger_when" }
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
}`,
|
|
417
|
+
whyThisMatches: "Two responders subscribe to the same category::Billing → both can fire.",
|
|
418
|
+
quickRemediation: "Consolidate responders or add mutually exclusive runIf conditions or priorities.",
|
|
419
|
+
},
|
|
420
|
+
detectionAlgorithm: "Group responder nodes (call_llm, external_action_caller, any node that produces final responses). For each responder, extract its trigger_when or runIf conditions; compute condition overlap: exact-match overlap (identical categories/conditions) or semantic overlap (overlapping category sets or both are ungated). If two responders have overlapping triggers and both feed the final sink (or both send to users), flag as multiple responders.",
|
|
421
|
+
detectionSteps: [
|
|
422
|
+
"Find all nodes with action_name in {call_llm, external_action_caller, respond_with_sources}",
|
|
423
|
+
"For each responder, extract trigger_when or runIf conditions",
|
|
424
|
+
"For each pair of responders, compute condition overlap",
|
|
425
|
+
"Check if both responders feed WORKFLOW_OUTPUT or response sink",
|
|
426
|
+
"If overlap AND both feed response sink, report as duplicate responders",
|
|
427
|
+
],
|
|
428
|
+
remediation: "Consolidate responders behind a single handler, add disambiguating conditions, or add a categorizer route that ensures only one responder is triggered.",
|
|
429
|
+
severity: "critical",
|
|
430
|
+
thresholds: [
|
|
431
|
+
{
|
|
432
|
+
description: "Condition overlap",
|
|
433
|
+
value: "Exact matches → high risk; semantic overlap (subset patterns) → medium risk",
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
438
|
+
// Ungated Catch-All Detection
|
|
439
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
440
|
+
{
|
|
441
|
+
id: "ungated_catch_all",
|
|
442
|
+
name: "Ungated Catch-All Detection",
|
|
443
|
+
definition: "A node that accepts the main trigger input (user_query or conversation) without a gating condition (no trigger_when, runIf or categorizer constraint), effectively acting as a default/catch-all that runs for all inputs.",
|
|
444
|
+
symptoms: [
|
|
445
|
+
"Nodes that have inputs from trigger.user_query or trigger.chat_conversation but do not have any trigger_when/runIf or category gating edges",
|
|
446
|
+
"A categorizer that has a 'default' category wired to a node that is intended to be fallback but has no specificity, causing it to capture everything",
|
|
447
|
+
],
|
|
448
|
+
examplePattern: "call_llm node: incoming edge from trigger.user_query and no trigger_when edge from categorizer → acts as catch-all.",
|
|
449
|
+
concreteExample: {
|
|
450
|
+
snippet: `{
|
|
451
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
452
|
+
"cat": {
|
|
453
|
+
"action_name": "chat_categorizer",
|
|
454
|
+
"incoming_edges": [
|
|
455
|
+
{ "source_node_id": "trigger", "source_output": "chat_conversation", "target_input": "conversation" }
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
"catch_responder": {
|
|
459
|
+
"action_name": "call_llm",
|
|
460
|
+
"incoming_edges": [
|
|
461
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" },
|
|
462
|
+
{ "source_node_id": "cat", "source_output": "category::CatchAll", "target_input": "trigger_when" }
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
}`,
|
|
466
|
+
whyThisMatches: "CatchAll connected directly to responder; if no other handlers have priority, every message may be consumed here.",
|
|
467
|
+
quickRemediation: "Add explicit priorities, runIf conditions, or refine categorizer so specific categories are routed first.",
|
|
468
|
+
},
|
|
469
|
+
detectionAlgorithm: "For each node that consumes trigger.* inputs directly, check presence of a trigger_when or runIf incoming edge from a categorizer or another gating node. If absent, flag as ungated catch-all. Also detect categorizer categories labeled 'default' or 'catch-all' that are broad (e.g., no examples or a category with pattern '*').",
|
|
470
|
+
detectionSteps: [
|
|
471
|
+
"For each node that consumes trigger.user_query or trigger.chat_conversation",
|
|
472
|
+
"Check if node has trigger_when or runIf incoming edge from categorizer",
|
|
473
|
+
"If absent, flag as ungated catch-all",
|
|
474
|
+
"Check categorizer categories for 'default' or 'catch-all' with broad patterns",
|
|
475
|
+
],
|
|
476
|
+
remediation: "Add explicit gating conditions or make it an explicit fallback with lower priority and documentation; use categorizer default only if intended and ensure it's mutually exclusive with other handlers.",
|
|
477
|
+
severity: "warning",
|
|
478
|
+
},
|
|
479
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
480
|
+
// Redundant Classifier Detection
|
|
481
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
482
|
+
{
|
|
483
|
+
id: "redundant_classifier",
|
|
484
|
+
name: "Redundant Classifier Detection",
|
|
485
|
+
definition: "Multiple categorizer/classifier components that overlap heavily in categories or are functionally duplicated, producing extra routing complexity and possible inconsistencies.",
|
|
486
|
+
symptoms: [
|
|
487
|
+
"Multiple chat_categorizer nodes with same or overlapping category names",
|
|
488
|
+
"Categories with identical examples/descriptions or categories that are strict subsets of others and are both used for routing",
|
|
489
|
+
"Multiple categorizer outputs wired to the same downstream nodes causing duplicate triggers",
|
|
490
|
+
],
|
|
491
|
+
examplePattern: "chat_categorizer A categories: {Cancel Booking, Flight Status}; chat_categorizer B categories: {Cancel Booking, Change Booking}; Both wired to the same downstream nodes for Cancel Booking (duplicate classification).",
|
|
492
|
+
concreteExample: {
|
|
493
|
+
snippet: `{
|
|
494
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
495
|
+
"classifier_1": {
|
|
496
|
+
"action_name": "chat_categorizer",
|
|
497
|
+
"incoming_edges": [
|
|
498
|
+
{ "source_node_id": "trigger", "source_output": "chat_conversation", "target_input": "conversation" }
|
|
499
|
+
]
|
|
500
|
+
},
|
|
501
|
+
"classifier_2": {
|
|
502
|
+
"action_name": "chat_categorizer",
|
|
503
|
+
"incoming_edges": [
|
|
504
|
+
{ "source_node_id": "classifier_1", "source_output": "conversation", "target_input": "conversation" }
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
"responder": {
|
|
508
|
+
"action_name": "call_llm",
|
|
509
|
+
"incoming_edges": [
|
|
510
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" },
|
|
511
|
+
{ "source_node_id": "classifier_2", "source_output": "category::Support", "target_input": "trigger_when" }
|
|
512
|
+
]
|
|
513
|
+
}
|
|
514
|
+
}`,
|
|
515
|
+
whyThisMatches: "Two classifiers in series often replicate work or re-label identically.",
|
|
516
|
+
quickRemediation: "Merge classifiers, or ensure each has distinct responsibility (coarse vs. fine classification) with documented downstream expectations.",
|
|
517
|
+
},
|
|
518
|
+
detectionAlgorithm: "List all categorizer nodes and their categories (name, examples). Compute string similarity and example overlap between categories across categorizers. Find categories that share > X% of example texts or identical names — flag as potential redundancy. Detect multiple categorizer outputs feeding the same node for the same category.",
|
|
519
|
+
detectionSteps: [
|
|
520
|
+
"List all categorizer nodes and their categories (name, examples)",
|
|
521
|
+
"Compute string similarity and example overlap between categories across categorizers",
|
|
522
|
+
"Find categories that share > 60-70% of example texts or identical names",
|
|
523
|
+
"Detect multiple categorizer outputs feeding the same node for the same category",
|
|
524
|
+
"Flag as potential redundancy",
|
|
525
|
+
],
|
|
526
|
+
remediation: "Merge classifiers or consolidate category definitions. Make classifier responsibilities orthogonal (e.g., one for intents, one for sentiment).",
|
|
527
|
+
severity: "warning",
|
|
528
|
+
thresholds: [
|
|
529
|
+
{
|
|
530
|
+
description: "Category similarity",
|
|
531
|
+
value: "Flag if > 60-70% example overlap or identical names",
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
},
|
|
535
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
536
|
+
// Data Flow Analysis
|
|
537
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
538
|
+
{
|
|
539
|
+
id: "data_flow_analysis",
|
|
540
|
+
name: "Data Flow Analysis",
|
|
541
|
+
definition: "Ensuring required inputs are provided to nodes, outputs are consumed, no type mismatches, and the overall flow is consistent.",
|
|
542
|
+
symptoms: [
|
|
543
|
+
"Node inputs that have no incoming_edges (missing required data)",
|
|
544
|
+
"Edges that reference non-existent source outputs or target inputs (typos)",
|
|
545
|
+
"Outputs that are never consumed (dead outputs) or consumed by nodes that don't exist",
|
|
546
|
+
"Type/semantic mismatches (e.g., passing a structured tool_execution_result into an LLM prompt expecting plain text)",
|
|
547
|
+
],
|
|
548
|
+
examplePattern: "Edge references source_output that does not exist in source node outputs → report. Node input field has no incoming edge → report missing input.",
|
|
549
|
+
concreteExample: {
|
|
550
|
+
snippet: `{
|
|
551
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
552
|
+
"finder": {
|
|
553
|
+
"action_name": "find_booking",
|
|
554
|
+
"incoming_edges": [
|
|
555
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" }
|
|
556
|
+
],
|
|
557
|
+
"outputs": ["booking_id"]
|
|
558
|
+
},
|
|
559
|
+
"responder": {
|
|
560
|
+
"action_name": "call_llm",
|
|
561
|
+
"incoming_edges": [
|
|
562
|
+
{ "source_node_id": "finder", "source_output": "booking_id", "target_input": "booking_ref" }
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
}`,
|
|
566
|
+
whyThisMatches: "responder expects booking_ref but finder outputs booking_id — mismatch leads to empty data at runtime.",
|
|
567
|
+
quickRemediation: "Align field names or add a transformer node to map booking_id -> booking_ref. Report outputs that are never consumed or inputs that have no upstream provider.",
|
|
568
|
+
},
|
|
569
|
+
detectionAlgorithm: "For each node, verify all required input fields are satisfied by incoming_edges. Validate source_output names exist on the source node (check the node spec for outputs). Check that at least one sink consumes the node's significant outputs; if none, mark as unused output. Identify type mismatches by checking node input/output semantic names (e.g., tool_execution_result vs. response_with_sources). Trace data lineage: for each final output (WorkflowOutput sink), walk backwards to see which nodes contributed which fields.",
|
|
570
|
+
detectionSteps: [
|
|
571
|
+
"For node N: for input field f in N.required_inputs: if no incoming edge maps to f, report missing input",
|
|
572
|
+
"For edge e: ensure source_node has output named source_output; ensure target_node has input named target_input",
|
|
573
|
+
"Check that node outputs are consumed by at least one downstream node or WORKFLOW_OUTPUT",
|
|
574
|
+
"Validate type compatibility: check semantic names match expected types",
|
|
575
|
+
"Trace data lineage from WORKFLOW_OUTPUT backwards to verify all paths are valid",
|
|
576
|
+
],
|
|
577
|
+
remediation: "Add missing edges, correct typos, or refactor node interfaces. Introduce intermediate transformation nodes where necessary.",
|
|
578
|
+
severity: "critical",
|
|
579
|
+
},
|
|
580
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
581
|
+
// RunIf Condition Validation
|
|
582
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
583
|
+
{
|
|
584
|
+
id: "runif_validation",
|
|
585
|
+
name: "RunIf Condition Validation",
|
|
586
|
+
definition: "Ensuring runIf/trigger_when conditions are syntactically valid, reference existing inputs/fields, and are logically satisfiable.",
|
|
587
|
+
symptoms: [
|
|
588
|
+
"Conditions referencing non-existent variables (typos), impossible conditions (x > 5 and x < 3), or always-true/always-false conditions",
|
|
589
|
+
"Use of unsupported operators or invalid value types",
|
|
590
|
+
"Logical contradictions between conditions on parallel nodes that make some nodes unreachable",
|
|
591
|
+
],
|
|
592
|
+
examplePattern: "Node has runIf: 'user.locale == \"en-US\" && user.locale == \"fr-FR\"' → unreachable. Node runIf references 'trigger.user_input_text' but upstream node outputs 'user_query' instead → invalid reference.",
|
|
593
|
+
concreteExample: {
|
|
594
|
+
snippet: `{
|
|
595
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
596
|
+
"proc": {
|
|
597
|
+
"action_name": "process_booking",
|
|
598
|
+
"incoming_edges": [
|
|
599
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" }
|
|
600
|
+
],
|
|
601
|
+
"runIf": "booking.status == 'confirmed' && user.is_admin"
|
|
602
|
+
}
|
|
603
|
+
}`,
|
|
604
|
+
whyThisMatches: "if upstream nodes never produce booking or user objects, the expression references undefined variables — should be validated.",
|
|
605
|
+
quickRemediation: "Validate runIf identifiers against upstream outputs; add safe existence checks or use documented expression language. Flag conditions referencing unknown fields as errors.",
|
|
606
|
+
},
|
|
607
|
+
detectionAlgorithm: "Parse each runIf/trigger_when expression. Validate identifiers exist in the upstream data model (available outputs). Evaluate static satisfiability where possible: check for contradictions (e.g., x == 'A' and x == 'B'). Check boolean expression correctness (operators, parentheses). Flag conditions that are always true or always false given constant inputs.",
|
|
608
|
+
detectionSteps: [
|
|
609
|
+
"Parse each runIf/trigger_when expression",
|
|
610
|
+
"Validate identifiers exist in the upstream data model (available outputs)",
|
|
611
|
+
"Evaluate static satisfiability: check for contradictions (e.g., x == 'A' and x == 'B')",
|
|
612
|
+
"Check boolean expression correctness (operators, parentheses)",
|
|
613
|
+
"Flag conditions that are always true or always false given constant inputs",
|
|
614
|
+
],
|
|
615
|
+
remediation: "Fix typos, ensure conditions reference correct fields, simplify conditions, or add descriptive comments/examples.",
|
|
616
|
+
severity: "critical",
|
|
617
|
+
},
|
|
618
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
619
|
+
// Parallel Execution Opportunities
|
|
620
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
621
|
+
{
|
|
622
|
+
id: "parallel_execution_opportunities",
|
|
623
|
+
name: "Parallel Execution Opportunities",
|
|
624
|
+
definition: "Independent nodes or subgraphs that can be executed concurrently (no data dependencies between them).",
|
|
625
|
+
symptoms: [
|
|
626
|
+
"Multiple responder or action nodes consuming only trigger data and not depending on each other's outputs",
|
|
627
|
+
"Subgraphs that start from the trigger and end in independent sinks with no cross edges",
|
|
628
|
+
],
|
|
629
|
+
examplePattern: "Nodes reachable from trigger that have no inter-node edges → mark as parallelizable.",
|
|
630
|
+
concreteExample: {
|
|
631
|
+
snippet: `{
|
|
632
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
633
|
+
"A": {
|
|
634
|
+
"action_name": "enrich_profile",
|
|
635
|
+
"incoming_edges": [
|
|
636
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" }
|
|
637
|
+
]
|
|
638
|
+
},
|
|
639
|
+
"B": {
|
|
640
|
+
"action_name": "fetch_metrics",
|
|
641
|
+
"incoming_edges": [
|
|
642
|
+
{ "source_node_id": "trigger", "source_output": "user_query", "target_input": "query" }
|
|
643
|
+
]
|
|
644
|
+
},
|
|
645
|
+
"merge": {
|
|
646
|
+
"action_name": "aggregate_results",
|
|
647
|
+
"incoming_edges": [
|
|
648
|
+
{ "source_node_id": "A", "source_output": "profile", "target_input": "profile" },
|
|
649
|
+
{ "source_node_id": "B", "source_output": "metrics", "target_input": "metrics" }
|
|
650
|
+
]
|
|
651
|
+
}
|
|
652
|
+
}`,
|
|
653
|
+
whyThisMatches: "A and B do not depend on each other and can be scheduled in parallel; only merge needs both results.",
|
|
654
|
+
quickRemediation: "Mark independent branches as parallelizable or set executor to run independent nodes concurrently.",
|
|
655
|
+
},
|
|
656
|
+
detectionAlgorithm: "Build dependency DAG (edges represent data dependency). Identify nodes or groups with no interconnections (no path between them); these are candidates for parallel execution. Use topological sort to find levels of nodes that can be run concurrently; nodes at same level with no edges between them are parallelizable.",
|
|
657
|
+
detectionSteps: [
|
|
658
|
+
"Build dependency DAG (edges represent data dependency)",
|
|
659
|
+
"Topologically sort graph",
|
|
660
|
+
"For each layer (nodes with same distance from sources), mark nodes that don't share dependencies as parallel",
|
|
661
|
+
"Identify nodes or groups with no interconnections (no path between them)",
|
|
662
|
+
],
|
|
663
|
+
remediation: "Recommend executing parallelizable actions concurrently to reduce latency. Ensure side-effects on shared resources are synchronized.",
|
|
664
|
+
severity: "info",
|
|
665
|
+
},
|
|
666
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
667
|
+
// Dead Code Path Detection
|
|
668
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
669
|
+
{
|
|
670
|
+
id: "dead_code_path",
|
|
671
|
+
name: "Dead Code Path Detection",
|
|
672
|
+
definition: "Nodes or edges that can never be reached/run during any valid execution (unreachable) or nodes whose outputs are never used.",
|
|
673
|
+
symptoms: [
|
|
674
|
+
"Nodes with no incoming edges from the trigger (except trigger node itself)",
|
|
675
|
+
"Nodes whose runIf/trigger_when are contradictory or shadowed by higher-priority rules",
|
|
676
|
+
"Outputs that are never connected to a sink",
|
|
677
|
+
],
|
|
678
|
+
examplePattern: "Node X exists, but no incoming edges. Or Node Y has a runIf that is false for all upstream values.",
|
|
679
|
+
concreteExample: {
|
|
680
|
+
snippet: `{
|
|
681
|
+
"trigger": { "action_name": "chat_trigger", "incoming_edges": [] },
|
|
682
|
+
"classifier": {
|
|
683
|
+
"action_name": "chat_categorizer",
|
|
684
|
+
"incoming_edges": [
|
|
685
|
+
{ "source_node_id": "trigger", "source_output": "chat_conversation", "target_input": "conversation" }
|
|
686
|
+
],
|
|
687
|
+
"outputs": ["category::A", "category::B", "category::Unused"]
|
|
688
|
+
},
|
|
689
|
+
"responder_A": {
|
|
690
|
+
"action_name": "call_llm",
|
|
691
|
+
"incoming_edges": [
|
|
692
|
+
{ "source_node_id": "classifier", "source_output": "category::A", "target_input": "trigger_when" }
|
|
693
|
+
]
|
|
694
|
+
},
|
|
695
|
+
"orphan": {
|
|
696
|
+
"action_name": "orphan_action",
|
|
697
|
+
"incoming_edges": [
|
|
698
|
+
{ "source_node_id": "nonexistent", "source_output": "x", "target_input": "input" }
|
|
699
|
+
]
|
|
700
|
+
}
|
|
701
|
+
}`,
|
|
702
|
+
whyThisMatches: "category::Unused is never wired to any node; orphan_action has no valid upstream → unreachable. Also check for runIf conditions that are constant-false.",
|
|
703
|
+
quickRemediation: "Remove or rewire unused categories/nodes; fix upstream wiring; simplify or document intentional dead paths.",
|
|
704
|
+
},
|
|
705
|
+
detectionAlgorithm: "Reachability analysis: starting from the trigger node(s), do BFS/DFS and mark visited nodes. Any node not visited = unreachable. For conditional nodes, conservatively evaluate whether their condition can be true given upstream outputs; if impossible, mark unreachable. For outputs, check whether there exists a path from node output to the WorkflowOutput sink; if none, mark as unused.",
|
|
706
|
+
detectionSteps: [
|
|
707
|
+
"Starting from trigger node(s), do BFS/DFS and mark visited nodes",
|
|
708
|
+
"Any node not visited = unreachable",
|
|
709
|
+
"For conditional nodes, evaluate whether their condition can be true given upstream outputs",
|
|
710
|
+
"If condition is impossible, mark as unreachable",
|
|
711
|
+
"For outputs, check if path exists from node output to WORKFLOW_OUTPUT sink",
|
|
712
|
+
"If no path, mark as unused output",
|
|
713
|
+
],
|
|
714
|
+
remediation: "Remove or repurpose unreachable nodes. Fix conditions or routing so intended logic is reachable.",
|
|
715
|
+
severity: "warning",
|
|
716
|
+
},
|
|
717
|
+
];
|
|
718
|
+
/**
|
|
719
|
+
* Generic LLM instruction for applying all graph analysis rules.
|
|
720
|
+
* Use this as a prompt template for LLM-based workflow validation.
|
|
721
|
+
*/
|
|
722
|
+
export const GRAPH_ANALYSIS_LLM_INSTRUCTION = `
|
|
723
|
+
Parse the workflow graph JSON. Build adjacency lists and node metadata (action_name, inputs, outputs, trigger_when/runIf/conditions). Run:
|
|
724
|
+
|
|
725
|
+
1. **Cycle detection** (Tarjan/DFS) - Find circular dependencies
|
|
726
|
+
2. **Responder-grouping and overlap detection** - Find duplicate responses
|
|
727
|
+
3. **Ungated-node scan** - Nodes directly consuming trigger.* with no trigger_when/runIf
|
|
728
|
+
4. **Classifier similarity comparison** - Redundant categorizers
|
|
729
|
+
5. **Dataflow validation** - Missing inputs, invalid edge references, type mismatches
|
|
730
|
+
6. **runIf parsing and satisfiability checks** - Invalid conditions
|
|
731
|
+
7. **Dependency DAG/topological sort for parallelism** - Optimization opportunities
|
|
732
|
+
8. **Reachability analysis** - Find dead code paths
|
|
733
|
+
|
|
734
|
+
For each finding produce:
|
|
735
|
+
- **type**: (Loop / Multiple responders / Ungated catch-all / Redundant classifier / Data flow / RunIf validation / Parallel opportunity / Dead code)
|
|
736
|
+
- **nodes_involved**: [node ids]
|
|
737
|
+
- **evidence**: short quote of graph edges/conditions
|
|
738
|
+
- **severity**: high/medium/low
|
|
739
|
+
- **suggested_fix**: one-sentence remediation
|
|
740
|
+
|
|
741
|
+
Optionally include a small graph snippet showing the offending edges.
|
|
742
|
+
`;
|
|
743
|
+
/**
|
|
744
|
+
* Concrete JSON-matching patterns for quick detection.
|
|
745
|
+
* These are patterns an LLM can look for in workflow JSON structures.
|
|
746
|
+
*/
|
|
747
|
+
export const GRAPH_ANALYSIS_PATTERNS = {
|
|
748
|
+
loop: "Find path sequence ['nodeA', 'nodeB', 'nodeA']",
|
|
749
|
+
multiple_responders: "List nodes where action_name in {call_llm, external_action_caller} and trigger_when == same_category → report duplicates",
|
|
750
|
+
ungated_catch_all: "Node has incoming from trigger.user_query AND no incoming trigger_when/runIf → report",
|
|
751
|
+
redundant_classifier: "Two chat_categorizer nodes with category names intersecting or identical examples → report",
|
|
752
|
+
data_flow: "Edge references source_output that does not exist in source node outputs → report",
|
|
753
|
+
runif: "Condition contains identifier not present in any upstream outputs → report",
|
|
754
|
+
parallelism: "Nodes reachable from trigger that have no inter-node edges → mark as parallelizable",
|
|
755
|
+
dead_code: "Node.id not in traversal from trigger → unreachable",
|
|
756
|
+
};
|
|
757
|
+
/**
|
|
758
|
+
* Concise algorithmic checks summary for LLM validation.
|
|
759
|
+
* These are short, actionable checks an LLM can run on workflow graphs.
|
|
760
|
+
*/
|
|
761
|
+
export const GRAPH_ANALYSIS_ALGORITHMIC_CHECKS = {
|
|
762
|
+
loop_detection: "Build adjacency graph from incoming_edges; run DFS/Tarjan to detect cycles (loop detection).",
|
|
763
|
+
multiple_responder_detection: "For each responder-type node, collect its trigger_when / category inputs and flag if multiple responders share the same condition (multiple responder detection).",
|
|
764
|
+
ungated_catch_all_detection: "Detect any 'catch-all' category outputs (names like CatchAll, Other, default, '*') wired without exclusive guards.",
|
|
765
|
+
redundant_classifier_detection: "Find classifier nodes in series or classifiers whose outputs are re-classified by another classifier without transformation (redundant classifier).",
|
|
766
|
+
data_flow_analysis: "Compare source_output names vs expected target_input names; list outputs with zero consumers and inputs with zero providers (data flow issues).",
|
|
767
|
+
runif_validation: "Parse runIf expressions: check referenced identifiers against upstream outputs; flag missing identifiers or unsupported syntax (runIf validation).",
|
|
768
|
+
parallel_execution_opportunities: "Identify independent subgraphs (no shared upstream dependencies) and suggest parallel execution points (parallel opportunities).",
|
|
769
|
+
dead_code_path_detection: "Find nodes with no path from trigger or nodes that never reach WORKFLOW_OUTPUT or whose runIf is constant false (dead code paths).",
|
|
770
|
+
};
|
|
279
771
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
772
|
// Exports
|
|
281
773
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -288,3 +780,9 @@ export function getInvariantById(id) {
|
|
|
288
780
|
export function getCriticalInvariants() {
|
|
289
781
|
return STRUCTURAL_INVARIANTS.filter(i => i.severity === "critical");
|
|
290
782
|
}
|
|
783
|
+
export function getGraphAnalysisRuleById(id) {
|
|
784
|
+
return GRAPH_ANALYSIS_RULES.find(r => r.id === id);
|
|
785
|
+
}
|
|
786
|
+
export function getAllGraphAnalysisRules() {
|
|
787
|
+
return GRAPH_ANALYSIS_RULES;
|
|
788
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* 2. Construct nodes based on user requirements
|
|
10
10
|
* 3. Pass to compileWorkflow() to generate deployment-ready JSON
|
|
11
11
|
*/
|
|
12
|
+
import { PROJECT_TYPES } from "./knowledge.js";
|
|
12
13
|
/**
|
|
13
14
|
* FALLBACK VALUES - Used only when ActionRegistry is not available.
|
|
14
15
|
* These are based on API data as of 2026-01-16.
|
|
@@ -329,7 +330,7 @@ function buildInputBinding(binding) {
|
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
332
|
function buildProtoConfig(spec) {
|
|
332
|
-
const projectType = spec.personaType
|
|
333
|
+
const projectType = PROJECT_TYPES[spec.personaType] ?? PROJECT_TYPES.dashboard;
|
|
333
334
|
// Widget format expected by Ema API:
|
|
334
335
|
// { name: "widgetName", type: <type_id>, widgetName: { ...config... } }
|
|
335
336
|
// The config is stored under a key matching the widget's name
|