@mandujs/core 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Brain v0.1 - Doctor Analyzer
3
+ *
4
+ * Analyzes Guard violations to determine root causes.
5
+ * Works with or without LLM - template-based analysis is always available.
6
+ */
7
+
8
+ import type { GuardViolation } from "../../guard/rules";
9
+ import { GUARD_RULES } from "../../guard/rules";
10
+ import type { DoctorAnalysis, PatchSuggestion } from "../types";
11
+ import { getBrain } from "../brain";
12
+ import { getSessionMemory } from "../memory";
13
+
14
+ /**
15
+ * Violation category for grouping
16
+ */
17
+ export type ViolationCategory =
18
+ | "spec"
19
+ | "generated"
20
+ | "slot"
21
+ | "contract"
22
+ | "unknown";
23
+
24
+ /**
25
+ * Categorize a violation by its rule ID
26
+ */
27
+ export function categorizeViolation(ruleId: string): ViolationCategory {
28
+ if (ruleId.includes("SPEC") || ruleId === "SPEC_HASH_MISMATCH") {
29
+ return "spec";
30
+ }
31
+ if (ruleId.includes("GENERATED") || ruleId.includes("FORBIDDEN_IMPORT")) {
32
+ return "generated";
33
+ }
34
+ if (ruleId.includes("SLOT")) {
35
+ return "slot";
36
+ }
37
+ if (ruleId.includes("CONTRACT")) {
38
+ return "contract";
39
+ }
40
+ return "unknown";
41
+ }
42
+
43
+ /**
44
+ * Template-based root cause analysis
45
+ *
46
+ * Provides analysis without requiring LLM.
47
+ */
48
+ export function analyzeRootCauseTemplate(
49
+ violations: GuardViolation[]
50
+ ): { summary: string; explanation: string } {
51
+ if (violations.length === 0) {
52
+ return {
53
+ summary: "No violations detected",
54
+ explanation: "All Guard checks passed successfully.",
55
+ };
56
+ }
57
+
58
+ // Group violations by category
59
+ const grouped = new Map<ViolationCategory, GuardViolation[]>();
60
+ for (const v of violations) {
61
+ const category = categorizeViolation(v.ruleId);
62
+ if (!grouped.has(category)) {
63
+ grouped.set(category, []);
64
+ }
65
+ grouped.get(category)!.push(v);
66
+ }
67
+
68
+ // Build summary
69
+ const summaryParts: string[] = [];
70
+ const explanationParts: string[] = [];
71
+
72
+ // Spec issues
73
+ const specViolations = grouped.get("spec") || [];
74
+ if (specViolations.length > 0) {
75
+ summaryParts.push(`${specViolations.length} spec 관련 위반`);
76
+ explanationParts.push(
77
+ `## Spec 관련 문제\n` +
78
+ specViolations.map((v) => `- ${v.message}`).join("\n") +
79
+ `\n\n원인: spec 파일이 변경되었거나 lock 파일과 동기화가 필요합니다.`
80
+ );
81
+ }
82
+
83
+ // Generated issues
84
+ const generatedViolations = grouped.get("generated") || [];
85
+ if (generatedViolations.length > 0) {
86
+ summaryParts.push(`${generatedViolations.length} generated 파일 위반`);
87
+ explanationParts.push(
88
+ `## Generated 파일 문제\n` +
89
+ generatedViolations.map((v) => `- ${v.message}`).join("\n") +
90
+ `\n\n원인: generated 파일이 수동으로 수정되었거나 금지된 import가 있습니다.`
91
+ );
92
+ }
93
+
94
+ // Slot issues
95
+ const slotViolations = grouped.get("slot") || [];
96
+ if (slotViolations.length > 0) {
97
+ summaryParts.push(`${slotViolations.length} slot 파일 위반`);
98
+ explanationParts.push(
99
+ `## Slot 파일 문제\n` +
100
+ slotViolations.map((v) => `- ${v.message}`).join("\n") +
101
+ `\n\n원인: slot 파일이 없거나 필수 패턴이 누락되었습니다.`
102
+ );
103
+ }
104
+
105
+ // Contract issues
106
+ const contractViolations = grouped.get("contract") || [];
107
+ if (contractViolations.length > 0) {
108
+ summaryParts.push(`${contractViolations.length} contract 위반`);
109
+ explanationParts.push(
110
+ `## Contract 문제\n` +
111
+ contractViolations.map((v) => `- ${v.message}`).join("\n") +
112
+ `\n\n원인: contract와 slot 간의 불일치가 있습니다.`
113
+ );
114
+ }
115
+
116
+ // Unknown issues
117
+ const unknownViolations = grouped.get("unknown") || [];
118
+ if (unknownViolations.length > 0) {
119
+ summaryParts.push(`${unknownViolations.length} 기타 위반`);
120
+ explanationParts.push(
121
+ `## 기타 문제\n` + unknownViolations.map((v) => `- ${v.message}`).join("\n")
122
+ );
123
+ }
124
+
125
+ return {
126
+ summary: summaryParts.join(", "),
127
+ explanation: explanationParts.join("\n\n"),
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Generate template-based patch suggestions
133
+ */
134
+ export function generateTemplatePatches(
135
+ violations: GuardViolation[]
136
+ ): PatchSuggestion[] {
137
+ const patches: PatchSuggestion[] = [];
138
+
139
+ for (const violation of violations) {
140
+ switch (violation.ruleId) {
141
+ case GUARD_RULES.SPEC_HASH_MISMATCH?.id:
142
+ patches.push({
143
+ file: "spec/spec.lock.json",
144
+ description: "Spec lock 파일 갱신",
145
+ type: "command",
146
+ command: "bunx mandu spec-upsert",
147
+ confidence: 0.9,
148
+ });
149
+ break;
150
+
151
+ case GUARD_RULES.GENERATED_MANUAL_EDIT?.id:
152
+ patches.push({
153
+ file: violation.file,
154
+ description: "Generated 파일 재생성",
155
+ type: "command",
156
+ command: "bunx mandu generate",
157
+ confidence: 0.9,
158
+ });
159
+ break;
160
+
161
+ case GUARD_RULES.SLOT_NOT_FOUND?.id:
162
+ patches.push({
163
+ file: violation.file,
164
+ description: "Slot 파일 생성",
165
+ type: "command",
166
+ command: "bunx mandu generate",
167
+ confidence: 0.8,
168
+ });
169
+ break;
170
+
171
+ case GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id:
172
+ patches.push({
173
+ file: violation.file,
174
+ description: "Default export 추가",
175
+ type: "modify",
176
+ content: `// Add default export to your slot file:\nexport default Mandu.filling()...`,
177
+ confidence: 0.7,
178
+ });
179
+ break;
180
+
181
+ case GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id:
182
+ patches.push({
183
+ file: violation.file,
184
+ description: "Mandu.filling() 패턴 추가",
185
+ type: "modify",
186
+ content: `import { Mandu } from "@mandujs/core";\n\nexport default Mandu.filling()\n .get(async (ctx) => {\n return ctx.json({ message: "Hello" });\n });`,
187
+ confidence: 0.6,
188
+ });
189
+ break;
190
+
191
+ case GUARD_RULES.CONTRACT_METHOD_NOT_IMPLEMENTED?.id:
192
+ patches.push({
193
+ file: violation.file,
194
+ description: "Contract 메서드 구현 또는 sync",
195
+ type: "command",
196
+ command: `bunx mandu contract validate --verbose`,
197
+ confidence: 0.7,
198
+ });
199
+ break;
200
+
201
+ case GUARD_RULES.FORBIDDEN_IMPORT_IN_GENERATED?.id:
202
+ patches.push({
203
+ file: violation.file,
204
+ description: "Generated 파일 재생성 (금지된 import 제거)",
205
+ type: "command",
206
+ command: "bunx mandu generate",
207
+ confidence: 0.8,
208
+ });
209
+ break;
210
+
211
+ default:
212
+ // Generic suggestion based on violation.suggestion
213
+ if (violation.suggestion) {
214
+ if (violation.suggestion.includes("generate")) {
215
+ patches.push({
216
+ file: violation.file,
217
+ description: violation.suggestion,
218
+ type: "command",
219
+ command: "bunx mandu generate",
220
+ confidence: 0.5,
221
+ });
222
+ } else {
223
+ patches.push({
224
+ file: violation.file,
225
+ description: violation.suggestion,
226
+ type: "modify",
227
+ confidence: 0.4,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ return patches;
235
+ }
236
+
237
+ /**
238
+ * LLM-enhanced prompt for root cause analysis
239
+ */
240
+ export function buildAnalysisPrompt(violations: GuardViolation[]): string {
241
+ const violationList = violations
242
+ .map(
243
+ (v, i) =>
244
+ `${i + 1}. [${v.ruleId}] ${v.file}\n Message: ${v.message}\n Suggestion: ${v.suggestion || "None"}`
245
+ )
246
+ .join("\n\n");
247
+
248
+ return `You are analyzing Mandu framework Guard violations.
249
+ Mandu is a spec-driven fullstack framework where:
250
+ - spec/ contains route manifests and slot files
251
+ - generated/ contains auto-generated code (DO NOT EDIT)
252
+ - slots handle business logic
253
+ - contracts define API schemas
254
+
255
+ Analyze these violations and provide:
256
+ 1. A brief summary (1-2 sentences) of the root cause
257
+ 2. A detailed explanation of why these violations occurred
258
+ 3. The recommended fix order
259
+
260
+ Violations:
261
+ ${violationList}
262
+
263
+ Respond in Korean. Be concise and actionable.`;
264
+ }
265
+
266
+ /**
267
+ * Parse LLM analysis response
268
+ */
269
+ export function parseLLMAnalysis(
270
+ response: string,
271
+ fallback: { summary: string; explanation: string }
272
+ ): { summary: string; explanation: string } {
273
+ if (!response || response.trim().length === 0) {
274
+ return fallback;
275
+ }
276
+
277
+ // Try to extract summary (first paragraph or sentence)
278
+ const lines = response.split("\n").filter((l) => l.trim());
279
+ const summary = lines[0]?.trim() || fallback.summary;
280
+
281
+ // Rest is explanation
282
+ const explanation = lines.slice(1).join("\n").trim() || fallback.explanation;
283
+
284
+ return { summary, explanation };
285
+ }
286
+
287
+ /**
288
+ * Analyze Guard violations
289
+ *
290
+ * Uses LLM if available for enhanced analysis,
291
+ * falls back to template-based analysis otherwise.
292
+ */
293
+ export async function analyzeViolations(
294
+ violations: GuardViolation[],
295
+ options: { useLLM?: boolean } = {}
296
+ ): Promise<DoctorAnalysis> {
297
+ const { useLLM = true } = options;
298
+
299
+ // Store in memory
300
+ const memory = getSessionMemory();
301
+ memory.setGuardResult(violations);
302
+
303
+ // Template-based analysis (always available)
304
+ const templateAnalysis = analyzeRootCauseTemplate(violations);
305
+ const templatePatches = generateTemplatePatches(violations);
306
+
307
+ // Determine recommended next command
308
+ let nextCommand = "bunx mandu generate";
309
+ if (violations.some((v) => v.ruleId === GUARD_RULES.SPEC_HASH_MISMATCH?.id)) {
310
+ nextCommand = "bunx mandu spec-upsert";
311
+ }
312
+
313
+ // If LLM is not requested or not available, return template analysis
314
+ if (!useLLM) {
315
+ return {
316
+ violations,
317
+ summary: templateAnalysis.summary,
318
+ explanation: templateAnalysis.explanation,
319
+ patches: templatePatches,
320
+ llmAssisted: false,
321
+ nextCommand,
322
+ };
323
+ }
324
+
325
+ // Try LLM-enhanced analysis
326
+ const brain = getBrain();
327
+ const llmAvailable = await brain.isLLMAvailable();
328
+
329
+ if (!llmAvailable || !brain.enabled) {
330
+ return {
331
+ violations,
332
+ summary: templateAnalysis.summary,
333
+ explanation: templateAnalysis.explanation,
334
+ patches: templatePatches,
335
+ llmAssisted: false,
336
+ nextCommand,
337
+ };
338
+ }
339
+
340
+ // Generate LLM analysis
341
+ const prompt = buildAnalysisPrompt(violations);
342
+ const llmResponse = await brain.generate(prompt);
343
+
344
+ if (llmResponse) {
345
+ const llmAnalysis = parseLLMAnalysis(llmResponse, templateAnalysis);
346
+
347
+ return {
348
+ violations,
349
+ summary: llmAnalysis.summary,
350
+ explanation: llmAnalysis.explanation,
351
+ patches: templatePatches, // Still use template patches (LLM for analysis, not patches)
352
+ llmAssisted: true,
353
+ nextCommand,
354
+ };
355
+ }
356
+
357
+ // Fallback to template
358
+ return {
359
+ violations,
360
+ summary: templateAnalysis.summary,
361
+ explanation: templateAnalysis.explanation,
362
+ patches: templatePatches,
363
+ llmAssisted: false,
364
+ nextCommand,
365
+ };
366
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Brain v0.1 - Doctor Module
3
+ *
4
+ * Doctor handles error recovery:
5
+ * - Guard failure analysis
6
+ * - Root cause summary
7
+ * - Minimal patch suggestions
8
+ * - Works with or without LLM
9
+ */
10
+
11
+ export {
12
+ categorizeViolation,
13
+ analyzeRootCauseTemplate,
14
+ generateTemplatePatches,
15
+ buildAnalysisPrompt,
16
+ parseLLMAnalysis,
17
+ analyzeViolations,
18
+ type ViolationCategory,
19
+ } from "./analyzer";
20
+
21
+ export {
22
+ prioritizePatches,
23
+ deduplicatePatches,
24
+ generatePatchDescription,
25
+ applyPatch,
26
+ applyPatches,
27
+ generatePatchReport,
28
+ type PatchResult,
29
+ type BatchPatchResult,
30
+ } from "./patcher";
31
+
32
+ export {
33
+ formatViolation,
34
+ formatPatch,
35
+ printDoctorReport,
36
+ generateJsonReport,
37
+ generateMarkdownReport,
38
+ formatDoctorReport,
39
+ type ReportFormat,
40
+ } from "./reporter";