/** * Brain v0.1 - Doctor Patcher * * Generates and applies minimal patches to fix violations. * Auto-apply is disabled by default - patches are suggestions only. */ import type { PatchSuggestion } from "../types"; import type { DoctorAnalysis } from "../types"; import { validatePatchSuggestion, filterSafePatchSuggestions, } from "../permissions"; import path from "path"; import fs from "fs/promises"; /** * Patch application result */ export interface PatchResult { /** Whether the patch was applied */ applied: boolean; /** The patch that was applied */ patch: PatchSuggestion; /** Error message if failed */ error?: string; /** Output from command (for command type) */ output?: string; } /** * Batch patch result */ export interface BatchPatchResult { /** Total patches attempted */ total: number; /** Successfully applied */ applied: number; /** Failed patches */ failed: number; /** Skipped (unsafe) patches */ skipped: number; /** Individual results */ results: PatchResult[]; } /** * Prioritize patches by confidence and type */ export function prioritizePatches(patches: PatchSuggestion[]): PatchSuggestion[] { return [...patches].sort((a, b) => { // Commands before modifications if (a.type === "command" && b.type !== "command") return -1; if (b.type === "command" && a.type !== "command") return 1; // Higher confidence first return b.confidence - a.confidence; }); } /** * Deduplicate patches by file and type */ export function deduplicatePatches(patches: PatchSuggestion[]): PatchSuggestion[] { const seen = new Set(); const result: PatchSuggestion[] = []; for (const patch of patches) { const key = `${patch.file}:${patch.type}:${patch.type === "command" ? patch.command : ""}`; if (!seen.has(key)) { seen.add(key); result.push(patch); } } return result; } /** * Generate a minimal patch for a violation * * Returns a human-readable patch description. */ export function generatePatchDescription(patch: PatchSuggestion): string { const confidenceLabel = patch.confidence >= 0.8 ? "높음" : patch.confidence >= 0.5 ? "보통" : "낮음"; switch (patch.type) { case "command": return `[명령어 실행] ${patch.command}\n 대상: ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`; case "add": return `[파일 생성] ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`; case "modify": const lineInfo = patch.line ? ` (라인 ${patch.line})` : ""; return `[파일 수정] ${patch.file}${lineInfo}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`; case "delete": return `[파일 삭제] ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`; default: { const _exhaustive: never = patch; return `[unknown] ${(_exhaustive as PatchSuggestion).file}\n 설명: ${(_exhaustive as PatchSuggestion).description}`; } } } /** * Apply a single patch * * NOTE: This is experimental and disabled by default. * Only applies safe patches (commands like mandu generate). */ export async function applyPatch( patch: PatchSuggestion, rootDir: string ): Promise { // Validate patch first const validation = validatePatchSuggestion(patch); if (!validation.valid) { return { applied: false, patch, error: validation.reason || "Patch validation failed", }; } if (validation.requiresConfirmation) { return { applied: false, patch, error: `Manual confirmation required: ${validation.reason}`, }; } try { switch (patch.type) { case "command": { if (!patch.command) { return { applied: false, patch, error: "No command specified", }; } // Only allow mandu commands for safety if (!patch.command.startsWith("bunx mandu")) { return { applied: false, patch, error: "Only mandu commands are allowed for auto-apply", }; } // Execute the command const proc = Bun.spawn(patch.command.split(" "), { cwd: rootDir, stdout: "pipe", stderr: "pipe", }); const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); const exitCode = await proc.exited; if (exitCode !== 0) { return { applied: false, patch, error: stderr || `Command exited with code ${exitCode}`, output: stdout, }; } return { applied: true, patch, output: stdout, }; } case "add": case "modify": { if (!patch.content) { return { applied: false, patch, error: "No content specified for file operation", }; } const filePath = path.join(rootDir, patch.file); // Ensure directory exists await fs.mkdir(path.dirname(filePath), { recursive: true }); // Write content await fs.writeFile(filePath, patch.content, "utf-8"); return { applied: true, patch, }; } case "delete": { const filePath = path.join(rootDir, patch.file); try { await fs.unlink(filePath); return { applied: true, patch, }; } catch (error) { return { applied: false, patch, error: error instanceof Error ? error.message : "Failed to delete file", }; } } default: { const _exhaustive: never = patch; return { applied: false, patch: _exhaustive as PatchSuggestion, error: `Unknown patch type`, }; } } } catch (error) { return { applied: false, patch, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * Apply multiple patches * * NOTE: Auto-apply is experimental and disabled by default. */ export async function applyPatches( patches: PatchSuggestion[], rootDir: string, options: { dryRun?: boolean; minConfidence?: number } = {} ): Promise { const { dryRun = true, minConfidence = 0.5 } = options; // Filter safe patches const safePatches = filterSafePatchSuggestions(patches); // Filter by confidence const confidentPatches = safePatches.filter( (p) => p.confidence >= minConfidence ); // Deduplicate and prioritize const orderedPatches = prioritizePatches(deduplicatePatches(confidentPatches)); const results: PatchResult[] = []; let applied = 0; let failed = 0; const skipped = patches.length - orderedPatches.length; for (const patch of orderedPatches) { if (dryRun) { // In dry run mode, just report what would be done results.push({ applied: false, patch, error: "Dry run mode - patch not applied", }); continue; } const result = await applyPatch(patch, rootDir); results.push(result); if (result.applied) { applied++; } else { failed++; } } return { total: patches.length, applied, failed, skipped, results, }; } /** * Generate a patch report from analysis */ export function generatePatchReport(analysis: DoctorAnalysis): string { const lines: string[] = []; lines.push("# Mandu Doctor Report"); lines.push(""); lines.push(`## 요약`); lines.push(analysis.summary); lines.push(""); if (analysis.explanation) { 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++) { lines.push(`### ${i + 1}. ${prioritized[i].description}`); lines.push(generatePatchDescription(prioritized[i])); lines.push(""); } } if (analysis.nextCommand) { lines.push(`## 권장 다음 명령어`); lines.push("```bash"); lines.push(analysis.nextCommand); lines.push("```"); lines.push(""); } lines.push(`---`); lines.push(`LLM 지원: ${analysis.llmAssisted ? "예" : "아니오"}`); return lines.join("\n"); }