@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.

Files changed (37) hide show
  1. package/.context/public/guides/ema-user-guide.md +12 -16
  2. package/.context/public/guides/mcp-tools-guide.md +203 -334
  3. package/dist/mcp/domain/loop-detection.js +97 -0
  4. package/dist/mcp/domain/structural-rules.js +4 -5
  5. package/dist/mcp/domain/validation-rules.js +5 -5
  6. package/dist/mcp/domain/workflow-graph.js +3 -5
  7. package/dist/mcp/guidance.js +9 -29
  8. package/dist/mcp/handlers/feedback/index.js +1 -1
  9. package/dist/mcp/handlers/persona/index.js +237 -8
  10. package/dist/mcp/handlers/persona/schema.js +27 -0
  11. package/dist/mcp/handlers/reference/index.js +6 -4
  12. package/dist/mcp/handlers/workflow/index.js +15 -19
  13. package/dist/mcp/handlers/workflow/validation.js +1 -1
  14. package/dist/mcp/knowledge-types.js +7 -0
  15. package/dist/mcp/knowledge.js +61 -800
  16. package/dist/mcp/resources.js +217 -1
  17. package/dist/mcp/server.js +195 -2152
  18. package/dist/mcp/tools.js +2 -3
  19. package/dist/sdk/generated/agent-catalog.js +615 -0
  20. package/dist/sdk/generated/widget-catalog.js +60 -0
  21. package/docs/README.md +17 -9
  22. package/package.json +1 -1
  23. package/.context/public/guides/dashboard-operations.md +0 -349
  24. package/.context/public/guides/email-patterns.md +0 -125
  25. package/.context/public/guides/workflow-builder-patterns.md +0 -708
  26. package/dist/mcp/domain/intent-architect.js +0 -914
  27. package/dist/mcp/domain/quality-gates.js +0 -110
  28. package/dist/mcp/domain/workflow-execution-analyzer.js +0 -412
  29. package/dist/mcp/domain/workflow-intent.js +0 -1806
  30. package/dist/mcp/domain/workflow-merge.js +0 -449
  31. package/dist/mcp/domain/workflow-tracer.js +0 -648
  32. package/dist/mcp/domain/workflow-transformer.js +0 -742
  33. package/dist/mcp/handlers/persona/intent.js +0 -141
  34. package/dist/mcp/handlers/workflow/analyze.js +0 -119
  35. package/dist/mcp/handlers/workflow/compare.js +0 -70
  36. package/dist/mcp/handlers/workflow/generate.js +0 -384
  37. package/dist/mcp/handlers-consolidated.js +0 -333
@@ -1,110 +0,0 @@
1
- /**
2
- * Quality Gates - Pre-Deploy Validation
3
- *
4
- * DEPRECATED: This module used detectWorkflowIssues which has been removed.
5
- *
6
- * The LLM should analyze workflows using rules from:
7
- * - ema://rules/anti-patterns
8
- * - ema://rules/input-sources
9
- * - ema://rules/optimizations
10
- *
11
- * Backend validation happens when workflow is deployed.
12
- * These quality gates are kept for backwards compatibility but are minimal.
13
- */
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // Minimal Quality Gates (structural only)
16
- // ─────────────────────────────────────────────────────────────────────────────
17
- export const QUALITY_GATES = [
18
- {
19
- id: "has_actions",
20
- name: "Has Actions",
21
- description: "Workflow must have at least one action",
22
- blocking: true,
23
- severity: "critical",
24
- check: (workflow) => {
25
- const actions = (workflow.actions || []);
26
- return {
27
- passed: actions.length > 0,
28
- message: actions.length > 0
29
- ? `Workflow has ${actions.length} action(s)`
30
- : "Workflow has no actions",
31
- };
32
- },
33
- },
34
- {
35
- id: "has_results",
36
- name: "Has Results Mapping",
37
- description: "Voice AI workflows must have results mapping",
38
- blocking: false,
39
- severity: "warning",
40
- check: (workflow) => {
41
- const results = workflow.results;
42
- const hasResults = results && Object.keys(results).length > 0;
43
- return {
44
- passed: hasResults || false,
45
- message: hasResults
46
- ? `Workflow has ${Object.keys(results).length} result mapping(s)`
47
- : "No results mapping found (may be OK for chat workflows)",
48
- };
49
- },
50
- },
51
- ];
52
- // ─────────────────────────────────────────────────────────────────────────────
53
- // Quality Gate Runner
54
- // ─────────────────────────────────────────────────────────────────────────────
55
- export function runQualityGates(workflow, gates = QUALITY_GATES) {
56
- const results = [];
57
- let blockingFailures = 0;
58
- let warnings = 0;
59
- for (const gate of gates) {
60
- const result = gate.check(workflow);
61
- results.push({
62
- gate_id: gate.id,
63
- gate_name: gate.name,
64
- result,
65
- blocking: gate.blocking,
66
- severity: gate.severity,
67
- });
68
- if (!result.passed) {
69
- if (gate.blocking) {
70
- blockingFailures++;
71
- }
72
- else if (gate.severity === "warning") {
73
- warnings++;
74
- }
75
- }
76
- }
77
- const deploymentAllowed = blockingFailures === 0;
78
- return {
79
- overall_status: blockingFailures > 0 ? "fail" : warnings > 0 ? "warning" : "pass",
80
- gates: results,
81
- blocking_failures: blockingFailures,
82
- warnings,
83
- deployment_allowed: deploymentAllowed,
84
- };
85
- }
86
- // ─────────────────────────────────────────────────────────────────────────────
87
- // Utility Functions
88
- // ─────────────────────────────────────────────────────────────────────────────
89
- export function isDeploymentAllowed(report) {
90
- return report.deployment_allowed;
91
- }
92
- export function getBlockingIssues(report) {
93
- return report.gates
94
- .filter(g => g.blocking && !g.result.passed)
95
- .map(g => `${g.gate_name}: ${g.result.message}`);
96
- }
97
- export function formatQualityReport(report) {
98
- let output = `Quality Report: ${report.overall_status.toUpperCase()}\n`;
99
- output += `Deployment Allowed: ${report.deployment_allowed ? "YES" : "NO"}\n\n`;
100
- for (const gate of report.gates) {
101
- const status = gate.result.passed ? "✓" : gate.blocking ? "✗" : "⚠";
102
- output += `${status} ${gate.gate_name}: ${gate.result.message}\n`;
103
- if (gate.result.details) {
104
- for (const detail of gate.result.details) {
105
- output += ` - ${detail}\n`;
106
- }
107
- }
108
- }
109
- return output;
110
- }
@@ -1,412 +0,0 @@
1
- /**
2
- * Workflow Execution Analyzer
3
- *
4
- * Deep analysis of workflow execution logic learned from Auto Builder insights:
5
- * - Loop detection (circular, infinite, re-entry)
6
- * - Multiple responder detection (causes duplicate/triple responses)
7
- * - Ungated catch-all detection
8
- * - Redundant classifier detection
9
- * - Data flow analysis
10
- * - runIf condition validation
11
- * - Parallel execution opportunities
12
- * - Dead code path detection
13
- */
14
- import { parseWorkflowDef } from "../knowledge.js";
15
- // ═══════════════════════════════════════════════════════════════════════════
16
- // GRAPH BUILDING
17
- // ═══════════════════════════════════════════════════════════════════════════
18
- function buildGraphMaps(nodes) {
19
- const forward = new Map();
20
- const reverse = new Map();
21
- const nodeMap = new Map();
22
- for (const node of nodes) {
23
- nodeMap.set(node.id, node);
24
- forward.set(node.id, new Set());
25
- reverse.set(node.id, new Set());
26
- }
27
- for (const node of nodes) {
28
- if (node.incoming_edges) {
29
- for (const edge of node.incoming_edges) {
30
- const sourceId = edge.source_node_id;
31
- forward.get(sourceId)?.add(node.id);
32
- reverse.get(node.id)?.add(sourceId);
33
- }
34
- }
35
- }
36
- return { forward, reverse, nodeMap };
37
- }
38
- // ═══════════════════════════════════════════════════════════════════════════
39
- // LOOP DETECTION
40
- // ═══════════════════════════════════════════════════════════════════════════
41
- export function detectLoops(workflowDef) {
42
- const nodes = parseWorkflowDef(workflowDef);
43
- const loops = [];
44
- const { forward, nodeMap } = buildGraphMaps(nodes);
45
- // DFS for cycle detection
46
- const visited = new Set();
47
- const recursionStack = new Set();
48
- const path = [];
49
- function dfs(nodeId) {
50
- visited.add(nodeId);
51
- recursionStack.add(nodeId);
52
- path.push(nodeId);
53
- for (const neighbor of (forward.get(nodeId) || [])) {
54
- if (!visited.has(neighbor)) {
55
- if (dfs(neighbor))
56
- return true;
57
- }
58
- else if (recursionStack.has(neighbor)) {
59
- const cycleStart = path.indexOf(neighbor);
60
- const cyclePath = path.slice(cycleStart);
61
- cyclePath.push(neighbor);
62
- loops.push({
63
- type: 'circular_dependency',
64
- nodes: cyclePath,
65
- description: `Circular dependency: ${cyclePath.join(' → ')}`,
66
- severity: 'critical',
67
- fixSuggestion: `Break the cycle by removing one edge.`,
68
- });
69
- return true;
70
- }
71
- }
72
- path.pop();
73
- recursionStack.delete(nodeId);
74
- return false;
75
- }
76
- const trigger = nodes.find(n => n.action_name === 'trigger' || n.id === 'trigger');
77
- if (trigger)
78
- dfs(trigger.id);
79
- for (const node of nodes) {
80
- if (!visited.has(node.id))
81
- dfs(node.id);
82
- }
83
- // Detect categorizer routing back to upstream (re-entry)
84
- for (const node of nodes) {
85
- if (node.action_name === 'chat_categorizer') {
86
- const downstream = forward.get(node.id) || new Set();
87
- const upstream = new Set();
88
- function findUpstream(nId, depth = 0) {
89
- if (depth > 20)
90
- return;
91
- const nodeInfo = nodeMap.get(nId);
92
- if (!nodeInfo?.incoming_edges)
93
- return;
94
- for (const edge of nodeInfo.incoming_edges) {
95
- upstream.add(edge.source_node_id);
96
- findUpstream(edge.source_node_id, depth + 1);
97
- }
98
- }
99
- findUpstream(node.id);
100
- for (const downNode of downstream) {
101
- if (upstream.has(downNode)) {
102
- loops.push({
103
- type: 'reentry_loop',
104
- nodes: [node.id, downNode],
105
- description: `Categorizer "${node.display_name || node.id}" routes to "${downNode}" which is upstream - can cause re-processing`,
106
- severity: 'warning',
107
- fixSuggestion: `Add state tracking or one-time-execution gate to prevent re-processing.`,
108
- });
109
- }
110
- }
111
- }
112
- }
113
- return loops;
114
- }
115
- // ═══════════════════════════════════════════════════════════════════════════
116
- // MULTIPLE RESPONDER DETECTION (Key cause of "says same thing 3 times")
117
- // ═══════════════════════════════════════════════════════════════════════════
118
- export function detectMultipleResponders(workflowDef) {
119
- const nodes = parseWorkflowDef(workflowDef);
120
- const issues = [];
121
- const { reverse, nodeMap } = buildGraphMaps(nodes);
122
- // Find all nodes that feed into WORKFLOW_OUTPUT
123
- const def = workflowDef;
124
- const resultMappings = def?.resultMappings;
125
- const outputFeeders = [];
126
- if (resultMappings) {
127
- for (const mapping of resultMappings) {
128
- const nodeName = mapping.namedOutput?.nodeName;
129
- if (nodeName)
130
- outputFeeders.push(nodeName);
131
- }
132
- }
133
- // Find responder-type nodes
134
- const responderNodes = nodes.filter(n => n.action_name === 'call_llm' ||
135
- n.action_name === 'custom_agent' ||
136
- n.action_name === 'respond_with_sources' ||
137
- n.action_name === 'fixed_response');
138
- const respondersFeedingOutput = responderNodes.filter(n => outputFeeders.includes(n.id));
139
- if (respondersFeedingOutput.length > 1) {
140
- const ungatedResponders = respondersFeedingOutput.filter(n => !n.runIf);
141
- const gatedResponders = respondersFeedingOutput.filter(n => n.runIf);
142
- if (ungatedResponders.length > 0 && gatedResponders.length > 0) {
143
- issues.push({
144
- type: 'competing_outputs',
145
- nodes: respondersFeedingOutput.map(n => n.id),
146
- description: `${ungatedResponders.length} ungated + ${gatedResponders.length} gated responders compete for output`,
147
- severity: 'critical',
148
- fixSuggestion: `Gate ALL responders with mutually exclusive conditions. Ungated: ${ungatedResponders.map(n => n.display_name || n.id).join(', ')}`,
149
- willCauseTripleResponse: ungatedResponders.length + gatedResponders.length >= 3,
150
- });
151
- }
152
- if (ungatedResponders.length > 1) {
153
- issues.push({
154
- type: 'parallel_responders',
155
- nodes: ungatedResponders.map(n => n.id),
156
- description: `${ungatedResponders.length} ungated responders fire simultaneously: ${ungatedResponders.map(n => n.display_name || n.id).join(', ')}`,
157
- severity: 'critical',
158
- fixSuggestion: `Add trigger_when conditions to make mutually exclusive, or consolidate into single responder`,
159
- willCauseTripleResponse: ungatedResponders.length >= 3,
160
- });
161
- }
162
- }
163
- // Find ALL ungated catch-all nodes (not just output feeders)
164
- for (const node of nodes) {
165
- if (node.action_name === 'trigger')
166
- continue;
167
- const isResponder = ['call_llm', 'custom_agent', 'respond_with_sources', 'fixed_response'].includes(node.action_name || '');
168
- if (!isResponder)
169
- continue;
170
- if (node.runIf)
171
- continue; // Has gating
172
- const upstream = reverse.get(node.id) || new Set();
173
- if (upstream.size > 0) {
174
- // Check if ALL upstream paths are ungated
175
- let allUpstreamUngated = true;
176
- for (const upId of upstream) {
177
- const upNode = nodeMap.get(upId);
178
- if (upNode?.runIf || upNode?.action_name === 'chat_categorizer') {
179
- allUpstreamUngated = false;
180
- break;
181
- }
182
- }
183
- if (allUpstreamUngated && !outputFeeders.includes(node.id)) {
184
- issues.push({
185
- type: 'ungated_responder',
186
- nodes: [node.id],
187
- description: `"${node.display_name || node.id}" always runs (no trigger_when gate)`,
188
- severity: 'warning',
189
- fixSuggestion: `Add runIf: { enum: { enumType: "category", enumValue: "specific_intent" } }`,
190
- willCauseTripleResponse: false,
191
- });
192
- }
193
- }
194
- }
195
- return issues;
196
- }
197
- // ═══════════════════════════════════════════════════════════════════════════
198
- // REDUNDANT CLASSIFIER DETECTION
199
- // ═══════════════════════════════════════════════════════════════════════════
200
- export function detectRedundantClassifiers(workflowDef) {
201
- const nodes = parseWorkflowDef(workflowDef);
202
- const issues = [];
203
- const classifiers = nodes.filter(n => n.action_name === 'chat_categorizer' ||
204
- n.action_name === 'intent_classifier');
205
- if (classifiers.length > 1) {
206
- // Group by input source
207
- const classifiersByInput = new Map();
208
- for (const classifier of classifiers) {
209
- const inputSources = [];
210
- if (classifier.incoming_edges) {
211
- for (const edge of classifier.incoming_edges) {
212
- if (edge.target_input === 'conversation' || edge.target_input === 'text_input') {
213
- inputSources.push(edge.source_node_id);
214
- }
215
- }
216
- }
217
- const key = inputSources.sort().join(',');
218
- const existing = classifiersByInput.get(key) || [];
219
- existing.push(classifier);
220
- classifiersByInput.set(key, existing);
221
- }
222
- for (const [, group] of classifiersByInput) {
223
- if (group.length > 1) {
224
- issues.push({
225
- classifiers: group.map(c => c.id),
226
- description: `${group.map(c => c.display_name || c.id).join(' & ')} analyze same input - causes overlapping/conflicting routing`,
227
- fixSuggestion: `Consolidate into single classifier, or chain sequentially with clear precedence`,
228
- });
229
- }
230
- }
231
- }
232
- return issues;
233
- }
234
- // ═══════════════════════════════════════════════════════════════════════════
235
- // DATA FLOW ISSUES
236
- // ═══════════════════════════════════════════════════════════════════════════
237
- export function analyzeDataFlow(workflowDef) {
238
- const nodes = parseWorkflowDef(workflowDef);
239
- const issues = [];
240
- const { forward, nodeMap } = buildGraphMaps(nodes);
241
- for (const node of nodes) {
242
- if (!node.incoming_edges)
243
- continue;
244
- for (const edge of node.incoming_edges) {
245
- const sourceNode = nodeMap.get(edge.source_node_id);
246
- if (!sourceNode) {
247
- issues.push({
248
- type: 'missing_data',
249
- node: node.id,
250
- description: `"${node.display_name || node.id}" expects data from non-existent "${edge.source_node_id}"`,
251
- severity: 'critical',
252
- fixSuggestion: `Add node "${edge.source_node_id}" or update input binding`,
253
- });
254
- }
255
- // Check if source is gated but target isn't
256
- if (sourceNode?.runIf && !node.runIf) {
257
- issues.push({
258
- type: 'gated_dependency',
259
- node: node.id,
260
- description: `"${node.display_name || node.id}" depends on gated "${sourceNode.display_name || sourceNode.id}" but has no gate itself - may not get data`,
261
- severity: 'warning',
262
- fixSuggestion: `Add matching runIf condition to "${node.display_name || node.id}"`,
263
- });
264
- }
265
- }
266
- }
267
- // Orphaned outputs
268
- for (const [nodeId, node] of nodeMap) {
269
- const downstream = forward.get(nodeId) || new Set();
270
- if (downstream.size === 0 &&
271
- nodeId !== 'WORKFLOW_OUTPUT' &&
272
- !['send_email_agent', 'send_communications_handler'].includes(node.action_name || '')) {
273
- const def = workflowDef;
274
- const resultMappings = def?.resultMappings;
275
- const isMapped = resultMappings?.some((m) => {
276
- const mapping = m;
277
- return mapping.namedOutput?.nodeName === nodeId;
278
- });
279
- if (!isMapped) {
280
- issues.push({
281
- type: 'orphaned_output',
282
- node: nodeId,
283
- description: `"${node.display_name || nodeId}" output is never consumed`,
284
- severity: 'warning',
285
- fixSuggestion: `Connect to downstream node or WORKFLOW_OUTPUT, or remove if unused`,
286
- });
287
- }
288
- }
289
- }
290
- return issues;
291
- }
292
- // ═══════════════════════════════════════════════════════════════════════════
293
- // DEAD CODE DETECTION
294
- // ═══════════════════════════════════════════════════════════════════════════
295
- export function findDeadCodePaths(workflowDef) {
296
- const nodes = parseWorkflowDef(workflowDef);
297
- const { forward } = buildGraphMaps(nodes);
298
- const trigger = nodes.find(n => n.action_name === 'trigger' || n.id === 'trigger');
299
- if (!trigger)
300
- return nodes.map(n => n.id);
301
- const reachable = new Set();
302
- const queue = [trigger.id];
303
- while (queue.length > 0) {
304
- const current = queue.shift();
305
- if (reachable.has(current))
306
- continue;
307
- reachable.add(current);
308
- for (const next of (forward.get(current) || [])) {
309
- queue.push(next);
310
- }
311
- }
312
- return nodes.filter(n => !reachable.has(n.id) && n.id !== 'WORKFLOW_OUTPUT').map(n => n.id);
313
- }
314
- // ═══════════════════════════════════════════════════════════════════════════
315
- // MAIN ANALYSIS FUNCTION
316
- // ═══════════════════════════════════════════════════════════════════════════
317
- export function analyzeExecutionFlow(workflowDef) {
318
- const nodes = parseWorkflowDef(workflowDef);
319
- const loops = detectLoops(workflowDef);
320
- const multipleResponderIssues = detectMultipleResponders(workflowDef);
321
- const redundantClassifiers = detectRedundantClassifiers(workflowDef);
322
- const dataFlowIssues = analyzeDataFlow(workflowDef);
323
- const deadCodePaths = findDeadCodePaths(workflowDef);
324
- const criticalIssues = loops.filter(l => l.severity === 'critical').length +
325
- multipleResponderIssues.filter(m => m.severity === 'critical').length +
326
- dataFlowIssues.filter(d => d.severity === 'critical').length;
327
- const summary = {
328
- totalNodes: nodes.length,
329
- reachableNodes: nodes.length - deadCodePaths.length,
330
- unreachableNodes: deadCodePaths.length,
331
- loopCount: loops.length,
332
- criticalIssues,
333
- warnings: loops.filter(l => l.severity === 'warning').length +
334
- multipleResponderIssues.filter(m => m.severity === 'warning').length +
335
- dataFlowIssues.filter(d => d.severity === 'warning').length,
336
- mayRepeatResponses: multipleResponderIssues.some(m => m.willCauseTripleResponse),
337
- ungatedResponderCount: multipleResponderIssues.filter(m => m.type === 'ungated_responder').length,
338
- redundantClassifierCount: redundantClassifiers.length,
339
- };
340
- return {
341
- loops,
342
- multipleResponderIssues,
343
- redundantClassifiers,
344
- dataFlowIssues,
345
- deadCodePaths,
346
- summary,
347
- };
348
- }
349
- // ═══════════════════════════════════════════════════════════════════════════
350
- // ASCII VISUALIZATION
351
- // ═══════════════════════════════════════════════════════════════════════════
352
- export function generateASCIIFlow(analysis) {
353
- let output = '';
354
- output += '╔══════════════════════════════════════════════════════════════════╗\n';
355
- output += '║ EXECUTION FLOW ANALYSIS (Auto Builder Insights) ║\n';
356
- output += '╚══════════════════════════════════════════════════════════════════╝\n\n';
357
- output += `📊 SUMMARY\n`;
358
- output += ` Total Nodes: ${analysis.summary.totalNodes}\n`;
359
- output += ` Reachable: ${analysis.summary.reachableNodes}\n`;
360
- output += ` Dead Code: ${analysis.summary.unreachableNodes}\n`;
361
- output += ` Critical Issues: ${analysis.summary.criticalIssues}\n`;
362
- output += ` Warnings: ${analysis.summary.warnings}\n`;
363
- output += ` May Repeat Responses: ${analysis.summary.mayRepeatResponses ? '⚠️ YES - TRIPLE RESPONSE RISK!' : '✓ No'}\n`;
364
- output += ` Ungated Responders: ${analysis.summary.ungatedResponderCount}\n`;
365
- output += ` Redundant Classifiers: ${analysis.summary.redundantClassifierCount}\n\n`;
366
- if (analysis.multipleResponderIssues.length > 0) {
367
- output += `🔊 DUPLICATE RESPONSE RISKS (${analysis.multipleResponderIssues.length})\n`;
368
- for (const issue of analysis.multipleResponderIssues) {
369
- const icon = issue.severity === 'critical' ? '❌' : '⚠️';
370
- output += ` ${icon} ${issue.type}\n`;
371
- output += ` ${issue.description}\n`;
372
- if (issue.willCauseTripleResponse)
373
- output += ` ⚡ WILL CAUSE TRIPLE RESPONSE!\n`;
374
- output += ` Fix: ${issue.fixSuggestion}\n\n`;
375
- }
376
- }
377
- if (analysis.redundantClassifiers.length > 0) {
378
- output += `🔀 REDUNDANT CLASSIFIERS (${analysis.redundantClassifiers.length})\n`;
379
- for (const issue of analysis.redundantClassifiers) {
380
- output += ` ⚠️ ${issue.classifiers.join(', ')}\n`;
381
- output += ` ${issue.description}\n`;
382
- output += ` Fix: ${issue.fixSuggestion}\n\n`;
383
- }
384
- }
385
- if (analysis.loops.length > 0) {
386
- output += `🔄 LOOPS (${analysis.loops.length})\n`;
387
- for (const loop of analysis.loops) {
388
- const icon = loop.severity === 'critical' ? '❌' : '⚠️';
389
- output += ` ${icon} ${loop.type}: ${loop.description}\n`;
390
- output += ` Fix: ${loop.fixSuggestion}\n\n`;
391
- }
392
- }
393
- if (analysis.deadCodePaths.length > 0) {
394
- output += `💀 DEAD CODE (${analysis.deadCodePaths.length})\n`;
395
- output += ` Unreachable: ${analysis.deadCodePaths.slice(0, 5).join(', ')}`;
396
- if (analysis.deadCodePaths.length > 5)
397
- output += ` +${analysis.deadCodePaths.length - 5} more`;
398
- output += '\n\n';
399
- }
400
- if (analysis.dataFlowIssues.length > 0) {
401
- output += `📦 DATA FLOW (${analysis.dataFlowIssues.length})\n`;
402
- for (const issue of analysis.dataFlowIssues.slice(0, 5)) {
403
- const icon = issue.severity === 'critical' ? '❌' : '⚠️';
404
- output += ` ${icon} ${issue.type} at "${issue.node}"\n`;
405
- output += ` ${issue.description}\n\n`;
406
- }
407
- if (analysis.dataFlowIssues.length > 5) {
408
- output += ` ... +${analysis.dataFlowIssues.length - 5} more\n`;
409
- }
410
- }
411
- return output;
412
- }