Skip to main content
Glama
coverage-guardrail.js12.6 kB
#!/usr/bin/env node /** * Enhanced Coverage Guardrail * Enforces component-specific coverage thresholds and provides actionable recommendations * * Environment Variables: * COVERAGE_MIN_LINES (default: 40 - Phase 1 target) * COVERAGE_MIN_BRANCHES (default: 30 - Phase 1 target) * COVERAGE_MIN_FUNCTIONS (default: 35 - Phase 1 target) * COVERAGE_MIN_STATEMENTS (default: 38 - Phase 1 target) * COVERAGE_PHASE (1|2|3 - selects threshold phase) * COVERAGE_STRICT (true|false - enforce component thresholds) */ import fs from "fs"; import path from "path"; // Phase-based thresholds const PHASE_THRESHOLDS = { 1: { lines: 40, branches: 30, functions: 35, statements: 38 }, 2: { lines: 55, branches: 45, functions: 50, statements: 53 }, 3: { lines: 70, branches: 65, functions: 70, statements: 68 }, }; // Component-specific thresholds (Phase 1) const COMPONENT_THRESHOLDS = { "src/utils/validation.ts": { lines: 85, branches: 80, functions: 90, statements: 85 }, "src/utils/error.ts": { lines: 100, branches: 100, functions: 100, statements: 100 }, "src/utils/toolWrapper.ts": { lines: 78, branches: 75, functions: 80, statements: 78 }, "src/client/api.ts": { lines: 45, branches: 40, functions: 50, statements: 45 }, "src/config/": { lines: 55, branches: 50, functions: 60, statements: 55 }, "src/tools/": { lines: 25, branches: 20, functions: 25, statements: 25 }, }; const phase = parseInt(process.env.COVERAGE_PHASE || "1"); const globalThresholds = PHASE_THRESHOLDS[phase] || PHASE_THRESHOLDS[1]; const strictMode = process.env.COVERAGE_STRICT === "true"; const lineThreshold = parseFloat(process.env.COVERAGE_MIN_LINES || globalThresholds.lines.toString()); const branchThreshold = parseFloat(process.env.COVERAGE_MIN_BRANCHES || globalThresholds.branches.toString()); const functionThreshold = parseFloat(process.env.COVERAGE_MIN_FUNCTIONS || globalThresholds.functions.toString()); const statementThreshold = parseFloat(process.env.COVERAGE_MIN_STATEMENTS || globalThresholds.statements.toString()); const reportPath = path.resolve("coverage/coverage-final.json"); // Enhanced error handling with helpful instructions if (!fs.existsSync(reportPath)) { console.error("❌ Coverage report not found at", reportPath); console.error("💡 Run 'npm test -- --coverage' to generate coverage report first"); process.exit(1); } let json; try { json = JSON.parse(fs.readFileSync(reportPath, "utf-8")); } catch (e) { console.error("❌ Unable to parse coverage report:", e.message); console.error("💡 Try deleting the coverage directory and regenerating: rm -rf coverage && npm test -- --coverage"); process.exit(1); } // Calculate global coverage let linesTotal = 0, linesCovered = 0; let branchesTotal = 0, branchesCovered = 0; let funcsTotal = 0, funcsCovered = 0; let statementsTotal = 0, statementsCovered = 0; // Component-specific coverage tracking const componentCoverage = new Map(); for (const [filePath, file] of Object.entries(json)) { // Global aggregation - handle both new and legacy Istanbul formats if (file.lines) { // New format with lines/branches/functions/statements objects linesTotal += file.lines.total || 0; linesCovered += file.lines.covered || 0; } else if (file.s && file.statementMap) { // Legacy format with s/f/b arrays and maps const stmtMap = file.statementMap; const stmtHits = file.s; linesTotal += Object.keys(stmtMap).length; linesCovered += Object.values(stmtHits).filter((hits) => hits > 0).length; } if (file.branches) { branchesTotal += file.branches.total || 0; branchesCovered += file.branches.covered || 0; } else if (file.b && file.branchMap) { const branchMap = file.branchMap; const branchHits = file.b; for (const branchId in branchMap) { const branch = branchMap[branchId]; const hits = branchHits[branchId] || []; branchesTotal += branch.locations ? branch.locations.length : hits.length; branchesCovered += hits.filter((hit) => hit > 0).length; } } if (file.functions) { funcsTotal += file.functions.total || 0; funcsCovered += file.functions.covered || 0; } else if (file.f && file.fnMap) { const fnMap = file.fnMap; const fnHits = file.f; funcsTotal += Object.keys(fnMap).length; funcsCovered += Object.values(fnHits).filter((hits) => hits > 0).length; } if (file.statements) { statementsTotal += file.statements.total || 0; statementsCovered += file.statements.covered || 0; } else if (file.s && file.statementMap) { // Same as lines for legacy format const stmtMap = file.statementMap; const stmtHits = file.s; statementsTotal += Object.keys(stmtMap).length; statementsCovered += Object.values(stmtHits).filter((hits) => hits > 0).length; } // Component-specific tracking const normalizedPath = filePath.replace(process.cwd(), "").replace(/\\/g, "/").replace(/^\//, ""); // Calculate percentages for component tracking let linePct = 0, branchPct = 0, funcPct = 0, stmtPct = 0; if (file.lines) { linePct = file.lines.total ? (file.lines.covered / file.lines.total) * 100 : 0; } else if (file.s && file.statementMap) { const total = Object.keys(file.statementMap).length; const covered = Object.values(file.s).filter((hits) => hits > 0).length; linePct = total ? (covered / total) * 100 : 0; } if (file.branches) { branchPct = file.branches.total ? (file.branches.covered / file.branches.total) * 100 : 0; } else if (file.b && file.branchMap) { let totalBranches = 0, coveredBranches = 0; for (const branchId in file.branchMap) { const branch = file.branchMap[branchId]; const hits = file.b[branchId] || []; totalBranches += branch.locations ? branch.locations.length : hits.length; coveredBranches += hits.filter((hit) => hit > 0).length; } branchPct = totalBranches ? (coveredBranches / totalBranches) * 100 : 0; } if (file.functions) { funcPct = file.functions.total ? (file.functions.covered / file.functions.total) * 100 : 0; } else if (file.f && file.fnMap) { const total = Object.keys(file.fnMap).length; const covered = Object.values(file.f).filter((hits) => hits > 0).length; funcPct = total ? (covered / total) * 100 : 0; } if (file.statements) { stmtPct = file.statements.total ? (file.statements.covered / file.statements.total) * 100 : 0; } else if (file.s && file.statementMap) { const total = Object.keys(file.statementMap).length; const covered = Object.values(file.s).filter((hits) => hits > 0).length; stmtPct = total ? (covered / total) * 100 : 0; } componentCoverage.set(normalizedPath, { lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct, }); } const linePct = linesTotal ? (linesCovered / linesTotal) * 100 : 0; const branchPct = branchesTotal ? (branchesCovered / branchesTotal) * 100 : 0; const funcPct = funcsTotal ? (funcsCovered / funcsTotal) * 100 : 0; const statementPct = statementsTotal ? (statementsCovered / statementsTotal) * 100 : 0; // Global threshold validation const globalFailures = []; if (linePct < lineThreshold) globalFailures.push(`Lines ${linePct.toFixed(2)}% < ${lineThreshold}%`); if (branchPct < branchThreshold) globalFailures.push(`Branches ${branchPct.toFixed(2)}% < ${branchThreshold}%`); if (funcPct < functionThreshold) globalFailures.push(`Functions ${funcPct.toFixed(2)}% < ${functionThreshold}%`); if (statementPct < statementThreshold) globalFailures.push(`Statements ${statementPct.toFixed(2)}% < ${statementThreshold}%`); // Component-specific threshold validation const componentFailures = []; const recommendations = []; if (strictMode) { for (const [pattern, thresholds] of Object.entries(COMPONENT_THRESHOLDS)) { const matchingFiles = Array.from(componentCoverage.entries()).filter(([filePath]) => { if (pattern.endsWith("/")) { return filePath.startsWith(pattern); } return filePath === pattern; }); for (const [filePath, coverage] of matchingFiles) { if (coverage.lines < thresholds.lines) { componentFailures.push(`${filePath}: Lines ${coverage.lines.toFixed(1)}% < ${thresholds.lines}%`); } if (coverage.branches < thresholds.branches) { componentFailures.push(`${filePath}: Branches ${coverage.branches.toFixed(1)}% < ${thresholds.branches}%`); } if (coverage.functions < thresholds.functions) { componentFailures.push(`${filePath}: Functions ${coverage.functions.toFixed(1)}% < ${thresholds.functions}%`); } if (coverage.statements < thresholds.statements) { componentFailures.push( `${filePath}: Statements ${coverage.statements.toFixed(1)}% < ${thresholds.statements}%`, ); } } } } // Generate improvement recommendations if (globalFailures.length > 0) { recommendations.push("🎯 Priority improvements needed:"); // Find lowest coverage files const sortedFiles = Array.from(componentCoverage.entries()) .filter(([filePath]) => filePath.startsWith("src/")) .sort(([, a], [, b]) => a.lines - b.lines) .slice(0, 5); for (const [filePath, coverage] of sortedFiles) { if (coverage.lines < lineThreshold) { recommendations.push(` • ${filePath}: ${coverage.lines.toFixed(1)}% lines coverage`); } } } const allFailures = [...globalFailures, ...componentFailures]; const result = { status: allFailures.length ? "failed" : "passed", phase: phase, strictMode: strictMode, thresholds: { global: { lines: lineThreshold, branches: branchThreshold, functions: functionThreshold, statements: statementThreshold, }, components: strictMode ? COMPONENT_THRESHOLDS : null, }, coverage: { global: { lines: parseFloat(linePct.toFixed(2)), branches: parseFloat(branchPct.toFixed(2)), functions: parseFloat(funcPct.toFixed(2)), statements: parseFloat(statementPct.toFixed(2)), }, components: Object.fromEntries( Array.from(componentCoverage.entries()) .filter(([path]) => path.startsWith("src/")) .map(([path, cov]) => [ path, { lines: parseFloat(cov.lines.toFixed(1)), branches: parseFloat(cov.branches.toFixed(1)), functions: parseFloat(cov.functions.toFixed(1)), statements: parseFloat(cov.statements.toFixed(1)), }, ]), ), }, failures: { global: globalFailures, components: componentFailures, }, recommendations: recommendations, summary: { totalFiles: componentCoverage.size, srcFiles: Array.from(componentCoverage.keys()).filter((p) => p.startsWith("src/")).length, failedComponents: componentFailures.length, phase: `Phase ${phase} (Target: ${globalThresholds.lines}% lines)`, }, }; // Output results if (allFailures.length > 0) { console.error(`❌ Coverage guardrail failed (Phase ${phase}${strictMode ? " + Strict" : ""})`); console.error(`\nGlobal Failures: ${globalFailures.length ? globalFailures.join("; ") : "None"}`); if (componentFailures.length > 0) { console.error(`\nComponent Failures: ${componentFailures.length}`); componentFailures.slice(0, 10).forEach((failure) => console.error(` • ${failure}`)); if (componentFailures.length > 10) { console.error(` ... and ${componentFailures.length - 10} more`); } } if (recommendations.length > 0) { console.error("\n" + recommendations.join("\n")); } console.error("\n📊 Current Coverage:"); console.error(` Lines: ${linePct.toFixed(2)}% (target: ${lineThreshold}%)`); console.error(` Branches: ${branchPct.toFixed(2)}% (target: ${branchThreshold}%)`); console.error(` Functions: ${funcPct.toFixed(2)}% (target: ${functionThreshold}%)`); console.error(` Statements: ${statementPct.toFixed(2)}% (target: ${statementThreshold}%)`); // Machine-readable output for CI/CD console.error("\n" + JSON.stringify(result)); process.exit(1); } console.log(`✅ Coverage guardrail passed (Phase ${phase}${strictMode ? " + Strict" : ""})`); console.log( `📊 Coverage: Lines ${linePct.toFixed(2)}%, Branches ${branchPct.toFixed(2)}%, Functions ${funcPct.toFixed( 2, )}%, Statements ${statementPct.toFixed(2)}%`, ); console.log( `🎯 Progress toward Phase ${phase} targets: ${((linePct / globalThresholds.lines) * 100).toFixed(1)}% complete`, ); // Machine-readable success output console.log("\n" + JSON.stringify(result));

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server