/** * Brain v0.1 - Doctor Reporter * * Formats and outputs Doctor analysis results. * Works with or without LLM - always provides actionable output. */ import type { DoctorAnalysis, PatchSuggestion } from "../types"; import type { GuardViolation } from "../../guard/rules"; import { prioritizePatches } from "./patcher"; /** * ANSI color codes for terminal output */ const colors = { reset: "\x1b[0m", bright: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", }; /** * Check if colors should be used */ function useColors(): boolean { // Check for NO_COLOR environment variable if (process.env.NO_COLOR) return false; // Check if stdout is a TTY if (typeof process?.stdout?.isTTY === "boolean") { return process.stdout.isTTY; } return true; } /** * Apply color if enabled */ function color(text: string, colorCode: string): string { if (!useColors()) return text; return `${colorCode}${text}${colors.reset}`; } /** * Format a violation for terminal output */ export function formatDoctorViolation(violation: GuardViolation): string { const lines: string[] = []; const severity = violation.severity || "error"; const icon = severity === "error" ? "❌" : "⚠ïļ"; const colorCode = severity === "error" ? colors.red : colors.yellow; lines.push( `${icon} ${color(`[${violation.ruleId}]`, colorCode)} ${violation.file}` ); lines.push(` ${violation.message}`); if (violation.suggestion) { lines.push(` ${color("ðŸ’Ą", colors.cyan)} ${violation.suggestion}`); } if (violation.line) { lines.push(` ${color("📍", colors.dim)} Line ${violation.line}`); } return lines.join("\n"); } /** * Format a patch suggestion for terminal output */ export function formatPatch(patch: PatchSuggestion, index: number): string { const lines: string[] = []; const confidenceIcon = patch.confidence >= 0.8 ? "ðŸŸĒ" : patch.confidence >= 0.5 ? "ðŸŸĄ" : "ðŸ”ī"; lines.push( `${color(`${index}.`, colors.bright)} ${patch.description} ${confidenceIcon}` ); switch (patch.type) { case "command": lines.push( ` ${color("$ ", colors.dim)}${color(patch.command || "", colors.cyan)}` ); break; case "add": lines.push(` ${color("+ íŒŒėž ėƒė„ą:", colors.green)} ${patch.file}`); break; case "modify": lines.push(` ${color("~ íŒŒėž ėˆ˜ė •:", colors.yellow)} ${patch.file}`); if (patch.line) { lines.push(` ${color("📍", colors.dim)} Line ${patch.line}`); } break; case "delete": lines.push(` ${color("- íŒŒėž ė‚­ė œ:", colors.red)} ${patch.file}`); break; } return lines.join("\n"); } /** * Print Doctor analysis to console */ export function printDoctorReport(analysis: DoctorAnalysis): void { const { violations, summary, explanation, patches, llmAssisted, nextCommand } = analysis; console.log(); console.log(color("ðŸĐš Mandu Doctor Report", colors.bright + colors.blue)); console.log(color("─".repeat(50), colors.dim)); console.log(); // Summary console.log(color("📋 ėš”ė•―", colors.bright)); console.log(` ${summary}`); console.log(); // Violations if (violations.length > 0) { console.log( color(`🔍 발ęēŽëœ ėœ„ë°˜ (${violations.length}氜)`, colors.bright) ); console.log(); for (const violation of violations) { console.log(formatDoctorViolation(violation)); console.log(); } } // Detailed explanation if (explanation) { console.log(color("📖 냁ė„ļ ëķ„ė„", colors.bright)); console.log(); // Format explanation with proper indentation const explLines = explanation.split("\n"); for (const line of explLines) { if (line.startsWith("##")) { console.log(color(line.replace("## ", "â–ļ "), colors.cyan)); } else if (line.trim()) { console.log(` ${line}`); } else { console.log(); } } console.log(); } // Patch suggestions if (patches.length > 0) { console.log(color(`💊 ė œė•ˆëœ ėˆ˜ė • (${patches.length}氜)`, colors.bright)); console.log(); const prioritized = prioritizePatches(patches); for (let i = 0; i < prioritized.length; i++) { console.log(formatPatch(prioritized[i], i + 1)); console.log(); } } // Next command if (nextCommand) { console.log(color("â–ķ ęķŒėžĨ ë‹ĪėŒ 멅ë đė–ī", colors.bright)); console.log(); console.log(` ${color("$ ", colors.dim)}${color(nextCommand, colors.green)}`); console.log(); } // Footer console.log(color("─".repeat(50), colors.dim)); console.log( `${color("â„đïļ", colors.cyan)} LLM 맀뛐: ${llmAssisted ? color("똈", colors.green) : color("ė•„ë‹ˆė˜Ī", colors.yellow)}` ); console.log(); } /** * Generate a JSON report for MCP/API consumption */ export function generateJsonReport(analysis: DoctorAnalysis): string { return JSON.stringify( { summary: analysis.summary, violationCount: analysis.violations.length, violations: analysis.violations.map((v) => ({ ruleId: v.ruleId, file: v.file, message: v.message, suggestion: v.suggestion, line: v.line, severity: v.severity || "error", })), patches: analysis.patches.map((p) => ({ file: p.file, type: p.type, description: p.description, ...(p.type === "command" ? { command: p.command } : {}), confidence: p.confidence, })), nextCommand: analysis.nextCommand, llmAssisted: analysis.llmAssisted, }, null, 2 ); } /** * Generate a Markdown report (Doctor Analysis) */ export function generateDoctorMarkdownReport(analysis: DoctorAnalysis): string { const lines: string[] = []; lines.push("# ðŸĐš Mandu Doctor Report"); lines.push(""); lines.push("## 📋 ėš”ė•―"); lines.push(""); lines.push(analysis.summary); lines.push(""); if (analysis.violations.length > 0) { lines.push(`## 🔍 발ęēŽëœ ėœ„ë°˜ (${analysis.violations.length}氜)`); lines.push(""); for (const v of analysis.violations) { const severity = v.severity || "error"; const icon = severity === "error" ? "❌" : "⚠ïļ"; lines.push(`### ${icon} ${v.ruleId}`); lines.push(""); lines.push(`- **íŒŒėž**: \`${v.file}\``); lines.push(`- **ëĐ”ė‹œė§€**: ${v.message}`); if (v.suggestion) { lines.push(`- **ė œė•ˆ**: ${v.suggestion}`); } if (v.line) { lines.push(`- **띞ėļ**: ${v.line}`); } lines.push(""); } } if (analysis.explanation) { lines.push("## 📖 냁ė„ļ ëķ„ė„"); lines.push(""); lines.push(analysis.explanation); lines.push(""); } if (analysis.patches.length > 0) { lines.push(`## 💊 ė œė•ˆëœ ėˆ˜ė • (${analysis.patches.length}氜)`); lines.push(""); const prioritized = prioritizePatches(analysis.patches); for (let i = 0; i < prioritized.length; i++) { const p = prioritized[i]; const confidenceLabel = p.confidence >= 0.8 ? "ðŸŸĒ ë†’ėŒ" : p.confidence >= 0.5 ? "ðŸŸĄ ëģīí†ĩ" : "ðŸ”ī ë‚ŪėŒ"; lines.push(`### ${i + 1}. ${p.description}`); lines.push(""); lines.push(`- **íƒ€ėž…**: ${p.type}`); lines.push(`- **ëŒ€ėƒ**: \`${p.file}\``); lines.push(`- **ė‹ ëĒ°ë„**: ${confidenceLabel}`); if (p.type === "command") { lines.push(""); lines.push("```bash"); lines.push(p.command); lines.push("```"); } lines.push(""); } } if (analysis.nextCommand) { lines.push("## â–ķ ęķŒėžĨ ë‹ĪėŒ 멅ë đė–ī"); lines.push(""); lines.push("```bash"); lines.push(analysis.nextCommand); lines.push("```"); lines.push(""); } lines.push("---"); lines.push(""); lines.push( `*LLM 맀뛐: ${analysis.llmAssisted ? "똈" : "ė•„ë‹ˆė˜Ī"}*` ); return lines.join("\n"); } /** * Format output based on requested format */ export type ReportFormat = "console" | "json" | "markdown"; export function formatDoctorReport( analysis: DoctorAnalysis, format: ReportFormat = "console" ): string | void { switch (format) { case "console": printDoctorReport(analysis); return; case "json": return generateJsonReport(analysis); case "markdown": return generateDoctorMarkdownReport(analysis); default: printDoctorReport(analysis); } }