mega-minds
Version:
Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration
1,139 lines (979 loc) โข 41.7 kB
JavaScript
// lib/quality/QualityGateManager.js
// Automated Quality Gates Engine - Core Feature #4 from PRD
// Built-in testing, code review, and security validation with 85%+ pass rate
const fs = require('fs-extra');
const path = require('path');
const { spawn } = require('child_process');
/**
* Manages automated quality gates for code quality, testing, and security
* PRD Requirements: 85%+ automated quality gate pass rate, <30s basic validation
*/
class QualityGateManager {
constructor(projectPath = process.cwd()) {
this.projectPath = projectPath;
this.qualityConfigFile = path.join(projectPath, '.mega-minds', 'quality', 'quality-config.json');
this.qualityReportsDir = path.join(projectPath, '.mega-minds', 'quality', 'reports');
this.qualityHistoryFile = path.join(projectPath, '.mega-minds', 'quality', 'quality-history.json');
// PRD-defined quality gates
this.availableGates = {
'code-quality': {
name: 'Code Quality Analysis',
description: 'ESLint, Prettier, and code complexity analysis',
priority: 'high',
timeoutMs: 15000, // 15 seconds
tools: ['eslint', 'prettier', 'jshint']
},
'security': {
name: 'Security Vulnerability Scan',
description: 'Security vulnerability and dependency scanning',
priority: 'critical',
timeoutMs: 20000, // 20 seconds
tools: ['npm-audit', 'semgrep', 'snyk']
},
'testing': {
name: 'Automated Testing',
description: 'Unit, integration, and basic e2e tests',
priority: 'high',
timeoutMs: 60000, // 60 seconds for tests
tools: ['jest', 'mocha', 'vitest', 'playwright']
},
'performance': {
name: 'Performance Analysis',
description: 'Bundle size and basic performance metrics',
priority: 'medium',
timeoutMs: 30000, // 30 seconds
tools: ['webpack-bundle-analyzer', 'lighthouse-ci']
},
'accessibility': {
name: 'Accessibility Check',
description: 'Basic accessibility compliance',
priority: 'medium',
timeoutMs: 15000, // 15 seconds
tools: ['axe-core', 'pa11y']
}
};
this.qualityHistory = [];
this.config = {
enabledGates: ['code-quality', 'security', 'testing'], // Default gates
blockingGates: ['security'], // Gates that must pass
warningGates: ['code-quality', 'accessibility'], // Gates that warn but don't block
autoFix: true, // Auto-fix issues when possible
reportLevel: 'detailed', // summary, detailed, verbose
maxRetries: 2,
parallelExecution: true
};
this.initialized = false;
}
/**
* Initialize quality gate system
*/
async initialize() {
if (this.initialized) return;
console.log('๐ก๏ธ Initializing Quality Gate Manager...');
// Ensure directories exist
await fs.ensureDir(path.dirname(this.qualityConfigFile));
await fs.ensureDir(this.qualityReportsDir);
// Load configuration
await this.loadConfiguration();
// Load quality history
await this.loadQualityHistory();
// Detect available tools
await this.detectAvailableTools();
this.initialized = true;
console.log('โ
Quality Gate Manager ready');
console.log(` Enabled gates: ${this.config.enabledGates.join(', ')}`);
console.log(` Blocking gates: ${this.config.blockingGates.join(', ')}`);
}
/**
* Load quality gate configuration
*/
async loadConfiguration() {
try {
if (await fs.pathExists(this.qualityConfigFile)) {
const savedConfig = await fs.readJSON(this.qualityConfigFile);
this.config = { ...this.config, ...savedConfig };
} else {
// Create default configuration
await this.saveConfiguration();
}
} catch (error) {
console.warn('โ ๏ธ Could not load quality configuration, using defaults:', error.message);
}
}
/**
* Save quality gate configuration
*/
async saveConfiguration() {
try {
await fs.writeJSON(this.qualityConfigFile, this.config, { spaces: 2 });
} catch (error) {
console.warn('โ ๏ธ Could not save quality configuration:', error.message);
}
}
/**
* Load quality history for metrics and trends
*/
async loadQualityHistory() {
try {
if (await fs.pathExists(this.qualityHistoryFile)) {
this.qualityHistory = await fs.readJSON(this.qualityHistoryFile);
// Keep only last 100 quality runs for performance
if (this.qualityHistory.length > 100) {
this.qualityHistory = this.qualityHistory.slice(-100);
}
}
} catch (error) {
console.warn('โ ๏ธ Could not load quality history:', error.message);
this.qualityHistory = [];
}
}
/**
* Save quality history
*/
async saveQualityHistory() {
try {
await fs.writeJSON(this.qualityHistoryFile, this.qualityHistory, { spaces: 2 });
} catch (error) {
console.warn('โ ๏ธ Could not save quality history:', error.message);
}
}
/**
* Detect available quality tools in the project
*/
async detectAvailableTools() {
const availableTools = new Map();
// Check package.json for tools
const packageJsonPath = path.join(this.projectPath, 'package.json');
if (await fs.pathExists(packageJsonPath)) {
try {
const packageJson = await fs.readJSON(packageJsonPath);
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
// Map dependencies to tools
const toolMap = {
'eslint': 'eslint',
'prettier': 'prettier',
'jest': 'jest',
'@jest/core': 'jest',
'mocha': 'mocha',
'vitest': 'vitest',
'playwright': 'playwright',
'@playwright/test': 'playwright',
'cypress': 'cypress',
'axe-core': 'axe-core',
'pa11y': 'pa11y',
'lighthouse': 'lighthouse-ci',
'semgrep': 'semgrep',
'snyk': 'snyk'
};
for (const [dep, tool] of Object.entries(toolMap)) {
if (allDeps[dep]) {
availableTools.set(tool, {
installed: true,
version: allDeps[dep],
configPath: await this.findToolConfig(tool)
});
}
}
// Always available: npm audit
availableTools.set('npm-audit', {
installed: true,
version: 'built-in',
configPath: null
});
} catch (error) {
console.warn('โ ๏ธ Could not analyze package.json for tools:', error.message);
}
}
this.availableTools = availableTools;
console.log(`๐ Detected ${availableTools.size} quality tools`);
}
/**
* Find configuration file for a specific tool
*/
async findToolConfig(toolName) {
const configFiles = {
'eslint': ['.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', '.eslintrc.yaml', 'eslint.config.js'],
'prettier': ['.prettierrc', '.prettierrc.json', '.prettierrc.yml', '.prettierrc.yaml', 'prettier.config.js'],
'jest': ['jest.config.js', 'jest.config.json', 'jest.config.ts'],
'vitest': ['vitest.config.js', 'vitest.config.ts', 'vite.config.js'],
'playwright': ['playwright.config.js', 'playwright.config.ts']
};
if (configFiles[toolName]) {
for (const configFile of configFiles[toolName]) {
const configPath = path.join(this.projectPath, configFile);
if (await fs.pathExists(configPath)) {
return configPath;
}
}
}
return null;
}
/**
* Run quality gates for specified changes or entire project
* PRD Requirement: <30 seconds for basic validation, 85%+ pass rate
*/
async runQualityGates(options = {}) {
await this.initialize();
const startTime = Date.now();
const runId = `quality-${Date.now()}`;
const qualityRun = {
id: runId,
timestamp: new Date().toISOString(),
startTime: startTime,
options: options,
gates: {},
overall: {
passed: false,
score: 0,
blockers: [],
warnings: [],
errors: []
}
};
console.log(`๐ก๏ธ Running quality gates (ID: ${runId})...`);
try {
const gatesToRun = options.gates || this.config.enabledGates;
const results = new Map();
// Run gates in parallel if enabled
if (this.config.parallelExecution && gatesToRun.length > 1) {
const gatePromises = gatesToRun.map(gateName =>
this.runSingleGate(gateName, options)
);
const gateResults = await Promise.all(gatePromises);
gatesToRun.forEach((gateName, index) => {
results.set(gateName, gateResults[index]);
});
} else {
// Run gates sequentially
for (const gateName of gatesToRun) {
const result = await this.runSingleGate(gateName, options);
results.set(gateName, result);
}
}
// Process results
let totalScore = 0;
let passedCount = 0;
for (const [gateName, result] of results.entries()) {
qualityRun.gates[gateName] = result;
if (result.passed) {
passedCount++;
}
totalScore += result.score;
// Check for blockers
if (this.config.blockingGates.includes(gateName) && !result.passed) {
qualityRun.overall.blockers.push({
gate: gateName,
message: `Blocking gate failed: ${result.message}`,
severity: 'critical'
});
}
// Check for warnings
if (this.config.warningGates.includes(gateName) && !result.passed) {
qualityRun.overall.warnings.push({
gate: gateName,
message: `Warning gate failed: ${result.message}`,
severity: 'warning'
});
}
// Collect errors
if (result.errors && result.errors.length > 0) {
qualityRun.overall.errors.push(...result.errors);
}
}
// Calculate overall results
qualityRun.overall.score = Math.round(totalScore / gatesToRun.length);
qualityRun.overall.passed = qualityRun.overall.blockers.length === 0;
qualityRun.overall.passRate = Math.round((passedCount / gatesToRun.length) * 100);
const endTime = Date.now();
qualityRun.duration = endTime - startTime;
qualityRun.endTime = endTime;
// Log results
this.logQualityResults(qualityRun);
// Save to history
this.qualityHistory.push(qualityRun);
await this.saveQualityHistory();
// Generate report
await this.generateQualityReport(qualityRun);
console.log(`โ
Quality gates completed in ${qualityRun.duration}ms`);
console.log(`๐ Overall score: ${qualityRun.overall.score}/100 (${qualityRun.overall.passRate}% pass rate)`);
// PRD compliance check
if (qualityRun.duration > 30000) {
console.warn(`โ ๏ธ Quality gates exceeded 30s target: ${qualityRun.duration}ms`);
}
if (qualityRun.overall.passRate < 85) {
console.warn(`โ ๏ธ Pass rate below 85% target: ${qualityRun.overall.passRate}%`);
}
return qualityRun;
} catch (error) {
qualityRun.overall.errors.push({
type: 'system-error',
message: error.message,
severity: 'critical'
});
console.error('โ Quality gates failed with system error:', error.message);
return qualityRun;
}
}
/**
* Run a single quality gate
*/
async runSingleGate(gateName, options = {}) {
const gate = this.availableGates[gateName];
if (!gate) {
return {
passed: false,
score: 0,
message: `Unknown gate: ${gateName}`,
errors: [{ type: 'config-error', message: `Gate '${gateName}' not found` }]
};
}
const gateStartTime = Date.now();
console.log(` ๐ Running ${gate.name}...`);
try {
let result;
switch (gateName) {
case 'code-quality':
result = await this.runCodeQualityGate(options);
break;
case 'security':
result = await this.runSecurityGate(options);
break;
case 'testing':
result = await this.runTestingGate(options);
break;
case 'performance':
result = await this.runPerformanceGate(options);
break;
case 'accessibility':
result = await this.runAccessibilityGate(options);
break;
default:
result = {
passed: false,
score: 0,
message: `Gate '${gateName}' not implemented`,
errors: [{ type: 'not-implemented', message: `Gate '${gateName}' not implemented` }]
};
}
const gateEndTime = Date.now();
result.duration = gateEndTime - gateStartTime;
result.gateName = gateName;
// Check timeout
if (result.duration > gate.timeoutMs) {
result.timedOut = true;
result.passed = false;
result.message = `${result.message} (timed out after ${result.duration}ms)`;
}
console.log(` ${result.passed ? 'โ
' : 'โ'} ${gate.name}: ${result.score}/100 (${result.duration}ms)`);
return result;
} catch (error) {
const gateEndTime = Date.now();
return {
passed: false,
score: 0,
message: `Gate '${gateName}' failed: ${error.message}`,
duration: gateEndTime - gateStartTime,
gateName: gateName,
errors: [{ type: 'execution-error', message: error.message }]
};
}
}
/**
* Run code quality analysis (ESLint, Prettier, etc.)
*/
async runCodeQualityGate(options = {}) {
const results = {
passed: true,
score: 100,
message: 'Code quality checks passed',
issues: [],
tools: {},
errors: []
};
try {
// ESLint check
if (this.availableTools.has('eslint')) {
const eslintResult = await this.runEslint(options);
results.tools.eslint = eslintResult;
if (eslintResult.issues > 0) {
const severityScore = Math.max(0, 100 - (eslintResult.issues * 5));
results.score = Math.min(results.score, severityScore);
results.issues.push(...eslintResult.details || []);
if (eslintResult.errors > 0) {
results.passed = false;
results.message = `ESLint found ${eslintResult.errors} error(s)`;
}
}
}
// Prettier check
if (this.availableTools.has('prettier')) {
const prettierResult = await this.runPrettier(options);
results.tools.prettier = prettierResult;
if (!prettierResult.formatted) {
results.score = Math.min(results.score, 90); // Minor penalty for formatting
results.issues.push('Code formatting issues detected');
if (this.config.autoFix && !options.dryRun) {
console.log(' ๐ง Auto-fixing formatting issues...');
await this.runPrettier({ ...options, fix: true });
results.message = 'Code formatting auto-fixed';
}
}
}
// Basic complexity analysis
const complexityResult = await this.analyzeCodeComplexity(options);
results.tools.complexity = complexityResult;
if (complexityResult.highComplexity > 0) {
results.score = Math.min(results.score, 80);
results.issues.push(`${complexityResult.highComplexity} functions with high complexity`);
}
if (results.score < 70) {
results.passed = false;
results.message = `Code quality score too low: ${results.score}/100`;
}
} catch (error) {
results.passed = false;
results.score = 0;
results.message = `Code quality analysis failed: ${error.message}`;
results.errors.push({ type: 'analysis-error', message: error.message });
}
return results;
}
/**
* Run security vulnerability scanning
*/
async runSecurityGate(options = {}) {
const results = {
passed: true,
score: 100,
message: 'Security checks passed',
vulnerabilities: [],
tools: {},
errors: []
};
try {
// npm audit (always available)
const auditResult = await this.runNpmAudit(options);
results.tools.npmAudit = auditResult;
if (auditResult.vulnerabilities.length > 0) {
const criticalVulns = auditResult.vulnerabilities.filter(v => v.severity === 'critical').length;
const highVulns = auditResult.vulnerabilities.filter(v => v.severity === 'high').length;
// Critical vulnerabilities = blocking
if (criticalVulns > 0) {
results.passed = false;
results.score = 0;
results.message = `${criticalVulns} critical security vulnerabilities found`;
} else if (highVulns > 0) {
results.score = Math.max(0, 100 - (highVulns * 20));
results.message = `${highVulns} high-severity vulnerabilities found`;
} else {
results.score = Math.max(70, 100 - (auditResult.vulnerabilities.length * 5));
}
results.vulnerabilities = auditResult.vulnerabilities;
}
// Additional security tools if available
if (this.availableTools.has('semgrep')) {
const semgrepResult = await this.runSemgrep(options);
results.tools.semgrep = semgrepResult;
if (semgrepResult.findings.length > 0) {
results.score = Math.min(results.score, 90);
results.vulnerabilities.push(...semgrepResult.findings);
}
}
} catch (error) {
results.passed = false;
results.score = 0;
results.message = `Security analysis failed: ${error.message}`;
results.errors.push({ type: 'security-error', message: error.message });
}
return results;
}
/**
* Run automated testing
*/
async runTestingGate(options = {}) {
const results = {
passed: true,
score: 100,
message: 'All tests passed',
testResults: {},
coverage: null,
tools: {},
errors: []
};
try {
// Detect and run available test frameworks
const testFrameworks = ['jest', 'mocha', 'vitest'];
let testFrameworkFound = false;
for (const framework of testFrameworks) {
if (this.availableTools.has(framework)) {
console.log(` ๐งช Running ${framework} tests...`);
const testResult = await this.runTestFramework(framework, options);
results.tools[framework] = testResult;
testFrameworkFound = true;
if (!testResult.passed) {
results.passed = false;
results.score = Math.min(results.score, 50);
results.message = `${testResult.failed} test(s) failed`;
}
if (testResult.coverage) {
results.coverage = testResult.coverage;
// Coverage scoring
if (testResult.coverage.percentage < 80) {
results.score = Math.min(results.score, 85);
results.message = `Low test coverage: ${testResult.coverage.percentage}%`;
}
}
break; // Use only the first available framework
}
}
if (!testFrameworkFound) {
results.passed = false;
results.score = 0;
results.message = 'No test framework detected';
results.errors.push({ type: 'no-tests', message: 'No test framework found' });
}
} catch (error) {
results.passed = false;
results.score = 0;
results.message = `Testing failed: ${error.message}`;
results.errors.push({ type: 'test-error', message: error.message });
}
return results;
}
/**
* Run performance analysis
*/
async runPerformanceGate(options = {}) {
return {
passed: true,
score: 90,
message: 'Performance analysis not fully implemented',
tools: {},
errors: []
};
}
/**
* Run accessibility checks
*/
async runAccessibilityGate(options = {}) {
return {
passed: true,
score: 85,
message: 'Accessibility checks not fully implemented',
tools: {},
errors: []
};
}
// Helper methods for running specific tools
async runEslint(options = {}) {
return new Promise((resolve) => {
const args = ['.', '--format', 'json'];
if (options.fix) args.push('--fix');
const eslint = spawn('npx', ['eslint', ...args], {
cwd: this.projectPath,
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
eslint.stdout.on('data', (data) => {
stdout += data.toString();
});
eslint.stderr.on('data', (data) => {
stderr += data.toString();
});
eslint.on('close', (code) => {
try {
const results = JSON.parse(stdout);
const totalIssues = results.reduce((sum, file) => sum + file.errorCount + file.warningCount, 0);
const totalErrors = results.reduce((sum, file) => sum + file.errorCount, 0);
resolve({
exitCode: code,
issues: totalIssues,
errors: totalErrors,
details: results,
stderr: stderr
});
} catch (parseError) {
resolve({
exitCode: code,
issues: code > 0 ? 1 : 0,
errors: code > 0 ? 1 : 0,
details: null,
stderr: stderr || parseError.message
});
}
});
// Timeout after 15 seconds
setTimeout(() => {
eslint.kill();
resolve({
exitCode: -1,
issues: 1,
errors: 1,
details: null,
stderr: 'ESLint timed out'
});
}, 15000);
});
}
async runPrettier(options = {}) {
return new Promise((resolve) => {
const args = ['.', '--check'];
if (options.fix) {
args.splice(-1, 1, '--write'); // Replace --check with --write
}
const prettier = spawn('npx', ['prettier', ...args], {
cwd: this.projectPath,
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
prettier.stdout.on('data', (data) => {
stdout += data.toString();
});
prettier.stderr.on('data', (data) => {
stderr += data.toString();
});
prettier.on('close', (code) => {
resolve({
exitCode: code,
formatted: code === 0,
stdout: stdout,
stderr: stderr
});
});
// Timeout after 10 seconds
setTimeout(() => {
prettier.kill();
resolve({
exitCode: -1,
formatted: false,
stderr: 'Prettier timed out'
});
}, 10000);
});
}
async runNpmAudit(options = {}) {
return new Promise((resolve) => {
const audit = spawn('npm', ['audit', '--json'], {
cwd: this.projectPath,
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
audit.stdout.on('data', (data) => {
stdout += data.toString();
});
audit.stderr.on('data', (data) => {
stderr += data.toString();
});
audit.on('close', (code) => {
try {
const auditResult = JSON.parse(stdout);
const vulnerabilities = [];
if (auditResult.vulnerabilities) {
for (const [name, vuln] of Object.entries(auditResult.vulnerabilities)) {
vulnerabilities.push({
name: name,
severity: vuln.severity,
title: vuln.title,
url: vuln.url
});
}
}
resolve({
exitCode: code,
vulnerabilities: vulnerabilities,
summary: auditResult.metadata || {}
});
} catch (parseError) {
resolve({
exitCode: code,
vulnerabilities: [],
summary: {},
error: parseError.message
});
}
});
// Timeout after 20 seconds
setTimeout(() => {
audit.kill();
resolve({
exitCode: -1,
vulnerabilities: [],
summary: {},
error: 'npm audit timed out'
});
}, 20000);
});
}
async runTestFramework(framework, options = {}) {
return new Promise((resolve) => {
let command, args;
switch (framework) {
case 'jest':
command = 'npx';
args = ['jest', '--json', '--coverage'];
break;
case 'vitest':
command = 'npx';
args = ['vitest', 'run', '--reporter=json'];
break;
case 'mocha':
command = 'npx';
args = ['mocha', '--reporter', 'json'];
break;
default:
resolve({
passed: false,
error: `Unknown test framework: ${framework}`
});
return;
}
const testProcess = spawn(command, args, {
cwd: this.projectPath,
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
testProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
testProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
testProcess.on('close', (code) => {
try {
const testResult = JSON.parse(stdout);
// Parse results based on framework
let parsedResult;
if (framework === 'jest') {
parsedResult = this.parseJestResults(testResult);
} else {
parsedResult = {
passed: code === 0,
total: 0,
passed_tests: 0,
failed: 0,
coverage: null
};
}
resolve({
...parsedResult,
exitCode: code,
stderr: stderr
});
} catch (parseError) {
resolve({
passed: code === 0,
total: 0,
passed_tests: 0,
failed: code > 0 ? 1 : 0,
coverage: null,
exitCode: code,
error: parseError.message,
stderr: stderr
});
}
});
// Timeout after 60 seconds
setTimeout(() => {
testProcess.kill();
resolve({
passed: false,
total: 0,
passed_tests: 0,
failed: 1,
coverage: null,
exitCode: -1,
error: 'Tests timed out'
});
}, 60000);
});
}
parseJestResults(jestResult) {
const total = jestResult.numTotalTests || 0;
const passed = jestResult.numPassedTests || 0;
const failed = jestResult.numFailedTests || 0;
let coverage = null;
if (jestResult.coverageMap) {
const coverageData = jestResult.coverageMap;
let totalLines = 0;
let coveredLines = 0;
for (const file of Object.values(coverageData)) {
if (file.s) { // Statement coverage
totalLines += Object.keys(file.s).length;
coveredLines += Object.values(file.s).filter(count => count > 0).length;
}
}
coverage = {
percentage: totalLines > 0 ? Math.round((coveredLines / totalLines) * 100) : 0,
lines: { total: totalLines, covered: coveredLines }
};
}
return {
passed: failed === 0,
total: total,
passed_tests: passed,
failed: failed,
coverage: coverage
};
}
async analyzeCodeComplexity(options = {}) {
// Basic complexity analysis - count function length and nesting
const files = await this.getSourceFiles();
let highComplexity = 0;
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf8');
const functionMatches = content.match(/function\s+\w+|=>\s*{|\w+\s*=\s*function/g) || [];
// Simple complexity heuristic: count nested blocks
const blockDepth = (content.match(/{/g) || []).length - (content.match(/}/g) || []).length;
if (Math.abs(blockDepth) > 10) {
highComplexity++;
}
} catch (error) {
// Skip files that can't be read
}
}
return {
filesAnalyzed: files.length,
highComplexity: highComplexity
};
}
async getSourceFiles() {
const extensions = ['.js', '.ts', '.jsx', '.tsx'];
const files = [];
const walkDir = async (dir) => {
try {
const items = await fs.readdir(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = await fs.stat(fullPath);
if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
await walkDir(fullPath);
} else if (stat.isFile() && extensions.includes(path.extname(item))) {
files.push(fullPath);
}
}
} catch (error) {
// Skip directories that can't be read
}
};
await walkDir(this.projectPath);
return files;
}
logQualityResults(qualityRun) {
console.log('\n๐ Quality Gate Results:');
console.log('========================');
for (const [gateName, result] of Object.entries(qualityRun.gates)) {
const status = result.passed ? 'โ
PASS' : 'โ FAIL';
console.log(`${status} ${gateName}: ${result.score}/100 (${result.duration}ms)`);
if (!result.passed && result.message) {
console.log(` ${result.message}`);
}
}
console.log(`\nOverall: ${qualityRun.overall.passed ? 'โ
PASS' : 'โ FAIL'} - ${qualityRun.overall.score}/100`);
if (qualityRun.overall.blockers.length > 0) {
console.log('\n๐ซ Blockers:');
qualityRun.overall.blockers.forEach(blocker => {
console.log(` โข ${blocker.message}`);
});
}
if (qualityRun.overall.warnings.length > 0) {
console.log('\nโ ๏ธ Warnings:');
qualityRun.overall.warnings.forEach(warning => {
console.log(` โข ${warning.message}`);
});
}
}
async generateQualityReport(qualityRun) {
const reportFile = path.join(this.qualityReportsDir, `quality-${qualityRun.id}.json`);
await fs.writeJSON(reportFile, qualityRun, { spaces: 2 });
// Generate HTML report if detailed reporting is enabled
if (this.config.reportLevel === 'detailed' || this.config.reportLevel === 'verbose') {
const htmlReportFile = path.join(this.qualityReportsDir, `quality-${qualityRun.id}.html`);
const htmlContent = this.generateHtmlReport(qualityRun);
await fs.writeFile(htmlReportFile, htmlContent);
console.log(`๐ Quality report saved: ${htmlReportFile}`);
}
}
generateHtmlReport(qualityRun) {
// Simple HTML report generation
return `<!DOCTYPE html>
<html>
<head>
<title>Quality Gate Report - ${qualityRun.id}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.pass { color: green; }
.fail { color: red; }
.summary { background: #f5f5f5; padding: 15px; border-radius: 5px; }
</style>
</head>
<body>
<h1>Quality Gate Report</h1>
<div class="summary">
<h2>Summary</h2>
<p><strong>Overall Status:</strong> <span class="${qualityRun.overall.passed ? 'pass' : 'fail'}">${qualityRun.overall.passed ? 'PASS' : 'FAIL'}</span></p>
<p><strong>Score:</strong> ${qualityRun.overall.score}/100</p>
<p><strong>Duration:</strong> ${qualityRun.duration}ms</p>
<p><strong>Pass Rate:</strong> ${qualityRun.overall.passRate}%</p>
</div>
<h2>Gate Results</h2>
${Object.entries(qualityRun.gates).map(([gate, result]) => `
<div>
<h3 class="${result.passed ? 'pass' : 'fail'}">${gate}: ${result.passed ? 'PASS' : 'FAIL'} (${result.score}/100)</h3>
<p>${result.message}</p>
<p><small>Duration: ${result.duration}ms</small></p>
</div>
`).join('')}
</body>
</html>`;
}
/**
* Get quality metrics for PRD compliance monitoring
*/
async getQualityMetrics() {
await this.initialize();
const recentRuns = this.qualityHistory.slice(-20); // Last 20 runs
if (recentRuns.length === 0) {
return {
totalRuns: 0,
averageScore: 0,
passRate: 0,
averageDuration: 0,
prdCompliance: {
passRateTarget: '85%',
responseTimeTarget: '30s',
status: 'no-data'
}
};
}
const totalScore = recentRuns.reduce((sum, run) => sum + run.overall.score, 0);
const passedRuns = recentRuns.filter(run => run.overall.passed).length;
const totalDuration = recentRuns.reduce((sum, run) => sum + run.duration, 0);
const metrics = {
totalRuns: this.qualityHistory.length,
recentRuns: recentRuns.length,
averageScore: Math.round(totalScore / recentRuns.length),
passRate: Math.round((passedRuns / recentRuns.length) * 100),
averageDuration: Math.round(totalDuration / recentRuns.length),
lastRun: recentRuns[recentRuns.length - 1]?.timestamp,
// PRD compliance
prdCompliance: {
passRateTarget: '85%',
responseTimeTarget: '30s',
meetsPassRate: (passedRuns / recentRuns.length) >= 0.85,
meetsResponseTime: (totalDuration / recentRuns.length) <= 30000,
status: 'compliant'
}
};
// Determine compliance status
if (!metrics.prdCompliance.meetsPassRate || !metrics.prdCompliance.meetsResponseTime) {
metrics.prdCompliance.status = 'needs-improvement';
}
return metrics;
}
}
module.exports = QualityGateManager;