claude-flow
Version:
Enterprise-grade AI agent orchestration with WASM-powered ReasoningBank memory and AgentDB vector database (always uses latest agentic-flow)
692 lines (586 loc) ⢠23.7 kB
text/typescript
#!/usr/bin/env -S deno run --allow-all
/**
* Advanced coverage analysis and reporting
*/
import { parseArgs } from "https://deno.land/std@0.220.0/cli/parse_args.ts";
import { exists } from "https://deno.land/std@0.220.0/fs/exists.ts";
import { ensureDir } from "https://deno.land/std@0.220.0/fs/ensure_dir.ts";
import { walk } from "https://deno.land/std@0.220.0/fs/walk.ts";
interface CoverageData {
url: string;
ranges: Array<{
start: { line: number; col: number };
end: { line: number; col: number };
}>;
}
interface FileCoverage {
path: string;
totalLines: number;
coveredLines: number;
uncoveredLines: number[];
coverage: number;
functions: FunctionCoverage[];
}
interface FunctionCoverage {
name: string;
line: number;
covered: boolean;
}
interface CoverageReport {
timestamp: string;
summary: {
totalFiles: number;
totalLines: number;
coveredLines: number;
coverage: number;
thresholds: {
statements: number;
branches: number;
functions: number;
lines: number;
};
passed: boolean;
};
files: FileCoverage[];
uncoveredFiles: string[];
}
class CoverageAnalyzer {
private sourceDir: string;
private coverageDir: string;
private outputDir: string;
private thresholds: {
statements: number;
branches: number;
functions: number;
lines: number;
};
constructor(options: {
sourceDir: string;
coverageDir: string;
outputDir: string;
thresholds: {
statements: number;
branches: number;
functions: number;
lines: number;
};
}) {
this.sourceDir = options.sourceDir;
this.coverageDir = options.coverageDir;
this.outputDir = options.outputDir;
this.thresholds = options.thresholds;
}
async generateReport(): Promise<CoverageReport> {
console.log("š Analyzing coverage data...");
await ensureDir(this.outputDir);
// Load coverage data
const coverageData = await this.loadCoverageData();
// Get all source files
const sourceFiles = await this.getSourceFiles();
// Analyze each file
const fileAnalysis = await this.analyzeFiles(sourceFiles, coverageData);
// Find uncovered files
const coveredFiles = new Set(coverageData.map(d => this.normalizePath(d.url)));
const uncoveredFiles = sourceFiles.filter(file => !coveredFiles.has(file));
// Calculate summary
const summary = this.calculateSummary(fileAnalysis, uncoveredFiles.length);
const report: CoverageReport = {
timestamp: new Date().toISOString(),
summary,
files: fileAnalysis,
uncoveredFiles,
};
// Generate various report formats
await this.generateHTMLReport(report);
await this.generateJSONReport(report);
await this.generateTextReport(report);
await this.generateBadges(report);
return report;
}
private async loadCoverageData(): Promise<CoverageData[]> {
const coverageFiles: CoverageData[] = [];
if (!await exists(this.coverageDir)) {
throw new Error(`Coverage directory not found: ${this.coverageDir}`);
}
for await (const entry of walk(this.coverageDir, {
exts: [".json"],
includeDirs: false
})) {
const content = await Deno.readTextFile(entry.path);
const data = JSON.parse(content);
if (data.url && data.ranges) {
coverageFiles.push(data);
}
}
return coverageFiles;
}
private async getSourceFiles(): Promise<string[]> {
const files: string[] = [];
for await (const entry of walk(this.sourceDir, {
exts: [".ts"],
includeDirs: false,
skip: [/\.test\.ts$/, /test\.ts$/, /tests?\//]
})) {
// Normalize path
const relativePath = entry.path.replace(Deno.cwd() + "/", "");
files.push(relativePath);
}
return files;
}
private normalizePath(url: string): string {
// Convert file:// URLs to relative paths
if (url.startsWith("file://")) {
url = url.replace("file://", "");
}
// Remove leading slash and make relative to cwd
const cwd = Deno.cwd();
if (url.startsWith(cwd)) {
url = url.replace(cwd + "/", "");
}
return url;
}
private async analyzeFiles(sourceFiles: string[], coverageData: CoverageData[]): Promise<FileCoverage[]> {
const analysis: FileCoverage[] = [];
for (const file of sourceFiles) {
const coverage = coverageData.find(d => this.normalizePath(d.url) === file);
const fileAnalysis = await this.analyzeFile(file, coverage);
analysis.push(fileAnalysis);
}
return analysis;
}
private async analyzeFile(filePath: string, coverage?: CoverageData): Promise<FileCoverage> {
const content = await Deno.readTextFile(filePath);
const lines = content.split('\n');
const totalLines = lines.filter(line => line.trim() && !line.trim().startsWith('//')).length;
if (!coverage) {
return {
path: filePath,
totalLines,
coveredLines: 0,
uncoveredLines: Array.from({ length: totalLines }, (_, i) => i + 1),
coverage: 0,
functions: this.extractFunctions(content, false),
};
}
// Analyze covered lines
const coveredLines = new Set<number>();
for (const range of coverage.ranges) {
for (let line = range.start.line; line <= range.end.line; line++) {
coveredLines.add(line);
}
}
const uncoveredLines: number[] = [];
for (let i = 1; i <= lines.length; i++) {
const line = lines[i - 1].trim();
if (line && !line.startsWith('//') && !coveredLines.has(i)) {
uncoveredLines.push(i);
}
}
const coveragePercent = totalLines > 0 ? ((totalLines - uncoveredLines.length) / totalLines) * 100 : 100;
return {
path: filePath,
totalLines,
coveredLines: totalLines - uncoveredLines.length,
uncoveredLines,
coverage: coveragePercent,
functions: this.extractFunctions(content, true, coveredLines),
};
}
private extractFunctions(content: string, checkCoverage: boolean, coveredLines?: Set<number>): FunctionCoverage[] {
const functions: FunctionCoverage[] = [];
const lines = content.split('\n');
// Simple regex patterns for TypeScript functions
const patterns = [
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)/,
/^\s*(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/,
/^\s*(?:public|private|protected)?\s*(?:async\s+)?(\w+)\s*\(/,
/^\s*(\w+)\s*:\s*\([^)]*\)\s*=>/,
];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
const functionName = match[1];
const lineNumber = i + 1;
const covered = checkCoverage ? (coveredLines?.has(lineNumber) ?? false) : false;
functions.push({
name: functionName,
line: lineNumber,
covered,
});
break;
}
}
}
return functions;
}
private calculateSummary(files: FileCoverage[], uncoveredFileCount: number): CoverageReport['summary'] {
const totalFiles = files.length + uncoveredFileCount;
const totalLines = files.reduce((sum, file) => sum + file.totalLines, 0);
const coveredLines = files.reduce((sum, file) => sum + file.coveredLines, 0);
const coverage = totalLines > 0 ? (coveredLines / totalLines) * 100 : 100;
const passed = coverage >= this.thresholds.lines;
return {
totalFiles,
totalLines,
coveredLines,
coverage,
thresholds: this.thresholds,
passed,
};
}
private async generateHTMLReport(report: CoverageReport): Promise<void> {
const fileRows = report.files
.sort((a, b) => a.coverage - b.coverage)
.map(file => {
const coverageClass = file.coverage >= 80 ? 'high' : file.coverage >= 60 ? 'medium' : 'low';
const coverageBar = `<div class="coverage-bar"><div class="coverage-fill ${coverageClass}" style="width: ${file.coverage}%"></div></div>`;
return `
<tr class="${coverageClass}">
<td><a href="#file-${file.path.replace(/[^a-zA-Z0-9]/g, '-')}">${file.path}</a></td>
<td>${file.coverage.toFixed(2)}%</td>
<td>${coverageBar}</td>
<td>${file.coveredLines}/${file.totalLines}</td>
<td>${file.uncoveredLines.length}</td>
<td>${file.functions.filter(f => f.covered).length}/${file.functions.length}</td>
</tr>`;
}).join('');
const uncoveredRows = report.uncoveredFiles.map(file => `
<tr class="uncovered">
<td>${file}</td>
<td>0%</td>
<td><div class="coverage-bar"><div class="coverage-fill low" style="width: 0%"></div></div></td>
<td>0/0</td>
<td>-</td>
<td>0/0</td>
</tr>
`).join('');
const fileDetails = report.files.map(file => {
const uncoveredLinesStr = file.uncoveredLines.length > 0
? file.uncoveredLines.slice(0, 20).join(', ') + (file.uncoveredLines.length > 20 ? '...' : '')
: 'All lines covered';
const functionRows = file.functions.map(func => `
<tr class="${func.covered ? 'covered' : 'uncovered'}">
<td>${func.name}</td>
<td>${func.line}</td>
<td>${func.covered ? 'ā
' : 'ā'}</td>
</tr>
`).join('');
return `
<div class="file-detail" id="file-${file.path.replace(/[^a-zA-Z0-9]/g, '-')}">
<h3>š ${file.path}</h3>
<div class="file-summary">
<div class="metric">
<span class="label">Coverage:</span>
<span class="value ${file.coverage >= 80 ? 'high' : file.coverage >= 60 ? 'medium' : 'low'}">${file.coverage.toFixed(2)}%</span>
</div>
<div class="metric">
<span class="label">Lines:</span>
<span class="value">${file.coveredLines}/${file.totalLines}</span>
</div>
<div class="metric">
<span class="label">Functions:</span>
<span class="value">${file.functions.filter(f => f.covered).length}/${file.functions.length}</span>
</div>
</div>
${file.uncoveredLines.length > 0 ? `
<div class="uncovered-lines">
<h4>Uncovered Lines:</h4>
<p>${uncoveredLinesStr}</p>
</div>
` : ''}
${file.functions.length > 0 ? `
<div class="functions">
<h4>Functions:</h4>
<table>
<thead>
<tr>
<th>Function</th>
<th>Line</th>
<th>Covered</th>
</tr>
</thead>
<tbody>
${functionRows}
</tbody>
</table>
</div>
` : ''}
</div>`;
}).join('');
const html = `
<html>
<head>
<title>Claude-Flow Coverage Report</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; }
.header { background: white; padding: 30px; border-radius: 8px; margin-bottom: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.header h1 { margin: 0; color: #2c3e50; }
.header .timestamp { color: #7f8c8d; margin-top: 5px; }
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
.summary-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
.summary-card h3 { margin: 0 0 10px 0; color: #2c3e50; font-size: 14px; text-transform: uppercase; }
.summary-card .value { font-size: 32px; font-weight: bold; margin: 10px 0; }
.summary-card .value.high { color: #27ae60; }
.summary-card .value.medium { color: #f39c12; }
.summary-card .value.low { color: #e74c3c; }
.summary-card .threshold { font-size: 12px; color: #7f8c8d; }
.coverage-status { padding: 15px; border-radius: 8px; margin-bottom: 30px; text-align: center; font-weight: bold; }
.coverage-status.passed { background: #d4edda; color: #155724; }
.coverage-status.failed { background: #f8d7da; color: #721c24; }
.files-table { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px; }
.files-table h2 { margin: 0; padding: 20px; background: #2c3e50; color: white; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ecf0f1; }
th { background: #f8f9fa; font-weight: 600; }
.coverage-bar { width: 100px; height: 20px; background: #ecf0f1; border-radius: 10px; overflow: hidden; }
.coverage-fill { height: 100%; transition: width 0.3s ease; }
.coverage-fill.high { background: linear-gradient(90deg, #27ae60, #2ecc71); }
.coverage-fill.medium { background: linear-gradient(90deg, #f39c12, #e67e22); }
.coverage-fill.low { background: linear-gradient(90deg, #e74c3c, #c0392b); }
.high { background-color: rgba(39, 174, 96, 0.1); }
.medium { background-color: rgba(243, 156, 18, 0.1); }
.low { background-color: rgba(231, 76, 60, 0.1); }
.uncovered { background-color: rgba(231, 76, 60, 0.2); }
.file-detail { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.file-detail h3 { margin: 0 0 15px 0; color: #2c3e50; }
.file-summary { display: flex; gap: 20px; margin-bottom: 20px; }
.metric { display: flex; flex-direction: column; }
.metric .label { font-size: 12px; color: #7f8c8d; text-transform: uppercase; }
.metric .value { font-size: 18px; font-weight: bold; margin-top: 5px; }
.uncovered-lines { margin: 15px 0; }
.uncovered-lines h4 { margin: 0 0 10px 0; color: #e74c3c; }
.functions { margin: 15px 0; }
.functions h4 { margin: 0 0 10px 0; color: #2c3e50; }
.functions table { margin-top: 10px; }
.covered { background-color: rgba(39, 174, 96, 0.1); }
.footer { text-align: center; margin-top: 40px; color: #7f8c8d; }
</style>
</head>
<body>
<div class="header">
<h1>š Claude-Flow Coverage Report</h1>
<div class="timestamp">Generated: ${report.timestamp}</div>
</div>
<div class="coverage-status ${report.summary.passed ? 'passed' : 'failed'}">
${report.summary.passed ? 'ā
Coverage thresholds passed' : 'ā Coverage thresholds not met'}
</div>
<div class="summary">
<div class="summary-card">
<h3>Overall Coverage</h3>
<div class="value ${report.summary.coverage >= 80 ? 'high' : report.summary.coverage >= 60 ? 'medium' : 'low'}">${report.summary.coverage.toFixed(2)}%</div>
<div class="threshold">Threshold: ${this.thresholds.lines}%</div>
</div>
<div class="summary-card">
<h3>Total Files</h3>
<div class="value">${report.summary.totalFiles}</div>
</div>
<div class="summary-card">
<h3>Covered Lines</h3>
<div class="value">${report.summary.coveredLines}</div>
<div class="threshold">of ${report.summary.totalLines}</div>
</div>
<div class="summary-card">
<h3>Uncovered Files</h3>
<div class="value ${report.uncoveredFiles.length === 0 ? 'high' : 'low'}">${report.uncoveredFiles.length}</div>
</div>
</div>
<div class="files-table">
<h2>š File Coverage</h2>
<table>
<thead>
<tr>
<th>File</th>
<th>Coverage</th>
<th>Progress</th>
<th>Lines</th>
<th>Uncovered</th>
<th>Functions</th>
</tr>
</thead>
<tbody>
${fileRows}
${uncoveredRows}
</tbody>
</table>
</div>
<div class="file-details">
<h2>š File Details</h2>
${fileDetails}
</div>
<div class="footer">
<p>Claude-Flow Coverage Report - Advanced AI Agent Orchestration System</p>
</div>
</body>
</html>`;
await Deno.writeTextFile(`${this.outputDir}/coverage-detailed.html`, html);
console.log(" ā
Detailed HTML coverage report generated");
}
private async generateJSONReport(report: CoverageReport): Promise<void> {
await Deno.writeTextFile(
`${this.outputDir}/coverage-report.json`,
JSON.stringify(report, null, 2)
);
console.log(" ā
JSON coverage report generated");
}
private async generateTextReport(report: CoverageReport): Promise<void> {
const lines: string[] = [];
lines.push("š CLAUDE-FLOW COVERAGE REPORT");
lines.push("=".repeat(50));
lines.push(`Generated: ${report.timestamp}`);
lines.push("");
lines.push("š SUMMARY");
lines.push("-".repeat(20));
lines.push(`Overall Coverage: ${report.summary.coverage.toFixed(2)}%`);
lines.push(`Total Files: ${report.summary.totalFiles}`);
lines.push(`Covered Lines: ${report.summary.coveredLines}/${report.summary.totalLines}`);
lines.push(`Uncovered Files: ${report.uncoveredFiles.length}`);
lines.push("");
lines.push("šÆ THRESHOLDS");
lines.push("-".repeat(20));
lines.push(`Lines: ${report.summary.coverage.toFixed(2)}% (threshold: ${report.summary.thresholds.lines}%)`);
lines.push(`Status: ${report.summary.passed ? 'ā
PASSED' : 'ā FAILED'}`);
lines.push("");
if (report.files.length > 0) {
lines.push("š FILES BY COVERAGE");
lines.push("-".repeat(20));
const sortedFiles = [...report.files].sort((a, b) => a.coverage - b.coverage);
for (const file of sortedFiles) {
const status = file.coverage >= 80 ? 'ā
' : file.coverage >= 60 ? 'ā ļø' : 'ā';
lines.push(`${status} ${file.path}: ${file.coverage.toFixed(2)}% (${file.coveredLines}/${file.totalLines} lines)`);
if (file.uncoveredLines.length > 0 && file.uncoveredLines.length <= 10) {
lines.push(` Uncovered lines: ${file.uncoveredLines.join(', ')}`);
} else if (file.uncoveredLines.length > 10) {
lines.push(` Uncovered lines: ${file.uncoveredLines.slice(0, 10).join(', ')}... (+${file.uncoveredLines.length - 10} more)`);
}
}
lines.push("");
}
if (report.uncoveredFiles.length > 0) {
lines.push("š« UNCOVERED FILES");
lines.push("-".repeat(20));
for (const file of report.uncoveredFiles) {
lines.push(`ā ${file}`);
}
lines.push("");
}
lines.push("š” RECOMMENDATIONS");
lines.push("-".repeat(20));
const lowCoverageFiles = report.files.filter(f => f.coverage < 60);
if (lowCoverageFiles.length > 0) {
lines.push("⢠Focus on improving coverage for these files:");
for (const file of lowCoverageFiles.slice(0, 5)) {
lines.push(` - ${file.path} (${file.coverage.toFixed(2)}%)`);
}
}
if (report.uncoveredFiles.length > 0) {
lines.push("⢠Add tests for uncovered files:");
for (const file of report.uncoveredFiles.slice(0, 5)) {
lines.push(` - ${file}`);
}
}
if (report.summary.coverage >= 90) {
lines.push("⢠Excellent coverage! Consider adding edge case tests.");
} else if (report.summary.coverage >= 80) {
lines.push("⢠Good coverage! Focus on critical paths and error handling.");
} else {
lines.push("⢠Coverage needs improvement. Add tests for core functionality first.");
}
await Deno.writeTextFile(`${this.outputDir}/coverage-report.txt`, lines.join('\n'));
console.log(" ā
Text coverage report generated");
}
private async generateBadges(report: CoverageReport): Promise<void> {
const coverage = report.summary.coverage;
const color = coverage >= 90 ? 'brightgreen' :
coverage >= 80 ? 'green' :
coverage >= 70 ? 'yellow' :
coverage >= 60 ? 'orange' : 'red';
const badgeUrl = `https://img.shields.io/badge/coverage-${coverage.toFixed(1)}%25-${color}`;
const badgeMarkdown = ``;
const badgeHTML = `<img src="${badgeUrl}" alt="Coverage ${coverage.toFixed(1)}%">`;
const badges = {
url: badgeUrl,
markdown: badgeMarkdown,
html: badgeHTML,
coverage: coverage.toFixed(1),
color,
status: report.summary.passed ? 'passed' : 'failed',
};
await Deno.writeTextFile(
`${this.outputDir}/coverage-badges.json`,
JSON.stringify(badges, null, 2)
);
console.log(" ā
Coverage badges generated");
}
}
async function main(): Promise<void> {
const args = parseArgs(Deno.args, {
string: ["source-dir", "coverage-dir", "output-dir"],
boolean: ["help"],
default: {
"source-dir": "./src",
"coverage-dir": "./tests/results/coverage",
"output-dir": "./tests/results",
},
alias: {
h: "help",
s: "source-dir",
c: "coverage-dir",
o: "output-dir",
},
});
if (args.help) {
console.log(`
š Claude-Flow Coverage Analysis Tool
USAGE:
coverage-report.ts [OPTIONS]
OPTIONS:
--source-dir, -s Source code directory (default: ./src)
--coverage-dir, -c Coverage data directory (default: ./tests/results/coverage)
--output-dir, -o Output directory for reports (default: ./tests/results)
--help, -h Show this help
EXAMPLES:
# Generate coverage report with defaults
./scripts/coverage-report.ts
# Custom directories
./scripts/coverage-report.ts --source-dir ./lib --coverage-dir ./coverage --output-dir ./reports
`);
return;
}
const thresholds = {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
};
const analyzer = new CoverageAnalyzer({
sourceDir: args["source-dir"],
coverageDir: args["coverage-dir"],
outputDir: args["output-dir"],
thresholds,
});
try {
const report = await analyzer.generateReport();
console.log("\nš COVERAGE SUMMARY");
console.log("=".repeat(40));
console.log(`Overall Coverage: ${report.summary.coverage.toFixed(2)}%`);
console.log(`Total Files: ${report.summary.totalFiles}`);
console.log(`Covered Lines: ${report.summary.coveredLines}/${report.summary.totalLines}`);
console.log(`Uncovered Files: ${report.uncoveredFiles.length}`);
const status = report.summary.passed ? "ā
PASSED" : "ā FAILED";
console.log(`Status: ${status}`);
console.log(`\nš Reports generated in: ${args["output-dir"]}`);
console.log(` - Detailed HTML: coverage-detailed.html`);
console.log(` - JSON Data: coverage-report.json`);
console.log(` - Text Summary: coverage-report.txt`);
console.log(` - Badges: coverage-badges.json`);
Deno.exit(report.summary.passed ? 0 : 1);
} catch (error) {
console.error("ā Coverage analysis failed:", error.message);
Deno.exit(1);
}
}
if (import.meta.main) {
main();
}