@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.
- package/package.json +9 -1
- package/src/brain/adapters/base.ts +120 -0
- package/src/brain/adapters/index.ts +8 -0
- package/src/brain/adapters/ollama.ts +249 -0
- package/src/brain/brain.ts +324 -0
- package/src/brain/doctor/analyzer.ts +366 -0
- package/src/brain/doctor/index.ts +40 -0
- package/src/brain/doctor/patcher.ts +349 -0
- package/src/brain/doctor/reporter.ts +336 -0
- package/src/brain/index.ts +45 -0
- package/src/brain/memory.ts +154 -0
- package/src/brain/permissions.ts +270 -0
- package/src/brain/types.ts +268 -0
- package/src/contract/contract.test.ts +381 -0
- package/src/contract/integration.test.ts +394 -0
- package/src/contract/validator.ts +113 -8
- package/src/generator/contract-glue.test.ts +211 -0
- package/src/guard/check.ts +51 -1
- package/src/guard/contract-guard.test.ts +303 -0
- package/src/guard/rules.ts +37 -0
- package/src/index.ts +2 -0
- package/src/openapi/openapi.test.ts +277 -0
- package/src/slot/validator.test.ts +203 -0
- package/src/slot/validator.ts +236 -17
- package/src/watcher/index.ts +44 -0
- package/src/watcher/reporter.ts +232 -0
- package/src/watcher/rules.ts +248 -0
- package/src/watcher/watcher.ts +330 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Doctor Patcher
|
|
3
|
+
*
|
|
4
|
+
* Generates and applies minimal patches to fix violations.
|
|
5
|
+
* Auto-apply is disabled by default - patches are suggestions only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PatchSuggestion } from "../types";
|
|
9
|
+
import type { DoctorAnalysis } from "../types";
|
|
10
|
+
import {
|
|
11
|
+
validatePatchSuggestion,
|
|
12
|
+
filterSafePatchSuggestions,
|
|
13
|
+
} from "../permissions";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import fs from "fs/promises";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Patch application result
|
|
19
|
+
*/
|
|
20
|
+
export interface PatchResult {
|
|
21
|
+
/** Whether the patch was applied */
|
|
22
|
+
applied: boolean;
|
|
23
|
+
/** The patch that was applied */
|
|
24
|
+
patch: PatchSuggestion;
|
|
25
|
+
/** Error message if failed */
|
|
26
|
+
error?: string;
|
|
27
|
+
/** Output from command (for command type) */
|
|
28
|
+
output?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Batch patch result
|
|
33
|
+
*/
|
|
34
|
+
export interface BatchPatchResult {
|
|
35
|
+
/** Total patches attempted */
|
|
36
|
+
total: number;
|
|
37
|
+
/** Successfully applied */
|
|
38
|
+
applied: number;
|
|
39
|
+
/** Failed patches */
|
|
40
|
+
failed: number;
|
|
41
|
+
/** Skipped (unsafe) patches */
|
|
42
|
+
skipped: number;
|
|
43
|
+
/** Individual results */
|
|
44
|
+
results: PatchResult[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Prioritize patches by confidence and type
|
|
49
|
+
*/
|
|
50
|
+
export function prioritizePatches(patches: PatchSuggestion[]): PatchSuggestion[] {
|
|
51
|
+
return [...patches].sort((a, b) => {
|
|
52
|
+
// Commands before modifications
|
|
53
|
+
if (a.type === "command" && b.type !== "command") return -1;
|
|
54
|
+
if (b.type === "command" && a.type !== "command") return 1;
|
|
55
|
+
|
|
56
|
+
// Higher confidence first
|
|
57
|
+
return b.confidence - a.confidence;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Deduplicate patches by file and type
|
|
63
|
+
*/
|
|
64
|
+
export function deduplicatePatches(patches: PatchSuggestion[]): PatchSuggestion[] {
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
const result: PatchSuggestion[] = [];
|
|
67
|
+
|
|
68
|
+
for (const patch of patches) {
|
|
69
|
+
const key = `${patch.file}:${patch.type}:${patch.command || ""}`;
|
|
70
|
+
if (!seen.has(key)) {
|
|
71
|
+
seen.add(key);
|
|
72
|
+
result.push(patch);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generate a minimal patch for a violation
|
|
81
|
+
*
|
|
82
|
+
* Returns a human-readable patch description.
|
|
83
|
+
*/
|
|
84
|
+
export function generatePatchDescription(patch: PatchSuggestion): string {
|
|
85
|
+
const confidenceLabel =
|
|
86
|
+
patch.confidence >= 0.8
|
|
87
|
+
? "높음"
|
|
88
|
+
: patch.confidence >= 0.5
|
|
89
|
+
? "보통"
|
|
90
|
+
: "낮음";
|
|
91
|
+
|
|
92
|
+
switch (patch.type) {
|
|
93
|
+
case "command":
|
|
94
|
+
return `[명령어 실행] ${patch.command}\n 대상: ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`;
|
|
95
|
+
|
|
96
|
+
case "add":
|
|
97
|
+
return `[파일 생성] ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`;
|
|
98
|
+
|
|
99
|
+
case "modify":
|
|
100
|
+
const lineInfo = patch.line ? ` (라인 ${patch.line})` : "";
|
|
101
|
+
return `[파일 수정] ${patch.file}${lineInfo}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`;
|
|
102
|
+
|
|
103
|
+
case "delete":
|
|
104
|
+
return `[파일 삭제] ${patch.file}\n 설명: ${patch.description}\n 신뢰도: ${confidenceLabel}`;
|
|
105
|
+
|
|
106
|
+
default:
|
|
107
|
+
return `[${patch.type}] ${patch.file}\n 설명: ${patch.description}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Apply a single patch
|
|
113
|
+
*
|
|
114
|
+
* NOTE: This is experimental and disabled by default.
|
|
115
|
+
* Only applies safe patches (commands like mandu generate).
|
|
116
|
+
*/
|
|
117
|
+
export async function applyPatch(
|
|
118
|
+
patch: PatchSuggestion,
|
|
119
|
+
rootDir: string
|
|
120
|
+
): Promise<PatchResult> {
|
|
121
|
+
// Validate patch first
|
|
122
|
+
const validation = validatePatchSuggestion(patch);
|
|
123
|
+
|
|
124
|
+
if (!validation.valid) {
|
|
125
|
+
return {
|
|
126
|
+
applied: false,
|
|
127
|
+
patch,
|
|
128
|
+
error: validation.reason || "Patch validation failed",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (validation.requiresConfirmation) {
|
|
133
|
+
return {
|
|
134
|
+
applied: false,
|
|
135
|
+
patch,
|
|
136
|
+
error: `Manual confirmation required: ${validation.reason}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
switch (patch.type) {
|
|
142
|
+
case "command": {
|
|
143
|
+
if (!patch.command) {
|
|
144
|
+
return {
|
|
145
|
+
applied: false,
|
|
146
|
+
patch,
|
|
147
|
+
error: "No command specified",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only allow mandu commands for safety
|
|
152
|
+
if (!patch.command.startsWith("bunx mandu")) {
|
|
153
|
+
return {
|
|
154
|
+
applied: false,
|
|
155
|
+
patch,
|
|
156
|
+
error: "Only mandu commands are allowed for auto-apply",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Execute the command
|
|
161
|
+
const proc = Bun.spawn(patch.command.split(" "), {
|
|
162
|
+
cwd: rootDir,
|
|
163
|
+
stdout: "pipe",
|
|
164
|
+
stderr: "pipe",
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const stdout = await new Response(proc.stdout).text();
|
|
168
|
+
const stderr = await new Response(proc.stderr).text();
|
|
169
|
+
const exitCode = await proc.exited;
|
|
170
|
+
|
|
171
|
+
if (exitCode !== 0) {
|
|
172
|
+
return {
|
|
173
|
+
applied: false,
|
|
174
|
+
patch,
|
|
175
|
+
error: stderr || `Command exited with code ${exitCode}`,
|
|
176
|
+
output: stdout,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
applied: true,
|
|
182
|
+
patch,
|
|
183
|
+
output: stdout,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
case "add":
|
|
188
|
+
case "modify": {
|
|
189
|
+
if (!patch.content) {
|
|
190
|
+
return {
|
|
191
|
+
applied: false,
|
|
192
|
+
patch,
|
|
193
|
+
error: "No content specified for file operation",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const filePath = path.join(rootDir, patch.file);
|
|
198
|
+
|
|
199
|
+
// Ensure directory exists
|
|
200
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
201
|
+
|
|
202
|
+
// Write content
|
|
203
|
+
await fs.writeFile(filePath, patch.content, "utf-8");
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
applied: true,
|
|
207
|
+
patch,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case "delete": {
|
|
212
|
+
const filePath = path.join(rootDir, patch.file);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await fs.unlink(filePath);
|
|
216
|
+
return {
|
|
217
|
+
applied: true,
|
|
218
|
+
patch,
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
applied: false,
|
|
223
|
+
patch,
|
|
224
|
+
error:
|
|
225
|
+
error instanceof Error
|
|
226
|
+
? error.message
|
|
227
|
+
: "Failed to delete file",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
default:
|
|
233
|
+
return {
|
|
234
|
+
applied: false,
|
|
235
|
+
patch,
|
|
236
|
+
error: `Unknown patch type: ${patch.type}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
applied: false,
|
|
242
|
+
patch,
|
|
243
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Apply multiple patches
|
|
250
|
+
*
|
|
251
|
+
* NOTE: Auto-apply is experimental and disabled by default.
|
|
252
|
+
*/
|
|
253
|
+
export async function applyPatches(
|
|
254
|
+
patches: PatchSuggestion[],
|
|
255
|
+
rootDir: string,
|
|
256
|
+
options: { dryRun?: boolean; minConfidence?: number } = {}
|
|
257
|
+
): Promise<BatchPatchResult> {
|
|
258
|
+
const { dryRun = true, minConfidence = 0.5 } = options;
|
|
259
|
+
|
|
260
|
+
// Filter safe patches
|
|
261
|
+
const safePatches = filterSafePatchSuggestions(patches);
|
|
262
|
+
|
|
263
|
+
// Filter by confidence
|
|
264
|
+
const confidentPatches = safePatches.filter(
|
|
265
|
+
(p) => p.confidence >= minConfidence
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Deduplicate and prioritize
|
|
269
|
+
const orderedPatches = prioritizePatches(deduplicatePatches(confidentPatches));
|
|
270
|
+
|
|
271
|
+
const results: PatchResult[] = [];
|
|
272
|
+
let applied = 0;
|
|
273
|
+
let failed = 0;
|
|
274
|
+
const skipped = patches.length - orderedPatches.length;
|
|
275
|
+
|
|
276
|
+
for (const patch of orderedPatches) {
|
|
277
|
+
if (dryRun) {
|
|
278
|
+
// In dry run mode, just report what would be done
|
|
279
|
+
results.push({
|
|
280
|
+
applied: false,
|
|
281
|
+
patch,
|
|
282
|
+
error: "Dry run mode - patch not applied",
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const result = await applyPatch(patch, rootDir);
|
|
288
|
+
results.push(result);
|
|
289
|
+
|
|
290
|
+
if (result.applied) {
|
|
291
|
+
applied++;
|
|
292
|
+
} else {
|
|
293
|
+
failed++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
total: patches.length,
|
|
299
|
+
applied,
|
|
300
|
+
failed,
|
|
301
|
+
skipped,
|
|
302
|
+
results,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Generate a patch report from analysis
|
|
308
|
+
*/
|
|
309
|
+
export function generatePatchReport(analysis: DoctorAnalysis): string {
|
|
310
|
+
const lines: string[] = [];
|
|
311
|
+
|
|
312
|
+
lines.push("# Mandu Doctor Report");
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push(`## 요약`);
|
|
315
|
+
lines.push(analysis.summary);
|
|
316
|
+
lines.push("");
|
|
317
|
+
|
|
318
|
+
if (analysis.explanation) {
|
|
319
|
+
lines.push(`## 상세 분석`);
|
|
320
|
+
lines.push(analysis.explanation);
|
|
321
|
+
lines.push("");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (analysis.patches.length > 0) {
|
|
325
|
+
lines.push(`## 제안된 패치 (${analysis.patches.length}개)`);
|
|
326
|
+
lines.push("");
|
|
327
|
+
|
|
328
|
+
const prioritized = prioritizePatches(analysis.patches);
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < prioritized.length; i++) {
|
|
331
|
+
lines.push(`### ${i + 1}. ${prioritized[i].description}`);
|
|
332
|
+
lines.push(generatePatchDescription(prioritized[i]));
|
|
333
|
+
lines.push("");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (analysis.nextCommand) {
|
|
338
|
+
lines.push(`## 권장 다음 명령어`);
|
|
339
|
+
lines.push("```bash");
|
|
340
|
+
lines.push(analysis.nextCommand);
|
|
341
|
+
lines.push("```");
|
|
342
|
+
lines.push("");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lines.push(`---`);
|
|
346
|
+
lines.push(`LLM 지원: ${analysis.llmAssisted ? "예" : "아니오"}`);
|
|
347
|
+
|
|
348
|
+
return lines.join("\n");
|
|
349
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Doctor Reporter
|
|
3
|
+
*
|
|
4
|
+
* Formats and outputs Doctor analysis results.
|
|
5
|
+
* Works with or without LLM - always provides actionable output.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DoctorAnalysis, PatchSuggestion } from "../types";
|
|
9
|
+
import type { GuardViolation } from "../../guard/rules";
|
|
10
|
+
import { prioritizePatches } from "./patcher";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ANSI color codes for terminal output
|
|
14
|
+
*/
|
|
15
|
+
const colors = {
|
|
16
|
+
reset: "\x1b[0m",
|
|
17
|
+
bright: "\x1b[1m",
|
|
18
|
+
dim: "\x1b[2m",
|
|
19
|
+
red: "\x1b[31m",
|
|
20
|
+
green: "\x1b[32m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
22
|
+
blue: "\x1b[34m",
|
|
23
|
+
magenta: "\x1b[35m",
|
|
24
|
+
cyan: "\x1b[36m",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if colors should be used
|
|
29
|
+
*/
|
|
30
|
+
function useColors(): boolean {
|
|
31
|
+
// Check for NO_COLOR environment variable
|
|
32
|
+
if (process.env.NO_COLOR) return false;
|
|
33
|
+
|
|
34
|
+
// Check if stdout is a TTY
|
|
35
|
+
if (typeof process?.stdout?.isTTY === "boolean") {
|
|
36
|
+
return process.stdout.isTTY;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply color if enabled
|
|
44
|
+
*/
|
|
45
|
+
function color(text: string, colorCode: string): string {
|
|
46
|
+
if (!useColors()) return text;
|
|
47
|
+
return `${colorCode}${text}${colors.reset}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format a violation for terminal output
|
|
52
|
+
*/
|
|
53
|
+
export function formatViolation(violation: GuardViolation): string {
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
|
|
56
|
+
const severity = violation.severity || "error";
|
|
57
|
+
const icon = severity === "error" ? "❌" : "⚠️";
|
|
58
|
+
const colorCode = severity === "error" ? colors.red : colors.yellow;
|
|
59
|
+
|
|
60
|
+
lines.push(
|
|
61
|
+
`${icon} ${color(`[${violation.ruleId}]`, colorCode)} ${violation.file}`
|
|
62
|
+
);
|
|
63
|
+
lines.push(` ${violation.message}`);
|
|
64
|
+
|
|
65
|
+
if (violation.suggestion) {
|
|
66
|
+
lines.push(` ${color("💡", colors.cyan)} ${violation.suggestion}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (violation.line) {
|
|
70
|
+
lines.push(` ${color("📍", colors.dim)} Line ${violation.line}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format a patch suggestion for terminal output
|
|
78
|
+
*/
|
|
79
|
+
export function formatPatch(patch: PatchSuggestion, index: number): string {
|
|
80
|
+
const lines: string[] = [];
|
|
81
|
+
|
|
82
|
+
const confidenceIcon =
|
|
83
|
+
patch.confidence >= 0.8 ? "🟢" : patch.confidence >= 0.5 ? "🟡" : "🔴";
|
|
84
|
+
|
|
85
|
+
lines.push(
|
|
86
|
+
`${color(`${index}.`, colors.bright)} ${patch.description} ${confidenceIcon}`
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
switch (patch.type) {
|
|
90
|
+
case "command":
|
|
91
|
+
lines.push(
|
|
92
|
+
` ${color("$ ", colors.dim)}${color(patch.command || "", colors.cyan)}`
|
|
93
|
+
);
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case "add":
|
|
97
|
+
lines.push(` ${color("+ 파일 생성:", colors.green)} ${patch.file}`);
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case "modify":
|
|
101
|
+
lines.push(` ${color("~ 파일 수정:", colors.yellow)} ${patch.file}`);
|
|
102
|
+
if (patch.line) {
|
|
103
|
+
lines.push(` ${color("📍", colors.dim)} Line ${patch.line}`);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case "delete":
|
|
108
|
+
lines.push(` ${color("- 파일 삭제:", colors.red)} ${patch.file}`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Print Doctor analysis to console
|
|
117
|
+
*/
|
|
118
|
+
export function printDoctorReport(analysis: DoctorAnalysis): void {
|
|
119
|
+
const { violations, summary, explanation, patches, llmAssisted, nextCommand } =
|
|
120
|
+
analysis;
|
|
121
|
+
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(color("🩺 Mandu Doctor Report", colors.bright + colors.blue));
|
|
124
|
+
console.log(color("─".repeat(50), colors.dim));
|
|
125
|
+
console.log();
|
|
126
|
+
|
|
127
|
+
// Summary
|
|
128
|
+
console.log(color("📋 요약", colors.bright));
|
|
129
|
+
console.log(` ${summary}`);
|
|
130
|
+
console.log();
|
|
131
|
+
|
|
132
|
+
// Violations
|
|
133
|
+
if (violations.length > 0) {
|
|
134
|
+
console.log(
|
|
135
|
+
color(`🔍 발견된 위반 (${violations.length}개)`, colors.bright)
|
|
136
|
+
);
|
|
137
|
+
console.log();
|
|
138
|
+
|
|
139
|
+
for (const violation of violations) {
|
|
140
|
+
console.log(formatViolation(violation));
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Detailed explanation
|
|
146
|
+
if (explanation) {
|
|
147
|
+
console.log(color("📖 상세 분석", colors.bright));
|
|
148
|
+
console.log();
|
|
149
|
+
|
|
150
|
+
// Format explanation with proper indentation
|
|
151
|
+
const explLines = explanation.split("\n");
|
|
152
|
+
for (const line of explLines) {
|
|
153
|
+
if (line.startsWith("##")) {
|
|
154
|
+
console.log(color(line.replace("## ", "▸ "), colors.cyan));
|
|
155
|
+
} else if (line.trim()) {
|
|
156
|
+
console.log(` ${line}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Patch suggestions
|
|
165
|
+
if (patches.length > 0) {
|
|
166
|
+
console.log(color(`💊 제안된 수정 (${patches.length}개)`, colors.bright));
|
|
167
|
+
console.log();
|
|
168
|
+
|
|
169
|
+
const prioritized = prioritizePatches(patches);
|
|
170
|
+
|
|
171
|
+
for (let i = 0; i < prioritized.length; i++) {
|
|
172
|
+
console.log(formatPatch(prioritized[i], i + 1));
|
|
173
|
+
console.log();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Next command
|
|
178
|
+
if (nextCommand) {
|
|
179
|
+
console.log(color("▶ 권장 다음 명령어", colors.bright));
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(` ${color("$ ", colors.dim)}${color(nextCommand, colors.green)}`);
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Footer
|
|
186
|
+
console.log(color("─".repeat(50), colors.dim));
|
|
187
|
+
console.log(
|
|
188
|
+
`${color("ℹ️", colors.cyan)} LLM 지원: ${llmAssisted ? color("예", colors.green) : color("아니오", colors.yellow)}`
|
|
189
|
+
);
|
|
190
|
+
console.log();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Generate a JSON report for MCP/API consumption
|
|
195
|
+
*/
|
|
196
|
+
export function generateJsonReport(analysis: DoctorAnalysis): string {
|
|
197
|
+
return JSON.stringify(
|
|
198
|
+
{
|
|
199
|
+
summary: analysis.summary,
|
|
200
|
+
violationCount: analysis.violations.length,
|
|
201
|
+
violations: analysis.violations.map((v) => ({
|
|
202
|
+
ruleId: v.ruleId,
|
|
203
|
+
file: v.file,
|
|
204
|
+
message: v.message,
|
|
205
|
+
suggestion: v.suggestion,
|
|
206
|
+
line: v.line,
|
|
207
|
+
severity: v.severity || "error",
|
|
208
|
+
})),
|
|
209
|
+
patches: analysis.patches.map((p) => ({
|
|
210
|
+
file: p.file,
|
|
211
|
+
type: p.type,
|
|
212
|
+
description: p.description,
|
|
213
|
+
command: p.command,
|
|
214
|
+
confidence: p.confidence,
|
|
215
|
+
})),
|
|
216
|
+
nextCommand: analysis.nextCommand,
|
|
217
|
+
llmAssisted: analysis.llmAssisted,
|
|
218
|
+
},
|
|
219
|
+
null,
|
|
220
|
+
2
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate a Markdown report
|
|
226
|
+
*/
|
|
227
|
+
export function generateMarkdownReport(analysis: DoctorAnalysis): string {
|
|
228
|
+
const lines: string[] = [];
|
|
229
|
+
|
|
230
|
+
lines.push("# 🩺 Mandu Doctor Report");
|
|
231
|
+
lines.push("");
|
|
232
|
+
|
|
233
|
+
lines.push("## 📋 요약");
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push(analysis.summary);
|
|
236
|
+
lines.push("");
|
|
237
|
+
|
|
238
|
+
if (analysis.violations.length > 0) {
|
|
239
|
+
lines.push(`## 🔍 발견된 위반 (${analysis.violations.length}개)`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
|
|
242
|
+
for (const v of analysis.violations) {
|
|
243
|
+
const severity = v.severity || "error";
|
|
244
|
+
const icon = severity === "error" ? "❌" : "⚠️";
|
|
245
|
+
|
|
246
|
+
lines.push(`### ${icon} ${v.ruleId}`);
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push(`- **파일**: \`${v.file}\``);
|
|
249
|
+
lines.push(`- **메시지**: ${v.message}`);
|
|
250
|
+
if (v.suggestion) {
|
|
251
|
+
lines.push(`- **제안**: ${v.suggestion}`);
|
|
252
|
+
}
|
|
253
|
+
if (v.line) {
|
|
254
|
+
lines.push(`- **라인**: ${v.line}`);
|
|
255
|
+
}
|
|
256
|
+
lines.push("");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (analysis.explanation) {
|
|
261
|
+
lines.push("## 📖 상세 분석");
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push(analysis.explanation);
|
|
264
|
+
lines.push("");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (analysis.patches.length > 0) {
|
|
268
|
+
lines.push(`## 💊 제안된 수정 (${analysis.patches.length}개)`);
|
|
269
|
+
lines.push("");
|
|
270
|
+
|
|
271
|
+
const prioritized = prioritizePatches(analysis.patches);
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < prioritized.length; i++) {
|
|
274
|
+
const p = prioritized[i];
|
|
275
|
+
const confidenceLabel =
|
|
276
|
+
p.confidence >= 0.8 ? "🟢 높음" : p.confidence >= 0.5 ? "🟡 보통" : "🔴 낮음";
|
|
277
|
+
|
|
278
|
+
lines.push(`### ${i + 1}. ${p.description}`);
|
|
279
|
+
lines.push("");
|
|
280
|
+
lines.push(`- **타입**: ${p.type}`);
|
|
281
|
+
lines.push(`- **대상**: \`${p.file}\``);
|
|
282
|
+
lines.push(`- **신뢰도**: ${confidenceLabel}`);
|
|
283
|
+
|
|
284
|
+
if (p.command) {
|
|
285
|
+
lines.push("");
|
|
286
|
+
lines.push("```bash");
|
|
287
|
+
lines.push(p.command);
|
|
288
|
+
lines.push("```");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
lines.push("");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (analysis.nextCommand) {
|
|
296
|
+
lines.push("## ▶ 권장 다음 명령어");
|
|
297
|
+
lines.push("");
|
|
298
|
+
lines.push("```bash");
|
|
299
|
+
lines.push(analysis.nextCommand);
|
|
300
|
+
lines.push("```");
|
|
301
|
+
lines.push("");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
lines.push("---");
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push(
|
|
307
|
+
`*LLM 지원: ${analysis.llmAssisted ? "예" : "아니오"}*`
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Format output based on requested format
|
|
315
|
+
*/
|
|
316
|
+
export type ReportFormat = "console" | "json" | "markdown";
|
|
317
|
+
|
|
318
|
+
export function formatDoctorReport(
|
|
319
|
+
analysis: DoctorAnalysis,
|
|
320
|
+
format: ReportFormat = "console"
|
|
321
|
+
): string | void {
|
|
322
|
+
switch (format) {
|
|
323
|
+
case "console":
|
|
324
|
+
printDoctorReport(analysis);
|
|
325
|
+
return;
|
|
326
|
+
|
|
327
|
+
case "json":
|
|
328
|
+
return generateJsonReport(analysis);
|
|
329
|
+
|
|
330
|
+
case "markdown":
|
|
331
|
+
return generateMarkdownReport(analysis);
|
|
332
|
+
|
|
333
|
+
default:
|
|
334
|
+
printDoctorReport(analysis);
|
|
335
|
+
}
|
|
336
|
+
}
|