/** * Brain v0.1 - Doctor Analyzer * * Analyzes Guard violations to determine root causes. * Works with or without LLM - template-based analysis is always available. */ import type { GuardViolation } from "../../guard/rules"; import { GUARD_RULES } from "../../guard/rules"; import type { DoctorAnalysis, PatchSuggestion } from "../types"; import { getBrain } from "../brain"; import { getSessionMemory } from "../memory"; /** * Violation category for grouping */ export type ViolationCategory = | "spec" | "generated" | "slot" | "contract" | "unknown"; /** * Categorize a violation by its rule ID */ export function categorizeViolation(ruleId: string): ViolationCategory { if (ruleId.includes("SPEC")) { return "spec"; } if (ruleId.includes("GENERATED") || ruleId.includes("FORBIDDEN_IMPORT")) { return "generated"; } if (ruleId.includes("SLOT")) { return "slot"; } if (ruleId.includes("CONTRACT")) { return "contract"; } return "unknown"; } /** * Template-based root cause analysis * * Provides analysis without requiring LLM. */ export function analyzeRootCauseTemplate( violations: GuardViolation[] ): { summary: string; explanation: string } { if (violations.length === 0) { return { summary: "No violations detected", explanation: "All Guard checks passed successfully.", }; } // Group violations by category const grouped = new Map(); for (const v of violations) { const category = categorizeViolation(v.ruleId); if (!grouped.has(category)) { grouped.set(category, []); } grouped.get(category)!.push(v); } // Build summary const summaryParts: string[] = []; const explanationParts: string[] = []; // Spec issues const specViolations = grouped.get("spec") || []; if (specViolations.length > 0) { summaryParts.push(`${specViolations.length} spec 관련 위반`); explanationParts.push( `## Spec 관련 문제\n` + specViolations.map((v) => `- ${v.message}`).join("\n") + `\n\n원인: spec 파일이 변경되었거나 lock 파일과 동기화가 필요합니다.` ); } // Generated issues const generatedViolations = grouped.get("generated") || []; if (generatedViolations.length > 0) { summaryParts.push(`${generatedViolations.length} generated 파일 위반`); explanationParts.push( `## Generated 파일 문제\n` + generatedViolations.map((v) => `- ${v.message}`).join("\n") + `\n\n원인: generated 파일이 수동으로 수정되었거나 금지된 import가 있습니다.` ); } // Slot issues const slotViolations = grouped.get("slot") || []; if (slotViolations.length > 0) { summaryParts.push(`${slotViolations.length} slot 파일 위반`); explanationParts.push( `## Slot 파일 문제\n` + slotViolations.map((v) => `- ${v.message}`).join("\n") + `\n\n원인: slot 파일이 없거나 필수 패턴이 누락되었습니다.` ); } // Contract issues const contractViolations = grouped.get("contract") || []; if (contractViolations.length > 0) { summaryParts.push(`${contractViolations.length} contract 위반`); explanationParts.push( `## Contract 문제\n` + contractViolations.map((v) => `- ${v.message}`).join("\n") + `\n\n원인: contract와 slot 간의 불일치가 있습니다.` ); } // Unknown issues const unknownViolations = grouped.get("unknown") || []; if (unknownViolations.length > 0) { summaryParts.push(`${unknownViolations.length} 기타 위반`); explanationParts.push( `## 기타 문제\n` + unknownViolations.map((v) => `- ${v.message}`).join("\n") ); } return { summary: summaryParts.join(", "), explanation: explanationParts.join("\n\n"), }; } /** * Generate template-based patch suggestions */ export function generateTemplatePatches( violations: GuardViolation[] ): PatchSuggestion[] { const patches: PatchSuggestion[] = []; for (const violation of violations) { switch (violation.ruleId) { case GUARD_RULES.GENERATED_MANUAL_EDIT?.id: patches.push({ file: violation.file, description: "Generated 파일 재생성", type: "command", command: "bunx mandu generate", confidence: 0.9, }); break; case GUARD_RULES.SLOT_NOT_FOUND?.id: patches.push({ file: violation.file, description: "Slot 파일 생성", type: "command", command: "bunx mandu generate", confidence: 0.8, }); break; case GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id: patches.push({ file: violation.file, description: "Default export 추가", type: "modify", content: `// Add default export to your slot file:\nexport default Mandu.filling()...`, confidence: 0.7, }); break; case GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id: patches.push({ file: violation.file, description: "Mandu.filling() 패턴 추가", type: "modify", content: `import { Mandu } from "@mandujs/core";\n\nexport default Mandu.filling()\n .get(async (ctx) => {\n return ctx.json({ message: "Hello" });\n });`, confidence: 0.6, }); break; case GUARD_RULES.CONTRACT_METHOD_NOT_IMPLEMENTED?.id: patches.push({ file: violation.file, description: "Contract 메서드 구현 또는 sync", type: "command", command: `bunx mandu contract validate --verbose`, confidence: 0.7, }); break; case GUARD_RULES.FORBIDDEN_IMPORT_IN_GENERATED?.id: patches.push({ file: violation.file, description: "Generated 파일 재생성 (금지된 import 제거)", type: "command", command: "bunx mandu generate", confidence: 0.8, }); break; case GUARD_RULES.ISLAND_FIRST_INTEGRITY?.id: patches.push({ file: violation.file, description: "Create a .island.tsx file in the same app/ directory as page.tsx. " + "Do NOT import or re-export the island in page.tsx — island() returns " + "a config object, not a React component. Use data-island attributes instead.", type: "modify", content: `// Example: app/my-feature.island.tsx\n` + `import { wrapComponent } from "@mandujs/core/client";\n\n` + `export default wrapComponent(MyComponent);\n\n` + `// In page.tsx, reference via:
...
`, confidence: 0.9, }); break; default: // Generic suggestion based on violation.suggestion if (violation.suggestion) { if (violation.suggestion.includes("generate")) { patches.push({ file: violation.file, description: violation.suggestion, type: "command", command: "bunx mandu generate", confidence: 0.5, }); } else { patches.push({ file: violation.file, description: violation.suggestion, type: "modify", content: "", confidence: 0.4, }); } } } } return patches; } /** * LLM-enhanced prompt for root cause analysis */ export function buildAnalysisPrompt(violations: GuardViolation[]): string { const violationList = violations .map( (v, i) => `${i + 1}. [${v.ruleId}] ${v.file}\n Message: ${v.message}\n Suggestion: ${v.suggestion || "None"}` ) .join("\n\n"); return `You are analyzing Mandu framework Guard violations. Mandu is a spec-driven fullstack framework where: - spec/ contains route manifests and slot files - generated/ contains auto-generated code (DO NOT EDIT) - slots handle business logic - contracts define API schemas Analyze these violations and provide: 1. A brief summary (1-2 sentences) of the root cause 2. A detailed explanation of why these violations occurred 3. The recommended fix order Violations: ${violationList} Respond in Korean. Be concise and actionable.`; } /** * Parse LLM analysis response */ export function parseLLMAnalysis( response: string, fallback: { summary: string; explanation: string } ): { summary: string; explanation: string } { if (!response || response.trim().length === 0) { return fallback; } // Try to extract summary (first paragraph or sentence) const lines = response.split("\n").filter((l) => l.trim()); const summary = lines[0]?.trim() || fallback.summary; // Rest is explanation const explanation = lines.slice(1).join("\n").trim() || fallback.explanation; return { summary, explanation }; } /** * Analyze Guard violations * * Uses LLM if available for enhanced analysis, * falls back to template-based analysis otherwise. */ export async function analyzeViolations( violations: GuardViolation[], options: { useLLM?: boolean } = {} ): Promise { const { useLLM = true } = options; // Store in memory const memory = getSessionMemory(); memory.setGuardResult(violations); // Template-based analysis (always available) const templateAnalysis = analyzeRootCauseTemplate(violations); const templatePatches = generateTemplatePatches(violations); // Determine recommended next command let nextCommand = "bunx mandu generate"; // If LLM is not requested or not available, return template analysis if (!useLLM) { return { violations, summary: templateAnalysis.summary, explanation: templateAnalysis.explanation, patches: templatePatches, llmAssisted: false, nextCommand, }; } // Try LLM-enhanced analysis const brain = getBrain(); const llmAvailable = await brain.isLLMAvailable(); if (!llmAvailable || !brain.enabled) { return { violations, summary: templateAnalysis.summary, explanation: templateAnalysis.explanation, patches: templatePatches, llmAssisted: false, nextCommand, }; } // Generate LLM analysis const prompt = buildAnalysisPrompt(violations); const llmResponse = await brain.generate(prompt); if (llmResponse) { const llmAnalysis = parseLLMAnalysis(llmResponse, templateAnalysis); return { violations, summary: llmAnalysis.summary, explanation: llmAnalysis.explanation, patches: templatePatches, // Still use template patches (LLM for analysis, not patches) llmAssisted: true, nextCommand, }; } // Fallback to template return { violations, summary: templateAnalysis.summary, explanation: templateAnalysis.explanation, patches: templatePatches, llmAssisted: false, nextCommand, }; }