@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,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";
|