/** * Code Review Engine - Markdown Reporter * * Generate Markdown format reports. */ import type { CodeIssue, ReportOptions, ReviewResult } from "../types.js"; export class MarkdownReporter { /** * Generate Markdown report */ generate(result: ReviewResult, options: ReportOptions): string { const lines: string[] = []; // Header lines.push("# Code Review Report"); lines.push(""); lines.push(`**Review ID:** ${result.id}`); lines.push(`**Timestamp:** ${result.timestamp}`); lines.push(`**Duration:** ${result.durationMs}ms`); lines.push(""); // Summary if (options.includeSummary !== false) { lines.push("## Summary"); lines.push(""); lines.push(`| Metric | Value |`); lines.push(`|--------|-------|`); lines.push(`| Files Reviewed | ${result.stats.filesReviewed} |`); lines.push(`| Lines Reviewed | ${result.stats.linesReviewed} |`); lines.push(`| Total Issues | ${result.summary.total} |`); lines.push(`| Errors | ${result.summary.errors} |`); lines.push(`| Warnings | ${result.summary.warnings} |`); lines.push(`| Info | ${result.summary.info} |`); lines.push(`| Hints | ${result.summary.hints} |`); lines.push(""); } // Category breakdown if (Object.keys(result.summary.byCategory).length > 0) { lines.push("### Issues by Category"); lines.push(""); lines.push(`| Category | Count |`); lines.push(`|----------|-------|`); for (const [category, count] of Object.entries( result.summary.byCategory, )) { lines.push(`| ${category} | ${count} |`); } lines.push(""); } // Issues if (result.issues.length > 0) { lines.push("## Issues"); lines.push(""); // Group issues const groupedIssues = options.groupByFile ? this.groupByFile(result.issues) : options.groupBySeverity ? this.groupBySeverity(result.issues) : [{ header: "", issues: result.issues }]; for (const group of groupedIssues) { if (group.header) { lines.push(`### ${group.header}`); lines.push(""); } for (const issue of group.issues) { lines.push(this.formatIssue(issue, options)); } } } else { lines.push("## Issues"); lines.push(""); lines.push( "🎉 **No issues found!** Your code passed all automated checks.", ); lines.push(""); } // Footer lines.push("---"); lines.push(""); lines.push("*Generated by pi-harness-runtime Code Review Engine*"); return lines.join("\n"); } /** * Format a single issue */ private formatIssue(issue: CodeIssue, options: ReportOptions): string { const lines: string[] = []; const severityEmoji = this.getSeverityEmoji(issue.severity); lines.push(`#### ${severityEmoji} ${issue.title}`); lines.push(""); lines.push(`- **Severity:** ${issue.severity}`); lines.push(`- **Category:** ${issue.category}`); lines.push( `- **Location:** ${issue.file}${issue.line ? `:${issue.line}` : ""}`, ); if (issue.rule) { lines.push(`- **Rule:** \`${issue.rule}\``); } lines.push(""); lines.push(`${issue.message}`); lines.push(""); if (issue.suggestion && options.includeSuggestions !== false) { lines.push(`> **Suggestion:** ${issue.suggestion}`); lines.push(""); } if (issue.code && options.includeCode !== false) { lines.push("```"); lines.push(issue.code); lines.push("```"); lines.push(""); } return lines.join("\n"); } /** * Get severity emoji */ private getSeverityEmoji(severity: string): string { switch (severity) { case "error": return "🔴"; case "warning": return "🟡"; case "info": return "🔵"; case "hint": return "💡"; default: return "⚪"; } } /** * Group issues by file */ private groupByFile( issues: CodeIssue[], ): { header: string; issues: CodeIssue[] }[] { const groups = new Map(); for (const issue of issues) { const existing = groups.get(issue.file); if (existing) { existing.push(issue); } else { groups.set(issue.file, [issue]); } } return Array.from(groups.entries()).map(([file, fileIssues]) => ({ header: file, issues: fileIssues, })); } /** * Group issues by severity */ private groupBySeverity( issues: CodeIssue[], ): { header: string; issues: CodeIssue[] }[] { const groups = new Map(); const order = ["error", "warning", "info", "hint"]; for (const issue of issues) { const existing = groups.get(issue.severity); if (existing) { existing.push(issue); } else { groups.set(issue.severity, [issue]); } } return order .filter((s) => groups.has(s)) .map((severity) => ({ header: `${severity.charAt(0).toUpperCase() + severity.slice(1)}s`, issues: groups.get(severity)!, })); } }