@mandujs/core 0.9.1 → 0.9.3
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 +1 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/architecture/analyzer.ts +541 -0
- package/src/brain/architecture/index.ts +8 -0
- package/src/brain/architecture/types.ts +195 -0
- package/src/brain/index.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/contract/client.test.ts +308 -0
- package/src/contract/client.ts +345 -0
- package/src/contract/handler.ts +270 -0
- package/src/contract/index.ts +137 -1
- package/src/contract/infer.test.ts +346 -0
- package/src/contract/types.ts +83 -0
- package/src/filling/filling.ts +5 -1
- package/src/filling/index.ts +1 -1
- package/src/index.ts +75 -0
package/package.json
CHANGED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.2 - Architecture Analyzer
|
|
3
|
+
*
|
|
4
|
+
* 프로젝트 아키텍처 규칙을 분석하고 위반을 감지
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ArchitectureConfig,
|
|
9
|
+
ArchitectureViolation,
|
|
10
|
+
CheckLocationRequest,
|
|
11
|
+
CheckLocationResult,
|
|
12
|
+
CheckImportRequest,
|
|
13
|
+
CheckImportResult,
|
|
14
|
+
ProjectStructure,
|
|
15
|
+
FolderInfo,
|
|
16
|
+
FolderRule,
|
|
17
|
+
ImportRule,
|
|
18
|
+
} from "./types";
|
|
19
|
+
import { getBrain } from "../brain";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import fs from "fs/promises";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mandu 기본 아키텍처 규칙
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_ARCHITECTURE_CONFIG: ArchitectureConfig = {
|
|
27
|
+
folders: {
|
|
28
|
+
"spec/": {
|
|
29
|
+
pattern: "spec/**",
|
|
30
|
+
description: "스펙 정의 전용. 구현 코드 금지",
|
|
31
|
+
allowedFiles: ["*.ts", "*.json"],
|
|
32
|
+
readonly: false,
|
|
33
|
+
},
|
|
34
|
+
"spec/slots/": {
|
|
35
|
+
pattern: "spec/slots/**",
|
|
36
|
+
description: "Slot 파일 전용",
|
|
37
|
+
allowedFiles: ["*.slot.ts", "*.client.ts"],
|
|
38
|
+
},
|
|
39
|
+
"spec/contracts/": {
|
|
40
|
+
pattern: "spec/contracts/**",
|
|
41
|
+
description: "Contract 파일 전용",
|
|
42
|
+
allowedFiles: ["*.contract.ts"],
|
|
43
|
+
},
|
|
44
|
+
"generated/": {
|
|
45
|
+
pattern: "**/generated/**",
|
|
46
|
+
description: "자동 생성 파일. 직접 수정 금지",
|
|
47
|
+
readonly: true,
|
|
48
|
+
},
|
|
49
|
+
"apps/server/": {
|
|
50
|
+
pattern: "apps/server/**",
|
|
51
|
+
description: "백엔드 로직",
|
|
52
|
+
allowedFiles: ["*.ts"],
|
|
53
|
+
},
|
|
54
|
+
"apps/web/": {
|
|
55
|
+
pattern: "apps/web/**",
|
|
56
|
+
description: "프론트엔드 컴포넌트",
|
|
57
|
+
allowedFiles: ["*.ts", "*.tsx"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
imports: [
|
|
61
|
+
{
|
|
62
|
+
source: "apps/web/**",
|
|
63
|
+
forbid: ["fs", "child_process", "path", "crypto"],
|
|
64
|
+
reason: "프론트엔드에서 Node.js 내장 모듈 사용 금지",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
source: "**/generated/**",
|
|
68
|
+
forbid: ["fs", "child_process"],
|
|
69
|
+
reason: "Generated 파일에서 시스템 모듈 사용 금지",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
source: "spec/**",
|
|
73
|
+
forbid: ["react", "react-dom"],
|
|
74
|
+
reason: "Spec 파일에서 React 사용 금지",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
layers: [
|
|
78
|
+
{
|
|
79
|
+
name: "spec",
|
|
80
|
+
folders: ["spec/**"],
|
|
81
|
+
canDependOn: [],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "generated",
|
|
85
|
+
folders: ["**/generated/**"],
|
|
86
|
+
canDependOn: ["spec"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "app",
|
|
90
|
+
folders: ["apps/**"],
|
|
91
|
+
canDependOn: ["spec", "generated"],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
naming: [
|
|
95
|
+
{
|
|
96
|
+
folder: "spec/slots/",
|
|
97
|
+
filePattern: "^[a-z][a-z0-9-]*\\.(slot|client)\\.ts$",
|
|
98
|
+
description: "Slot 파일은 kebab-case.slot.ts 또는 kebab-case.client.ts",
|
|
99
|
+
examples: ["users-list.slot.ts", "counter.client.ts"],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
folder: "spec/contracts/",
|
|
103
|
+
filePattern: "^[a-z][a-z0-9-]*\\.contract\\.ts$",
|
|
104
|
+
description: "Contract 파일은 kebab-case.contract.ts",
|
|
105
|
+
examples: ["users.contract.ts", "auth.contract.ts"],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 글로브 패턴을 정규식으로 변환
|
|
112
|
+
*/
|
|
113
|
+
function globToRegex(glob: string): RegExp {
|
|
114
|
+
const escaped = glob
|
|
115
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
116
|
+
.replace(/\*\*/g, "§§")
|
|
117
|
+
.replace(/\*/g, "[^/]*")
|
|
118
|
+
.replace(/§§/g, ".*")
|
|
119
|
+
.replace(/\?/g, ".");
|
|
120
|
+
return new RegExp(`^${escaped}$`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 경로가 패턴과 매칭되는지 확인
|
|
125
|
+
*/
|
|
126
|
+
function matchesPattern(filePath: string, pattern: string): boolean {
|
|
127
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
128
|
+
const regex = globToRegex(pattern);
|
|
129
|
+
return regex.test(normalizedPath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Architecture Analyzer 클래스
|
|
134
|
+
*/
|
|
135
|
+
export class ArchitectureAnalyzer {
|
|
136
|
+
private config: ArchitectureConfig;
|
|
137
|
+
private rootDir: string;
|
|
138
|
+
private projectStructure: ProjectStructure | null = null;
|
|
139
|
+
|
|
140
|
+
constructor(rootDir: string, config?: Partial<ArchitectureConfig>) {
|
|
141
|
+
this.rootDir = rootDir;
|
|
142
|
+
this.config = {
|
|
143
|
+
...DEFAULT_ARCHITECTURE_CONFIG,
|
|
144
|
+
...config,
|
|
145
|
+
folders: {
|
|
146
|
+
...DEFAULT_ARCHITECTURE_CONFIG.folders,
|
|
147
|
+
...config?.folders,
|
|
148
|
+
},
|
|
149
|
+
imports: [
|
|
150
|
+
...(DEFAULT_ARCHITECTURE_CONFIG.imports || []),
|
|
151
|
+
...(config?.imports || []),
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 설정 업데이트
|
|
158
|
+
*/
|
|
159
|
+
updateConfig(config: Partial<ArchitectureConfig>): void {
|
|
160
|
+
this.config = {
|
|
161
|
+
...this.config,
|
|
162
|
+
...config,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 현재 설정 반환
|
|
168
|
+
*/
|
|
169
|
+
getConfig(): ArchitectureConfig {
|
|
170
|
+
return this.config;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 파일 위치 검증
|
|
175
|
+
*/
|
|
176
|
+
async checkLocation(request: CheckLocationRequest): Promise<CheckLocationResult> {
|
|
177
|
+
const violations: ArchitectureViolation[] = [];
|
|
178
|
+
const normalizedPath = request.path.replace(/\\/g, "/");
|
|
179
|
+
|
|
180
|
+
// 1. readonly 폴더 검사
|
|
181
|
+
for (const [key, rule] of Object.entries(this.config.folders || {})) {
|
|
182
|
+
const folderRule = typeof rule === "string"
|
|
183
|
+
? { pattern: key, description: rule }
|
|
184
|
+
: rule;
|
|
185
|
+
|
|
186
|
+
if (folderRule.readonly && matchesPattern(normalizedPath, folderRule.pattern)) {
|
|
187
|
+
violations.push({
|
|
188
|
+
ruleId: "READONLY_FOLDER",
|
|
189
|
+
ruleType: "folder",
|
|
190
|
+
file: request.path,
|
|
191
|
+
message: `이 폴더는 수정 금지입니다: ${folderRule.description}`,
|
|
192
|
+
suggestion: "이 파일은 자동 생성됩니다. bunx mandu generate를 사용하세요.",
|
|
193
|
+
severity: "error",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. 네이밍 규칙 검사
|
|
199
|
+
for (const rule of this.config.naming || []) {
|
|
200
|
+
if (normalizedPath.startsWith(rule.folder.replace(/\\/g, "/"))) {
|
|
201
|
+
const fileName = path.basename(normalizedPath);
|
|
202
|
+
const regex = new RegExp(rule.filePattern);
|
|
203
|
+
|
|
204
|
+
if (!regex.test(fileName)) {
|
|
205
|
+
violations.push({
|
|
206
|
+
ruleId: "NAMING_CONVENTION",
|
|
207
|
+
ruleType: "naming",
|
|
208
|
+
file: request.path,
|
|
209
|
+
message: `네이밍 규칙 위반: ${rule.description}`,
|
|
210
|
+
suggestion: `예시: ${rule.examples?.join(", ") || "N/A"}`,
|
|
211
|
+
severity: "error",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 3. 허용된 파일 타입 검사
|
|
218
|
+
for (const [key, rule] of Object.entries(this.config.folders || {})) {
|
|
219
|
+
const folderRule = typeof rule === "string"
|
|
220
|
+
? { pattern: key, description: rule }
|
|
221
|
+
: rule;
|
|
222
|
+
|
|
223
|
+
if (matchesPattern(normalizedPath, folderRule.pattern)) {
|
|
224
|
+
if (folderRule.allowedFiles && folderRule.allowedFiles.length > 0) {
|
|
225
|
+
const fileName = path.basename(normalizedPath);
|
|
226
|
+
const isAllowed = folderRule.allowedFiles.some((pattern) => {
|
|
227
|
+
const regex = globToRegex(pattern);
|
|
228
|
+
return regex.test(fileName);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!isAllowed) {
|
|
232
|
+
violations.push({
|
|
233
|
+
ruleId: "DISALLOWED_FILE_TYPE",
|
|
234
|
+
ruleType: "folder",
|
|
235
|
+
file: request.path,
|
|
236
|
+
message: `이 폴더에서 허용되지 않는 파일 타입입니다`,
|
|
237
|
+
suggestion: `허용: ${folderRule.allowedFiles.join(", ")}`,
|
|
238
|
+
severity: "warning",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 4. 내용 기반 검사 (content가 제공된 경우)
|
|
246
|
+
if (request.content) {
|
|
247
|
+
const importViolations = await this.checkImports({
|
|
248
|
+
sourceFile: request.path,
|
|
249
|
+
imports: this.extractImports(request.content),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
for (const v of importViolations.violations) {
|
|
253
|
+
violations.push({
|
|
254
|
+
ruleId: "FORBIDDEN_IMPORT",
|
|
255
|
+
ruleType: "import",
|
|
256
|
+
file: request.path,
|
|
257
|
+
message: v.reason,
|
|
258
|
+
suggestion: v.suggestion,
|
|
259
|
+
severity: "error",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 5. LLM 제안 (위반이 있는 경우)
|
|
265
|
+
let suggestion: string | undefined;
|
|
266
|
+
let recommendedPath: string | undefined;
|
|
267
|
+
|
|
268
|
+
if (violations.length > 0) {
|
|
269
|
+
const brain = getBrain();
|
|
270
|
+
if (await brain.isLLMAvailable()) {
|
|
271
|
+
const llmSuggestion = await this.getLLMSuggestion(request, violations);
|
|
272
|
+
suggestion = llmSuggestion.suggestion;
|
|
273
|
+
recommendedPath = llmSuggestion.recommendedPath;
|
|
274
|
+
} else {
|
|
275
|
+
// 템플릿 기반 제안
|
|
276
|
+
recommendedPath = this.getTemplateRecommendedPath(request.path);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
allowed: violations.length === 0,
|
|
282
|
+
violations,
|
|
283
|
+
suggestion,
|
|
284
|
+
recommendedPath,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Import 검증
|
|
290
|
+
*/
|
|
291
|
+
async checkImports(request: CheckImportRequest): Promise<CheckImportResult> {
|
|
292
|
+
const violations: Array<{
|
|
293
|
+
import: string;
|
|
294
|
+
reason: string;
|
|
295
|
+
suggestion?: string;
|
|
296
|
+
}> = [];
|
|
297
|
+
|
|
298
|
+
const normalizedSource = request.sourceFile.replace(/\\/g, "/");
|
|
299
|
+
|
|
300
|
+
for (const importPath of request.imports) {
|
|
301
|
+
for (const rule of this.config.imports || []) {
|
|
302
|
+
if (matchesPattern(normalizedSource, rule.source)) {
|
|
303
|
+
// 금지된 import 검사
|
|
304
|
+
if (rule.forbid) {
|
|
305
|
+
for (const forbidden of rule.forbid) {
|
|
306
|
+
if (
|
|
307
|
+
importPath === forbidden ||
|
|
308
|
+
importPath.startsWith(`${forbidden}/`)
|
|
309
|
+
) {
|
|
310
|
+
violations.push({
|
|
311
|
+
import: importPath,
|
|
312
|
+
reason: rule.reason || `'${importPath}' import 금지`,
|
|
313
|
+
suggestion: this.getImportSuggestion(importPath, normalizedSource),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 허용된 import만 검사 (allow가 정의된 경우)
|
|
320
|
+
if (rule.allow && rule.allow.length > 0) {
|
|
321
|
+
const isAllowed = rule.allow.some((allowed) =>
|
|
322
|
+
matchesPattern(importPath, allowed)
|
|
323
|
+
);
|
|
324
|
+
if (!isAllowed) {
|
|
325
|
+
violations.push({
|
|
326
|
+
import: importPath,
|
|
327
|
+
reason: `'${importPath}'는 허용되지 않은 import입니다`,
|
|
328
|
+
suggestion: `허용된 패턴: ${rule.allow.join(", ")}`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
allowed: violations.length === 0,
|
|
338
|
+
violations,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 프로젝트 구조 인덱싱
|
|
344
|
+
*/
|
|
345
|
+
async indexProject(): Promise<ProjectStructure> {
|
|
346
|
+
const folders = await this.scanFolders(this.rootDir, 0, 3);
|
|
347
|
+
|
|
348
|
+
this.projectStructure = {
|
|
349
|
+
rootDir: this.rootDir,
|
|
350
|
+
folders,
|
|
351
|
+
config: this.config,
|
|
352
|
+
indexedAt: new Date().toISOString(),
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return this.projectStructure;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 프로젝트 구조 반환
|
|
360
|
+
*/
|
|
361
|
+
async getProjectStructure(): Promise<ProjectStructure> {
|
|
362
|
+
if (!this.projectStructure) {
|
|
363
|
+
return this.indexProject();
|
|
364
|
+
}
|
|
365
|
+
return this.projectStructure;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* 코드에서 import 문 추출
|
|
370
|
+
*/
|
|
371
|
+
private extractImports(content: string): string[] {
|
|
372
|
+
const imports: string[] = [];
|
|
373
|
+
|
|
374
|
+
// ES6 import
|
|
375
|
+
const importRegex = /import\s+(?:.*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
376
|
+
let match;
|
|
377
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
378
|
+
imports.push(match[1]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// require
|
|
382
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
383
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
384
|
+
imports.push(match[1]);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return imports;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 폴더 스캔
|
|
392
|
+
*/
|
|
393
|
+
private async scanFolders(
|
|
394
|
+
dir: string,
|
|
395
|
+
depth: number,
|
|
396
|
+
maxDepth: number
|
|
397
|
+
): Promise<FolderInfo[]> {
|
|
398
|
+
if (depth >= maxDepth) return [];
|
|
399
|
+
|
|
400
|
+
const folders: FolderInfo[] = [];
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
404
|
+
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (!entry.isDirectory()) continue;
|
|
407
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
408
|
+
|
|
409
|
+
const fullPath = path.join(dir, entry.name);
|
|
410
|
+
const relativePath = path.relative(this.rootDir, fullPath).replace(/\\/g, "/");
|
|
411
|
+
|
|
412
|
+
// 폴더 설명 찾기
|
|
413
|
+
let description: string | undefined;
|
|
414
|
+
for (const [key, rule] of Object.entries(this.config.folders || {})) {
|
|
415
|
+
const folderRule = typeof rule === "string" ? { pattern: key, description: rule } : rule;
|
|
416
|
+
if (matchesPattern(relativePath + "/", folderRule.pattern)) {
|
|
417
|
+
description = folderRule.description;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 파일 수 계산
|
|
423
|
+
const files = entries.filter((e) => e.isFile());
|
|
424
|
+
|
|
425
|
+
folders.push({
|
|
426
|
+
path: relativePath,
|
|
427
|
+
description,
|
|
428
|
+
fileCount: files.length,
|
|
429
|
+
children: await this.scanFolders(fullPath, depth + 1, maxDepth),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// 권한 없는 폴더 무시
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return folders;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Import에 대한 템플릿 제안
|
|
441
|
+
*/
|
|
442
|
+
private getImportSuggestion(importPath: string, sourceFile: string): string {
|
|
443
|
+
if (importPath === "fs") {
|
|
444
|
+
if (sourceFile.includes("apps/web")) {
|
|
445
|
+
return "프론트엔드에서는 fetch API를 사용하세요";
|
|
446
|
+
}
|
|
447
|
+
return "Bun.file() 또는 Bun.write()를 사용하세요";
|
|
448
|
+
}
|
|
449
|
+
if (importPath === "child_process") {
|
|
450
|
+
return "Bun.spawn() 또는 Bun.$를 사용하세요";
|
|
451
|
+
}
|
|
452
|
+
if (importPath === "path") {
|
|
453
|
+
return "import.meta.dir 또는 Bun.pathToFileURL을 사용하세요";
|
|
454
|
+
}
|
|
455
|
+
return "다른 모듈을 사용하세요";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 템플릿 기반 권장 경로
|
|
460
|
+
*/
|
|
461
|
+
private getTemplateRecommendedPath(filePath: string): string | undefined {
|
|
462
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
463
|
+
const fileName = path.basename(normalized);
|
|
464
|
+
|
|
465
|
+
if (fileName.endsWith(".slot.ts")) {
|
|
466
|
+
return `spec/slots/${fileName}`;
|
|
467
|
+
}
|
|
468
|
+
if (fileName.endsWith(".contract.ts")) {
|
|
469
|
+
return `spec/contracts/${fileName}`;
|
|
470
|
+
}
|
|
471
|
+
if (normalized.includes("generated/")) {
|
|
472
|
+
return undefined; // generated는 이동 불가
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* LLM 기반 제안
|
|
480
|
+
*/
|
|
481
|
+
private async getLLMSuggestion(
|
|
482
|
+
request: CheckLocationRequest,
|
|
483
|
+
violations: ArchitectureViolation[]
|
|
484
|
+
): Promise<{ suggestion?: string; recommendedPath?: string }> {
|
|
485
|
+
const brain = getBrain();
|
|
486
|
+
|
|
487
|
+
const prompt = `Mandu Framework 아키텍처 분석:
|
|
488
|
+
|
|
489
|
+
파일: ${request.path}
|
|
490
|
+
위반 사항:
|
|
491
|
+
${violations.map((v) => `- ${v.message}`).join("\n")}
|
|
492
|
+
|
|
493
|
+
프로젝트 구조 규칙:
|
|
494
|
+
${JSON.stringify(this.config.folders, null, 2)}
|
|
495
|
+
|
|
496
|
+
질문:
|
|
497
|
+
1. 이 파일의 올바른 위치는 어디인가요?
|
|
498
|
+
2. 어떻게 수정해야 하나요?
|
|
499
|
+
|
|
500
|
+
짧고 명확하게 답변하세요 (3줄 이내).`;
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const result = await brain.complete([
|
|
504
|
+
{ role: "user", content: prompt },
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
// 응답에서 경로 추출 시도
|
|
508
|
+
const pathMatch = result.content.match(/(?:spec\/|apps\/|packages\/)[^\s,)]+/);
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
suggestion: result.content,
|
|
512
|
+
recommendedPath: pathMatch?.[0],
|
|
513
|
+
};
|
|
514
|
+
} catch {
|
|
515
|
+
return {};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 글로벌 analyzer 인스턴스
|
|
522
|
+
*/
|
|
523
|
+
let globalAnalyzer: ArchitectureAnalyzer | null = null;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Architecture Analyzer 초기화
|
|
527
|
+
*/
|
|
528
|
+
export function initializeArchitectureAnalyzer(
|
|
529
|
+
rootDir: string,
|
|
530
|
+
config?: Partial<ArchitectureConfig>
|
|
531
|
+
): ArchitectureAnalyzer {
|
|
532
|
+
globalAnalyzer = new ArchitectureAnalyzer(rootDir, config);
|
|
533
|
+
return globalAnalyzer;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 글로벌 analyzer 반환
|
|
538
|
+
*/
|
|
539
|
+
export function getArchitectureAnalyzer(): ArchitectureAnalyzer | null {
|
|
540
|
+
return globalAnalyzer;
|
|
541
|
+
}
|