@ema.co/mcp-toolkit 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/dist/mcp/handlers-consolidated.js +400 -14
- package/dist/mcp/prompts.js +80 -123
- package/dist/mcp/server.js +134 -209
- package/dist/mcp/tools-consolidated.js +212 -150
- package/dist/sdk/action-registry.js +128 -0
- package/dist/sdk/client.js +58 -90
- package/dist/sdk/demo-generator.js +978 -0
- package/dist/sdk/generated/api-types.js +11 -0
- package/dist/sdk/index.js +15 -1
- package/dist/sdk/knowledge.js +38 -8
- package/dist/sdk/quality-gates.js +386 -0
- package/dist/sdk/structural-rules.js +290 -0
- package/dist/sdk/workflow-generator.js +187 -39
- package/dist/sdk/workflow-intent.js +246 -24
- package/dist/sdk/workflow-optimizer.js +665 -0
- package/dist/sdk/workflow-tracer.js +648 -0
- package/dist/sdk/workflow-transformer.js +10 -0
- package/dist/sdk/workflow-validator.js +391 -0
- package/docs/.temp/datasource-attach.har +198369 -0
- package/docs/.temp/grpcweb.gar +1 -0
- package/docs/local-generation.md +508 -0
- package/docs/mcp-flow-diagram.md +135 -0
- package/docs/mcp-tools-guide.md +163 -197
- package/docs/openapi.json +8000 -0
- package/docs/release-process.md +153 -0
- package/docs/test-persona-creation.md +196 -0
- package/docs/tool-consolidation-proposal.md +166 -378
- package/package.json +3 -1
- package/resources/templates/demo-scenarios/README.md +63 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated TypeScript types from Ema Platform OpenAPI spec.
|
|
3
|
+
* Generated from: Ema Platform API v1.0.0
|
|
4
|
+
* Generated at: 2026-01-16T13:46:50.729Z
|
|
5
|
+
*
|
|
6
|
+
* DO NOT EDIT MANUALLY - regenerate with: npm run generate:types
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// API Operation Types
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
package/dist/sdk/index.js
CHANGED
|
@@ -33,8 +33,14 @@ export {
|
|
|
33
33
|
compileWorkflow,
|
|
34
34
|
// Settings builders
|
|
35
35
|
buildVoiceConfig, buildChatConfig, } from "./workflow-generator.js";
|
|
36
|
+
// Action Registry (API-driven action/template definitions)
|
|
37
|
+
export { ActionRegistry, ensureActionRegistry, parseActionDefinition, parseTemplateDefinition, } from "./action-registry.js";
|
|
36
38
|
// Workflow Intent (Normalization layer)
|
|
37
|
-
export { parseInput, validateIntent, intentToSpec, detectInputType, parseNaturalLanguage, parsePartialSpec,
|
|
39
|
+
export { parseInput, validateIntent, intentToSpec, detectInputType, parseNaturalLanguage, parsePartialSpec,
|
|
40
|
+
// LLM-driven generation
|
|
41
|
+
needsLLMGeneration, generateWorkflow, parseWorkflowSpecFromLLM, } from "./workflow-intent.js";
|
|
42
|
+
// Workflow Validator (API-driven validation)
|
|
43
|
+
export { APISchemaRegistry, ensureSchemaRegistry, getSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM, generateTemplateCatalogForLLM, } from "./workflow-validator.js";
|
|
38
44
|
// Generation Schema (Compact format for LLM-based generation)
|
|
39
45
|
export { generateSchema, generateSchemaMarkdown, buildCompactAgents, buildTypeRules, buildConstraints, getAgentSchema, isTypeCompatible, getRecommendedInput, } from "./generation-schema.js";
|
|
40
46
|
// Version Tracking (Persona version history management)
|
|
@@ -49,3 +55,11 @@ export { VersionPolicyEngine, createVersionPolicyEngine, } from "./version-polic
|
|
|
49
55
|
export { analyzeExecutionFlow, detectLoops, detectMultipleResponders, detectRedundantClassifiers, analyzeDataFlow, findDeadCodePaths, generateASCIIFlow, } from "./workflow-execution-analyzer.js";
|
|
50
56
|
// Workflow Fixer (Auto-fix including multiple responder issues)
|
|
51
57
|
export { autoFixWorkflow, suggestFixes, } from "./workflow-fixer.js";
|
|
58
|
+
// Workflow Optimizer (Health scoring & optimization recommendations)
|
|
59
|
+
export { analyzeOptimizations, summarizeOptimizationReport, } from "./workflow-optimizer.js";
|
|
60
|
+
// Workflow Tracer (Flow visualization & path analysis)
|
|
61
|
+
export { traceWorkflow, generateDetailedTrace, formatFlowTrace, formatDetailedTrace, } from "./workflow-tracer.js";
|
|
62
|
+
// Quality Gates (Pre-deploy validation)
|
|
63
|
+
export { runQualityGates, canDeploy, getQualityGates, formatQualityReport, } from "./quality-gates.js";
|
|
64
|
+
// Structural Rules (LLM validation context)
|
|
65
|
+
export { STRUCTURAL_RULES_FOR_LLM, STRUCTURAL_INVARIANTS, EXECUTION_RULES, COMMON_STRUCTURAL_MISTAKES, getAllStructuralRules, getInvariantById, getCriticalInvariants, } from "./structural-rules.js";
|
package/dist/sdk/knowledge.js
CHANGED
|
@@ -1019,6 +1019,7 @@ export const COMMON_MISTAKES = [
|
|
|
1019
1019
|
{ mistake: "Missing Fallback category", problem: "Workflow validation fails, unhandled intents", solution: "ALWAYS include Fallback category in every categorizer" },
|
|
1020
1020
|
{ mistake: "Type mismatches in connections", problem: "Validation errors, runtime failures", solution: "Check type compatibility: use conversation_to_search_query when needed" },
|
|
1021
1021
|
{ mistake: "Not mapping to WORKFLOW_OUTPUT", problem: "Responses don't reach user", solution: "Ensure ALL paths terminate at WORKFLOW_OUTPUT" },
|
|
1022
|
+
{ mistake: "Using workflow_def instead of workflow in updates", problem: "API accepts request but changes silently don't persist", solution: "Use 'workflow' field (not 'workflow_def') in updateAiEmployee(). GET returns workflow_def, UPDATE expects workflow." },
|
|
1022
1023
|
];
|
|
1023
1024
|
export const DEBUG_CHECKLIST = [
|
|
1024
1025
|
{ step: 1, action: "Check Status", description: "Is the AI Employee active/ready?", apiField: "status" },
|
|
@@ -2072,17 +2073,34 @@ function detectCategorizerIssues(nodes) {
|
|
|
2072
2073
|
reason: `Categorizer "${categorizer.id}" has no outgoing category edges - routing won't work`,
|
|
2073
2074
|
});
|
|
2074
2075
|
}
|
|
2075
|
-
// Check for Fallback -
|
|
2076
|
+
// Check for Fallback - multiple ways to configure:
|
|
2077
|
+
// 1. default_category input set to a fallback-like value
|
|
2078
|
+
// 2. Outgoing edges with "fallback" in the source_output
|
|
2079
|
+
// 3. Category handlers wired to fallback output
|
|
2076
2080
|
let hasFallback = false;
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2081
|
+
// Check default_category input (most common way to set fallback)
|
|
2082
|
+
const params = categorizer.parameters;
|
|
2083
|
+
if (params?.default_category) {
|
|
2084
|
+
const defaultCat = params.default_category;
|
|
2085
|
+
const defaultValue = defaultCat?.inline?.enumValue?.toLowerCase() || "";
|
|
2086
|
+
if (defaultValue && ["fallback", "other", "general", "unknown", "default"].includes(defaultValue)) {
|
|
2087
|
+
hasFallback = true;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
// Also check for edges with fallback in the name
|
|
2091
|
+
if (!hasFallback) {
|
|
2092
|
+
for (const node of nodes) {
|
|
2093
|
+
if (node.incoming_edges) {
|
|
2094
|
+
for (const edge of node.incoming_edges) {
|
|
2095
|
+
if (edge.source_node_id === categorizer.id &&
|
|
2096
|
+
(edge.source_output?.toLowerCase().includes("fallback"))) {
|
|
2097
|
+
hasFallback = true;
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2084
2100
|
}
|
|
2085
2101
|
}
|
|
2102
|
+
if (hasFallback)
|
|
2103
|
+
break;
|
|
2086
2104
|
}
|
|
2087
2105
|
}
|
|
2088
2106
|
if (!hasFallback && outgoing.size > 0) {
|
|
@@ -2191,10 +2209,22 @@ function detectWrongInputSource(nodes) {
|
|
|
2191
2209
|
const rule = findInputSourceRule(actionName);
|
|
2192
2210
|
if (!rule)
|
|
2193
2211
|
continue;
|
|
2212
|
+
// For send_email nodes, the rule only applies to email_to/recipient inputs
|
|
2213
|
+
// Other inputs (subject, body) can legitimately use LLM/text outputs
|
|
2214
|
+
const isEmailNode = actionName.includes("email") || actionName.includes("send_email");
|
|
2194
2215
|
if (node.incoming_edges) {
|
|
2195
2216
|
for (const edge of node.incoming_edges) {
|
|
2196
2217
|
const sourceOutput = edge.source_output?.toLowerCase() ?? "";
|
|
2197
2218
|
const targetInput = edge.target_input?.toLowerCase() ?? "";
|
|
2219
|
+
// For email nodes, only check the email_to/recipient input, not all inputs
|
|
2220
|
+
if (isEmailNode) {
|
|
2221
|
+
const isRecipientInput = targetInput.includes("email_to") ||
|
|
2222
|
+
targetInput.includes("to_email") ||
|
|
2223
|
+
targetInput.includes("recipient") ||
|
|
2224
|
+
targetInput.includes("to_address");
|
|
2225
|
+
if (!isRecipientInput)
|
|
2226
|
+
continue; // Skip non-recipient inputs for email nodes
|
|
2227
|
+
}
|
|
2198
2228
|
// Check if using an avoided input
|
|
2199
2229
|
for (const avoid of rule.avoid) {
|
|
2200
2230
|
if (sourceOutput.includes(avoid.toLowerCase()) ||
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality Gates - Pre-Deploy Validation
|
|
3
|
+
*
|
|
4
|
+
* Ensures workflows meet minimum quality standards before deployment.
|
|
5
|
+
* These are the "guardrails" that prevent broken workflows from going live.
|
|
6
|
+
*/
|
|
7
|
+
import { detectWorkflowIssues } from "./knowledge.js";
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Quality Gate Definitions
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
const QUALITY_GATES = [
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// CRITICAL GATES (Blocking)
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
{
|
|
16
|
+
id: "has_trigger",
|
|
17
|
+
name: "Has Entry Point",
|
|
18
|
+
description: "Workflow must have a trigger node as entry point",
|
|
19
|
+
blocking: true,
|
|
20
|
+
severity: "critical",
|
|
21
|
+
check: (workflow) => {
|
|
22
|
+
const actions = (workflow.actions || []);
|
|
23
|
+
const hasTrigger = actions.some(a => {
|
|
24
|
+
const actionName = a.action?.name;
|
|
25
|
+
const name = (actionName?.name || "").toLowerCase();
|
|
26
|
+
return name.includes("trigger");
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
passed: hasTrigger,
|
|
30
|
+
message: hasTrigger
|
|
31
|
+
? "Workflow has a valid trigger"
|
|
32
|
+
: "Missing trigger node - workflow has no entry point",
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "has_output",
|
|
38
|
+
name: "Has Workflow Output",
|
|
39
|
+
description: "Workflow must have at least one WORKFLOW_OUTPUT defined",
|
|
40
|
+
blocking: true,
|
|
41
|
+
severity: "critical",
|
|
42
|
+
check: (workflow) => {
|
|
43
|
+
const results = workflow.results;
|
|
44
|
+
const hasOutput = results && Object.keys(results).length > 0;
|
|
45
|
+
return {
|
|
46
|
+
passed: !!hasOutput,
|
|
47
|
+
message: hasOutput
|
|
48
|
+
? `Workflow has ${Object.keys(results).length} output(s) defined`
|
|
49
|
+
: "No WORKFLOW_OUTPUT defined - responses won't reach users",
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "no_critical_issues",
|
|
55
|
+
name: "No Critical Issues",
|
|
56
|
+
description: "Workflow must not have any critical structural issues",
|
|
57
|
+
blocking: true,
|
|
58
|
+
severity: "critical",
|
|
59
|
+
check: (workflow) => {
|
|
60
|
+
const issues = detectWorkflowIssues(workflow);
|
|
61
|
+
const criticalIssues = issues.filter((i) => i.severity === "critical");
|
|
62
|
+
return {
|
|
63
|
+
passed: criticalIssues.length === 0,
|
|
64
|
+
message: criticalIssues.length === 0
|
|
65
|
+
? "No critical issues detected"
|
|
66
|
+
: `${criticalIssues.length} critical issue(s) found`,
|
|
67
|
+
details: criticalIssues.map((i) => `${i.type}: ${i.reason}`),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "all_paths_reach_output",
|
|
73
|
+
name: "All Paths Reach Output",
|
|
74
|
+
description: "Every branch from trigger must eventually reach WORKFLOW_OUTPUT",
|
|
75
|
+
blocking: true,
|
|
76
|
+
severity: "critical",
|
|
77
|
+
check: (workflow) => {
|
|
78
|
+
const actions = (workflow.actions || []);
|
|
79
|
+
const results = workflow.results;
|
|
80
|
+
if (!results || Object.keys(results).length === 0) {
|
|
81
|
+
return { passed: false, message: "No outputs to check" };
|
|
82
|
+
}
|
|
83
|
+
// Get all output nodes
|
|
84
|
+
const outputNodes = new Set(Object.values(results).map((r) => r.actionName));
|
|
85
|
+
// Build reverse adjacency (who feeds into whom)
|
|
86
|
+
const feedsInto = new Map();
|
|
87
|
+
for (const action of actions) {
|
|
88
|
+
const name = action.name;
|
|
89
|
+
feedsInto.set(name, new Set());
|
|
90
|
+
const inputs = action.inputs;
|
|
91
|
+
if (inputs) {
|
|
92
|
+
for (const binding of Object.values(inputs)) {
|
|
93
|
+
const bindingObj = binding;
|
|
94
|
+
const actionOutput = bindingObj?.actionOutput;
|
|
95
|
+
if (actionOutput?.actionName) {
|
|
96
|
+
const source = actionOutput.actionName;
|
|
97
|
+
if (!feedsInto.has(source)) {
|
|
98
|
+
feedsInto.set(source, new Set());
|
|
99
|
+
}
|
|
100
|
+
feedsInto.get(source).add(name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Check if each output node is reachable from trigger
|
|
106
|
+
const reachableFromTrigger = new Set();
|
|
107
|
+
const queue = ["trigger"];
|
|
108
|
+
while (queue.length > 0) {
|
|
109
|
+
const current = queue.shift();
|
|
110
|
+
if (reachableFromTrigger.has(current))
|
|
111
|
+
continue;
|
|
112
|
+
reachableFromTrigger.add(current);
|
|
113
|
+
const children = feedsInto.get(current) || new Set();
|
|
114
|
+
for (const child of children) {
|
|
115
|
+
if (!reachableFromTrigger.has(child)) {
|
|
116
|
+
queue.push(child);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const unreachableOutputs = Array.from(outputNodes).filter(n => !reachableFromTrigger.has(n));
|
|
121
|
+
return {
|
|
122
|
+
passed: unreachableOutputs.length === 0,
|
|
123
|
+
message: unreachableOutputs.length === 0
|
|
124
|
+
? "All output nodes are reachable from trigger"
|
|
125
|
+
: `${unreachableOutputs.length} output node(s) unreachable from trigger`,
|
|
126
|
+
details: unreachableOutputs.length > 0
|
|
127
|
+
? [`Unreachable: ${unreachableOutputs.join(", ")}`]
|
|
128
|
+
: undefined,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
+
// WARNING GATES (Non-blocking but important)
|
|
134
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
135
|
+
{
|
|
136
|
+
id: "hitl_for_side_effects",
|
|
137
|
+
name: "HITL for Side Effects",
|
|
138
|
+
description: "Actions with external side effects should have human approval",
|
|
139
|
+
blocking: false,
|
|
140
|
+
severity: "warning",
|
|
141
|
+
check: (workflow) => {
|
|
142
|
+
const actions = (workflow.actions || []);
|
|
143
|
+
const sideEffectNodes = actions.filter(a => {
|
|
144
|
+
const actionName = a.action?.name;
|
|
145
|
+
const name = (actionName?.name || "").toLowerCase();
|
|
146
|
+
return name.includes("email") ||
|
|
147
|
+
name.includes("external") ||
|
|
148
|
+
name.includes("create_") ||
|
|
149
|
+
name.includes("update_") ||
|
|
150
|
+
name.includes("delete_");
|
|
151
|
+
});
|
|
152
|
+
const hitlNodes = new Set(actions
|
|
153
|
+
.filter(a => {
|
|
154
|
+
const actionName = a.action?.name;
|
|
155
|
+
const name = (actionName?.name || "").toLowerCase();
|
|
156
|
+
return name.includes("hitl");
|
|
157
|
+
})
|
|
158
|
+
.map(a => a.name));
|
|
159
|
+
// Check if side effect nodes have HITL upstream
|
|
160
|
+
const unprotected = sideEffectNodes.filter(a => {
|
|
161
|
+
const inputs = a.inputs;
|
|
162
|
+
if (!inputs)
|
|
163
|
+
return true;
|
|
164
|
+
// Check if any input comes from a HITL node
|
|
165
|
+
for (const binding of Object.values(inputs)) {
|
|
166
|
+
const bindingObj = binding;
|
|
167
|
+
const actionOutput = bindingObj?.actionOutput;
|
|
168
|
+
if (actionOutput?.actionName && hitlNodes.has(actionOutput.actionName)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
passed: unprotected.length === 0,
|
|
176
|
+
message: unprotected.length === 0
|
|
177
|
+
? "All side-effect actions have HITL protection"
|
|
178
|
+
: `${unprotected.length} side-effect action(s) without HITL approval`,
|
|
179
|
+
details: unprotected.map(a => `${a.name}: Consider adding human approval before this action`),
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "categorizer_has_fallback",
|
|
185
|
+
name: "Categorizer Has Fallback",
|
|
186
|
+
description: "Intent categorizers should have a Fallback category",
|
|
187
|
+
blocking: false,
|
|
188
|
+
severity: "warning",
|
|
189
|
+
check: (workflow) => {
|
|
190
|
+
const enumTypes = (workflow.enumTypes || []);
|
|
191
|
+
const missingFallback = enumTypes.filter(e => {
|
|
192
|
+
if (!e.values)
|
|
193
|
+
return false;
|
|
194
|
+
return !e.values.some(v => v.name.toLowerCase() === "fallback");
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
passed: missingFallback.length === 0,
|
|
198
|
+
message: missingFallback.length === 0
|
|
199
|
+
? "All categorizers have Fallback category"
|
|
200
|
+
: `${missingFallback.length} categorizer(s) missing Fallback`,
|
|
201
|
+
details: missingFallback.map(e => `${e.name}: Add Fallback category for unmatched intents`),
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "no_orphan_nodes",
|
|
207
|
+
name: "No Orphan Nodes",
|
|
208
|
+
description: "All nodes should be connected to the workflow",
|
|
209
|
+
blocking: false,
|
|
210
|
+
severity: "warning",
|
|
211
|
+
check: (workflow) => {
|
|
212
|
+
const issues = detectWorkflowIssues(workflow);
|
|
213
|
+
const orphans = issues.filter((i) => i.type === "orphan");
|
|
214
|
+
return {
|
|
215
|
+
passed: orphans.length === 0,
|
|
216
|
+
message: orphans.length === 0
|
|
217
|
+
? "No orphan nodes detected"
|
|
218
|
+
: `${orphans.length} orphan node(s) found`,
|
|
219
|
+
details: orphans.map((i) => `${i.node}: ${i.reason}`),
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
+
// INFO GATES (Best practices)
|
|
225
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
226
|
+
{
|
|
227
|
+
id: "reasonable_complexity",
|
|
228
|
+
name: "Reasonable Complexity",
|
|
229
|
+
description: "Workflow should not be overly complex",
|
|
230
|
+
blocking: false,
|
|
231
|
+
severity: "info",
|
|
232
|
+
check: (workflow) => {
|
|
233
|
+
const actions = (workflow.actions || []);
|
|
234
|
+
const nodeCount = actions.length;
|
|
235
|
+
// Calculate branching factor
|
|
236
|
+
let branches = 0;
|
|
237
|
+
for (const action of actions) {
|
|
238
|
+
const runIf = action.runIf;
|
|
239
|
+
if (runIf)
|
|
240
|
+
branches++;
|
|
241
|
+
}
|
|
242
|
+
const isComplex = nodeCount > 20 || branches > 10;
|
|
243
|
+
return {
|
|
244
|
+
passed: !isComplex,
|
|
245
|
+
message: isComplex
|
|
246
|
+
? `High complexity: ${nodeCount} nodes, ${branches} conditional branches`
|
|
247
|
+
: `Reasonable complexity: ${nodeCount} nodes, ${branches} branches`,
|
|
248
|
+
details: isComplex
|
|
249
|
+
? ["Consider breaking into smaller sub-workflows for maintainability"]
|
|
250
|
+
: undefined,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
256
|
+
// Main Entry Point
|
|
257
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
258
|
+
/**
|
|
259
|
+
* Run all quality gates on a workflow
|
|
260
|
+
*/
|
|
261
|
+
export function runQualityGates(workflow) {
|
|
262
|
+
const blockingFailures = [];
|
|
263
|
+
const warnings = [];
|
|
264
|
+
const passed = [];
|
|
265
|
+
for (const gate of QUALITY_GATES) {
|
|
266
|
+
const result = gate.check(workflow);
|
|
267
|
+
const report = {
|
|
268
|
+
gate_id: gate.id,
|
|
269
|
+
gate_name: gate.name,
|
|
270
|
+
result,
|
|
271
|
+
severity: gate.severity,
|
|
272
|
+
};
|
|
273
|
+
if (!result.passed) {
|
|
274
|
+
if (gate.blocking) {
|
|
275
|
+
blockingFailures.push(report);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
warnings.push(report);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
passed.push(report);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const overallStatus = blockingFailures.length > 0
|
|
286
|
+
? "fail"
|
|
287
|
+
: warnings.length > 0
|
|
288
|
+
? "warning"
|
|
289
|
+
: "pass";
|
|
290
|
+
const summary = generateSummary(blockingFailures, warnings, passed);
|
|
291
|
+
return {
|
|
292
|
+
overall_status: overallStatus,
|
|
293
|
+
blocking_failures: blockingFailures,
|
|
294
|
+
warnings,
|
|
295
|
+
passed,
|
|
296
|
+
summary,
|
|
297
|
+
deploy_allowed: blockingFailures.length === 0,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Quick check if a workflow passes all blocking gates
|
|
302
|
+
*/
|
|
303
|
+
export function canDeploy(workflow) {
|
|
304
|
+
for (const gate of QUALITY_GATES) {
|
|
305
|
+
if (gate.blocking) {
|
|
306
|
+
const result = gate.check(workflow);
|
|
307
|
+
if (!result.passed) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get list of all quality gates
|
|
316
|
+
*/
|
|
317
|
+
export function getQualityGates() {
|
|
318
|
+
return [...QUALITY_GATES];
|
|
319
|
+
}
|
|
320
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
321
|
+
// Helpers
|
|
322
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
323
|
+
function generateSummary(failures, warnings, passed) {
|
|
324
|
+
const parts = [];
|
|
325
|
+
if (failures.length > 0) {
|
|
326
|
+
parts.push(`🔴 ${failures.length} BLOCKING failure(s): ${failures.map(f => f.gate_name).join(", ")}`);
|
|
327
|
+
}
|
|
328
|
+
if (warnings.length > 0) {
|
|
329
|
+
parts.push(`🟡 ${warnings.length} warning(s): ${warnings.map(w => w.gate_name).join(", ")}`);
|
|
330
|
+
}
|
|
331
|
+
if (passed.length > 0) {
|
|
332
|
+
parts.push(`🟢 ${passed.length} check(s) passed`);
|
|
333
|
+
}
|
|
334
|
+
if (failures.length === 0) {
|
|
335
|
+
parts.push("✅ Workflow is ready for deployment");
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
parts.push("❌ Deployment blocked - fix critical issues first");
|
|
339
|
+
}
|
|
340
|
+
return parts.join("\n");
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get a human-readable quality report
|
|
344
|
+
*/
|
|
345
|
+
export function formatQualityReport(report) {
|
|
346
|
+
const lines = [];
|
|
347
|
+
lines.push("## Quality Gate Report");
|
|
348
|
+
lines.push("");
|
|
349
|
+
lines.push(`**Status**: ${report.overall_status.toUpperCase()}`);
|
|
350
|
+
lines.push(`**Deploy Allowed**: ${report.deploy_allowed ? "Yes ✅" : "No ❌"}`);
|
|
351
|
+
lines.push("");
|
|
352
|
+
if (report.blocking_failures.length > 0) {
|
|
353
|
+
lines.push("### 🔴 Blocking Failures");
|
|
354
|
+
for (const failure of report.blocking_failures) {
|
|
355
|
+
lines.push(`- **${failure.gate_name}**: ${failure.result.message}`);
|
|
356
|
+
if (failure.result.details) {
|
|
357
|
+
for (const detail of failure.result.details) {
|
|
358
|
+
lines.push(` - ${detail}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
lines.push("");
|
|
363
|
+
}
|
|
364
|
+
if (report.warnings.length > 0) {
|
|
365
|
+
lines.push("### 🟡 Warnings");
|
|
366
|
+
for (const warning of report.warnings) {
|
|
367
|
+
lines.push(`- **${warning.gate_name}**: ${warning.result.message}`);
|
|
368
|
+
if (warning.result.details) {
|
|
369
|
+
for (const detail of warning.result.details) {
|
|
370
|
+
lines.push(` - ${detail}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
lines.push("");
|
|
375
|
+
}
|
|
376
|
+
if (report.passed.length > 0) {
|
|
377
|
+
lines.push("### 🟢 Passed");
|
|
378
|
+
for (const pass of report.passed) {
|
|
379
|
+
lines.push(`- ${pass.gate_name}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
lines.push("");
|
|
383
|
+
lines.push("---");
|
|
384
|
+
lines.push(report.summary);
|
|
385
|
+
return lines.join("\n");
|
|
386
|
+
}
|