@mandujs/core 0.9.46 → 0.11.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.
Files changed (56) hide show
  1. package/README.md +79 -10
  2. package/package.json +1 -1
  3. package/src/brain/doctor/config-analyzer.ts +498 -0
  4. package/src/brain/doctor/index.ts +10 -0
  5. package/src/change/snapshot.ts +46 -1
  6. package/src/change/types.ts +13 -0
  7. package/src/config/index.ts +9 -2
  8. package/src/config/mcp-ref.ts +348 -0
  9. package/src/config/mcp-status.ts +348 -0
  10. package/src/config/metadata.test.ts +308 -0
  11. package/src/config/metadata.ts +293 -0
  12. package/src/config/symbols.ts +144 -0
  13. package/src/config/validate.ts +122 -65
  14. package/src/config/watcher.ts +311 -0
  15. package/src/contract/index.ts +26 -25
  16. package/src/contract/protection.ts +364 -0
  17. package/src/error/domains.ts +265 -0
  18. package/src/error/index.ts +25 -13
  19. package/src/errors/extractor.ts +409 -0
  20. package/src/errors/index.ts +19 -0
  21. package/src/filling/context.ts +29 -1
  22. package/src/filling/deps.ts +238 -0
  23. package/src/filling/filling.ts +94 -8
  24. package/src/filling/index.ts +18 -0
  25. package/src/guard/analyzer.ts +7 -2
  26. package/src/guard/config-guard.ts +281 -0
  27. package/src/guard/decision-memory.test.ts +293 -0
  28. package/src/guard/decision-memory.ts +532 -0
  29. package/src/guard/healing.test.ts +259 -0
  30. package/src/guard/healing.ts +874 -0
  31. package/src/guard/index.ts +119 -0
  32. package/src/guard/negotiation.test.ts +282 -0
  33. package/src/guard/negotiation.ts +975 -0
  34. package/src/guard/semantic-slots.test.ts +379 -0
  35. package/src/guard/semantic-slots.ts +796 -0
  36. package/src/index.ts +4 -1
  37. package/src/lockfile/generate.ts +259 -0
  38. package/src/lockfile/index.ts +186 -0
  39. package/src/lockfile/lockfile.test.ts +410 -0
  40. package/src/lockfile/types.ts +184 -0
  41. package/src/lockfile/validate.ts +308 -0
  42. package/src/logging/index.ts +22 -0
  43. package/src/logging/transports.ts +365 -0
  44. package/src/plugins/index.ts +38 -0
  45. package/src/plugins/registry.ts +377 -0
  46. package/src/plugins/types.ts +363 -0
  47. package/src/runtime/security.ts +155 -0
  48. package/src/runtime/server.ts +318 -256
  49. package/src/runtime/session-key.ts +328 -0
  50. package/src/utils/differ.test.ts +342 -0
  51. package/src/utils/differ.ts +482 -0
  52. package/src/utils/hasher.test.ts +326 -0
  53. package/src/utils/hasher.ts +319 -0
  54. package/src/utils/index.ts +29 -0
  55. package/src/utils/safe-io.ts +188 -0
  56. package/src/utils/string-safe.ts +298 -0
@@ -0,0 +1,975 @@
1
+ /**
2
+ * Architecture Negotiation - 아키텍처 협상 시스템
3
+ *
4
+ * AI가 기능 구현 전에 프레임워크와 "협상"하여 최적의 구조를 받아옴
5
+ *
6
+ * @module guard/negotiation
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { negotiate, generateScaffold } from "@mandujs/core/guard";
11
+ *
12
+ * const plan = await negotiate({
13
+ * intent: "사용자 인증 기능 추가",
14
+ * requirements: ["JWT 기반", "리프레시 토큰"],
15
+ * constraints: ["기존 User 모델 활용"],
16
+ * }, projectRoot);
17
+ *
18
+ * if (plan.approved) {
19
+ * await generateScaffold(plan.structure, projectRoot);
20
+ * }
21
+ * ```
22
+ */
23
+
24
+ import { join, dirname } from "path";
25
+ import { mkdir, writeFile, readdir, stat } from "fs/promises";
26
+ import { searchDecisions, type ArchitectureDecision } from "./decision-memory";
27
+ import { getPreset, type GuardPreset, type PresetDefinition } from "./presets";
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ // Types
31
+ // ═══════════════════════════════════════════════════════════════════════════
32
+
33
+ /**
34
+ * 협상 요청
35
+ */
36
+ export interface NegotiationRequest {
37
+ /** 구현하려는 기능의 의도 */
38
+ intent: string;
39
+
40
+ /** 요구사항 목록 */
41
+ requirements?: string[];
42
+
43
+ /** 제약 조건 */
44
+ constraints?: string[];
45
+
46
+ /** 사용할 프리셋 (미지정 시 프로젝트 설정 사용) */
47
+ preset?: GuardPreset;
48
+
49
+ /** 기능 카테고리 (자동 감지 시도) */
50
+ category?: FeatureCategory;
51
+
52
+ /** 추가 컨텍스트 */
53
+ context?: string;
54
+ }
55
+
56
+ /**
57
+ * 기능 카테고리
58
+ */
59
+ export type FeatureCategory =
60
+ | "auth" // 인증/인가
61
+ | "crud" // CRUD 작업
62
+ | "api" // API 엔드포인트
63
+ | "ui" // UI 컴포넌트
64
+ | "integration" // 외부 서비스 연동
65
+ | "data" // 데이터 처리
66
+ | "util" // 유틸리티
67
+ | "config" // 설정
68
+ | "other"; // 기타
69
+
70
+ /**
71
+ * 디렉토리 구조 제안
72
+ */
73
+ export interface DirectoryProposal {
74
+ /** 디렉토리 경로 */
75
+ path: string;
76
+
77
+ /** 목적 설명 */
78
+ purpose: string;
79
+
80
+ /** 생성할 파일들 */
81
+ files: FileProposal[];
82
+
83
+ /** 레이어 (FSD/Clean 등) */
84
+ layer?: string;
85
+ }
86
+
87
+ /**
88
+ * 파일 제안
89
+ */
90
+ export interface FileProposal {
91
+ /** 파일명 */
92
+ name: string;
93
+
94
+ /** 목적 */
95
+ purpose: string;
96
+
97
+ /** 템플릿 타입 */
98
+ template?: FileTemplate;
99
+
100
+ /** 슬롯 여부 */
101
+ isSlot?: boolean;
102
+
103
+ /** 권장 제약 조건 */
104
+ suggestedConstraints?: string[];
105
+ }
106
+
107
+ /**
108
+ * 파일 템플릿 타입
109
+ */
110
+ export type FileTemplate =
111
+ | "service"
112
+ | "repository"
113
+ | "usecase"
114
+ | "controller"
115
+ | "route"
116
+ | "component"
117
+ | "hook"
118
+ | "util"
119
+ | "type"
120
+ | "test"
121
+ | "slot";
122
+
123
+ /**
124
+ * 협상 응답
125
+ */
126
+ export interface NegotiationResponse {
127
+ /** 승인 여부 */
128
+ approved: boolean;
129
+
130
+ /** 승인 거부 사유 (approved=false일 때) */
131
+ rejectionReason?: string;
132
+
133
+ /** 제안된 구조 */
134
+ structure: DirectoryProposal[];
135
+
136
+ /** 생성할 슬롯 목록 */
137
+ slots: SlotProposal[];
138
+
139
+ /** 경고 사항 */
140
+ warnings: string[];
141
+
142
+ /** 권장 사항 */
143
+ recommendations: string[];
144
+
145
+ /** 관련 기존 결정 */
146
+ relatedDecisions: RelatedDecision[];
147
+
148
+ /** 예상 파일 수 */
149
+ estimatedFiles: number;
150
+
151
+ /** 사용된 프리셋 */
152
+ preset: GuardPreset;
153
+
154
+ /** 다음 단계 안내 */
155
+ nextSteps: string[];
156
+ }
157
+
158
+ /**
159
+ * 슬롯 제안
160
+ */
161
+ export interface SlotProposal {
162
+ /** 슬롯 경로 */
163
+ path: string;
164
+
165
+ /** 목적 */
166
+ purpose: string;
167
+
168
+ /** 권장 제약 조건 */
169
+ constraints?: string[];
170
+
171
+ /** 필요한 import */
172
+ suggestedImports?: string[];
173
+ }
174
+
175
+ /**
176
+ * 관련 결정 요약
177
+ */
178
+ export interface RelatedDecision {
179
+ /** 결정 ID */
180
+ id: string;
181
+
182
+ /** 제목 */
183
+ title: string;
184
+
185
+ /** 핵심 내용 요약 */
186
+ summary: string;
187
+
188
+ /** 관련성 설명 */
189
+ relevance: string;
190
+ }
191
+
192
+ /**
193
+ * Scaffold 생성 결과
194
+ */
195
+ export interface ScaffoldResult {
196
+ /** 성공 여부 */
197
+ success: boolean;
198
+
199
+ /** 생성된 디렉토리 */
200
+ createdDirs: string[];
201
+
202
+ /** 생성된 파일 */
203
+ createdFiles: string[];
204
+
205
+ /** 건너뛴 파일 (이미 존재) */
206
+ skippedFiles: string[];
207
+
208
+ /** 에러 메시지 */
209
+ errors: string[];
210
+ }
211
+
212
+ // ═══════════════════════════════════════════════════════════════════════════
213
+ // Category Detection
214
+ // ═══════════════════════════════════════════════════════════════════════════
215
+
216
+ /**
217
+ * 키워드 기반 카테고리 매핑
218
+ */
219
+ const CATEGORY_KEYWORDS: Record<FeatureCategory, string[]> = {
220
+ auth: ["인증", "로그인", "로그아웃", "회원가입", "비밀번호", "토큰", "jwt", "oauth", "session", "auth", "login", "signup", "password"],
221
+ crud: ["생성", "조회", "수정", "삭제", "목록", "create", "read", "update", "delete", "list", "crud"],
222
+ api: ["api", "엔드포인트", "endpoint", "rest", "graphql", "route"],
223
+ ui: ["컴포넌트", "페이지", "화면", "폼", "버튼", "component", "page", "form", "button", "modal", "ui"],
224
+ integration: ["연동", "통합", "외부", "third-party", "integration", "webhook", "stripe", "payment", "email", "sms"],
225
+ data: ["데이터", "처리", "변환", "마이그레이션", "data", "transform", "migration", "import", "export"],
226
+ util: ["유틸", "헬퍼", "공통", "util", "helper", "common", "shared"],
227
+ config: ["설정", "환경", "config", "env", "setting"],
228
+ other: [],
229
+ };
230
+
231
+ /**
232
+ * 의도에서 카테고리 자동 감지
233
+ */
234
+ export function detectCategory(intent: string): FeatureCategory {
235
+ const normalizedIntent = intent.toLowerCase();
236
+
237
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
238
+ if (category === "other") continue;
239
+ if (keywords.some((kw) => normalizedIntent.includes(kw))) {
240
+ return category as FeatureCategory;
241
+ }
242
+ }
243
+
244
+ return "other";
245
+ }
246
+
247
+ // ═══════════════════════════════════════════════════════════════════════════
248
+ // Structure Templates
249
+ // ═══════════════════════════════════════════════════════════════════════════
250
+
251
+ /**
252
+ * 카테고리별 구조 템플릿 (FSD + Clean 조합)
253
+ */
254
+ const STRUCTURE_TEMPLATES: Record<FeatureCategory, (featureName: string) => DirectoryProposal[]> = {
255
+ auth: (name) => [
256
+ {
257
+ path: `server/domain/${name}`,
258
+ purpose: `${name} 도메인 로직`,
259
+ layer: "domain",
260
+ files: [
261
+ { name: `${name}.service.ts`, purpose: "핵심 비즈니스 로직", template: "service" },
262
+ { name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
263
+ ],
264
+ },
265
+ {
266
+ path: `server/application/${name}`,
267
+ purpose: `${name} 유스케이스`,
268
+ layer: "application",
269
+ files: [
270
+ { name: `login.usecase.ts`, purpose: "로그인 유스케이스", template: "usecase" },
271
+ { name: `logout.usecase.ts`, purpose: "로그아웃 유스케이스", template: "usecase" },
272
+ { name: `refresh.usecase.ts`, purpose: "토큰 갱신 유스케이스", template: "usecase" },
273
+ ],
274
+ },
275
+ {
276
+ path: `server/infra/${name}`,
277
+ purpose: `${name} 인프라 어댑터`,
278
+ layer: "infrastructure",
279
+ files: [
280
+ { name: `token.provider.ts`, purpose: "토큰 생성/검증", template: "service" },
281
+ { name: `session.repository.ts`, purpose: "세션 저장소", template: "repository" },
282
+ ],
283
+ },
284
+ {
285
+ path: `app/api/${name}`,
286
+ purpose: `${name} API 라우트`,
287
+ layer: "api",
288
+ files: [
289
+ { name: `login/route.ts`, purpose: "로그인 API", template: "route", isSlot: true },
290
+ { name: `logout/route.ts`, purpose: "로그아웃 API", template: "route", isSlot: true },
291
+ { name: `refresh/route.ts`, purpose: "토큰 갱신 API", template: "route", isSlot: true },
292
+ ],
293
+ },
294
+ ],
295
+
296
+ crud: (name) => [
297
+ {
298
+ path: `server/domain/${name}`,
299
+ purpose: `${name} 도메인`,
300
+ layer: "domain",
301
+ files: [
302
+ { name: `${name}.service.ts`, purpose: "CRUD 비즈니스 로직", template: "service" },
303
+ { name: `${name}.repository.ts`, purpose: "데이터 접근", template: "repository" },
304
+ { name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
305
+ ],
306
+ },
307
+ {
308
+ path: `app/api/${name}`,
309
+ purpose: `${name} API`,
310
+ layer: "api",
311
+ files: [
312
+ { name: `route.ts`, purpose: "목록/생성 API (GET, POST)", template: "route", isSlot: true },
313
+ { name: `[id]/route.ts`, purpose: "상세/수정/삭제 API (GET, PUT, DELETE)", template: "route", isSlot: true },
314
+ ],
315
+ },
316
+ ],
317
+
318
+ api: (name) => [
319
+ {
320
+ path: `server/domain/${name}`,
321
+ purpose: `${name} 도메인 로직`,
322
+ layer: "domain",
323
+ files: [
324
+ { name: `${name}.service.ts`, purpose: "비즈니스 로직", template: "service" },
325
+ { name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
326
+ ],
327
+ },
328
+ {
329
+ path: `app/api/${name}`,
330
+ purpose: `${name} API 엔드포인트`,
331
+ layer: "api",
332
+ files: [
333
+ { name: `route.ts`, purpose: "API 핸들러", template: "route", isSlot: true },
334
+ ],
335
+ },
336
+ ],
337
+
338
+ ui: (name) => [
339
+ {
340
+ path: `client/widgets/${name}`,
341
+ purpose: `${name} 위젯`,
342
+ layer: "widgets",
343
+ files: [
344
+ { name: `${name}.tsx`, purpose: "메인 컴포넌트", template: "component" },
345
+ { name: `${name}.styles.ts`, purpose: "스타일", template: "util" },
346
+ { name: `index.ts`, purpose: "Public API", template: "util" },
347
+ ],
348
+ },
349
+ {
350
+ path: `client/features/${name}`,
351
+ purpose: `${name} 기능 로직`,
352
+ layer: "features",
353
+ files: [
354
+ { name: `model/store.ts`, purpose: "상태 관리", template: "service" },
355
+ { name: `model/types.ts`, purpose: "타입 정의", template: "type" },
356
+ { name: `api/${name}.api.ts`, purpose: "API 호출", template: "service" },
357
+ ],
358
+ },
359
+ ],
360
+
361
+ integration: (name) => [
362
+ {
363
+ path: `server/infra/${name}`,
364
+ purpose: `${name} 외부 서비스 어댑터`,
365
+ layer: "infrastructure",
366
+ files: [
367
+ { name: `${name}.client.ts`, purpose: "외부 API 클라이언트", template: "service" },
368
+ { name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
369
+ { name: `${name}.config.ts`, purpose: "설정", template: "util" },
370
+ ],
371
+ },
372
+ {
373
+ path: `server/domain/${name}`,
374
+ purpose: `${name} 도메인 인터페이스`,
375
+ layer: "domain",
376
+ files: [
377
+ { name: `${name}.port.ts`, purpose: "포트 인터페이스", template: "type" },
378
+ ],
379
+ },
380
+ {
381
+ path: `app/api/webhooks/${name}`,
382
+ purpose: `${name} 웹훅`,
383
+ layer: "api",
384
+ files: [
385
+ { name: `route.ts`, purpose: "웹훅 핸들러", template: "route", isSlot: true },
386
+ ],
387
+ },
388
+ ],
389
+
390
+ data: (name) => [
391
+ {
392
+ path: `server/domain/${name}`,
393
+ purpose: `${name} 데이터 처리`,
394
+ layer: "domain",
395
+ files: [
396
+ { name: `${name}.processor.ts`, purpose: "데이터 처리 로직", template: "service" },
397
+ { name: `${name}.transformer.ts`, purpose: "데이터 변환", template: "util" },
398
+ { name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
399
+ ],
400
+ },
401
+ ],
402
+
403
+ util: (name) => [
404
+ {
405
+ path: `shared/utils/${name}`,
406
+ purpose: `${name} 유틸리티`,
407
+ layer: "shared",
408
+ files: [
409
+ { name: `${name}.ts`, purpose: "유틸리티 함수", template: "util" },
410
+ { name: `${name}.test.ts`, purpose: "테스트", template: "test" },
411
+ { name: `index.ts`, purpose: "Public API", template: "util" },
412
+ ],
413
+ },
414
+ ],
415
+
416
+ config: (name) => [
417
+ {
418
+ path: `shared/config`,
419
+ purpose: "설정 관리",
420
+ layer: "shared",
421
+ files: [
422
+ { name: `${name}.config.ts`, purpose: `${name} 설정`, template: "util" },
423
+ { name: `${name}.schema.ts`, purpose: "설정 스키마 (Zod)", template: "type" },
424
+ ],
425
+ },
426
+ ],
427
+
428
+ other: (name) => [
429
+ {
430
+ path: `server/domain/${name}`,
431
+ purpose: `${name} 도메인`,
432
+ layer: "domain",
433
+ files: [
434
+ { name: `${name}.service.ts`, purpose: "비즈니스 로직", template: "service" },
435
+ { name: `${name}.types.ts`, purpose: "타입 정의", template: "type" },
436
+ ],
437
+ },
438
+ ],
439
+ };
440
+
441
+ // ═══════════════════════════════════════════════════════════════════════════
442
+ // File Templates
443
+ // ═══════════════════════════════════════════════════════════════════════════
444
+
445
+ /**
446
+ * 파일 템플릿 생성
447
+ */
448
+ function generateFileContent(template: FileTemplate, name: string, purpose: string): string {
449
+ switch (template) {
450
+ case "service":
451
+ return `/**
452
+ * ${purpose}
453
+ */
454
+
455
+ export class ${toPascalCase(name)}Service {
456
+ // TODO: Implement service methods
457
+ }
458
+
459
+ export const ${toCamelCase(name)}Service = new ${toPascalCase(name)}Service();
460
+ `;
461
+
462
+ case "repository":
463
+ return `/**
464
+ * ${purpose}
465
+ */
466
+
467
+ export interface ${toPascalCase(name)}Repository {
468
+ // TODO: Define repository interface
469
+ }
470
+
471
+ export class ${toPascalCase(name)}RepositoryImpl implements ${toPascalCase(name)}Repository {
472
+ // TODO: Implement repository methods
473
+ }
474
+ `;
475
+
476
+ case "usecase":
477
+ return `/**
478
+ * ${purpose}
479
+ */
480
+
481
+ export interface ${toPascalCase(name)}Input {
482
+ // TODO: Define input
483
+ }
484
+
485
+ export interface ${toPascalCase(name)}Output {
486
+ // TODO: Define output
487
+ }
488
+
489
+ export async function ${toCamelCase(name)}(input: ${toPascalCase(name)}Input): Promise<${toPascalCase(name)}Output> {
490
+ // TODO: Implement usecase
491
+ throw new Error("Not implemented");
492
+ }
493
+ `;
494
+
495
+ case "route":
496
+ case "slot":
497
+ return `/**
498
+ * ${purpose}
499
+ *
500
+ * @slot
501
+ */
502
+
503
+ import { Mandu } from "@mandujs/core";
504
+
505
+ export default Mandu.filling()
506
+ .purpose("${purpose}")
507
+ .constraints({
508
+ maxLines: 50,
509
+ requiredPatterns: ["input-validation", "error-handling"],
510
+ })
511
+ .get(async (ctx) => {
512
+ // TODO: Implement handler
513
+ return ctx.json({ message: "Not implemented" }, 501);
514
+ });
515
+ `;
516
+
517
+ case "component":
518
+ return `/**
519
+ * ${purpose}
520
+ */
521
+
522
+ export interface ${toPascalCase(name)}Props {
523
+ // TODO: Define props
524
+ }
525
+
526
+ export function ${toPascalCase(name)}({ ...props }: ${toPascalCase(name)}Props) {
527
+ return (
528
+ <div>
529
+ {/* TODO: Implement component */}
530
+ </div>
531
+ );
532
+ }
533
+ `;
534
+
535
+ case "type":
536
+ return `/**
537
+ * ${purpose}
538
+ */
539
+
540
+ export interface ${toPascalCase(name)} {
541
+ // TODO: Define type
542
+ }
543
+
544
+ export type ${toPascalCase(name)}Id = string;
545
+ `;
546
+
547
+ case "test":
548
+ return `/**
549
+ * ${purpose}
550
+ */
551
+
552
+ import { describe, it, expect } from "bun:test";
553
+
554
+ describe("${name}", () => {
555
+ it("should work", () => {
556
+ // TODO: Add tests
557
+ expect(true).toBe(true);
558
+ });
559
+ });
560
+ `;
561
+
562
+ case "util":
563
+ default:
564
+ return `/**
565
+ * ${purpose}
566
+ */
567
+
568
+ // TODO: Implement utility functions
569
+ `;
570
+ }
571
+ }
572
+
573
+ // ═══════════════════════════════════════════════════════════════════════════
574
+ // Helper Functions
575
+ // ═══════════════════════════════════════════════════════════════════════════
576
+
577
+ function toPascalCase(str: string): string {
578
+ return str
579
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
580
+ .replace(/^(.)/, (_, c) => c.toUpperCase());
581
+ }
582
+
583
+ function toCamelCase(str: string): string {
584
+ return str
585
+ .replace(/[-_](.)/g, (_, c) => c.toUpperCase())
586
+ .replace(/^(.)/, (_, c) => c.toLowerCase());
587
+ }
588
+
589
+ function extractFeatureName(intent: string): string {
590
+ // 한글/영문에서 핵심 명사 추출 시도
591
+ const patterns = [
592
+ /(?:추가|구현|만들)(?:어|해)[줘요]?\s*[:\-]?\s*(.+)/,
593
+ /(.+?)\s*(?:기능|시스템|모듈)/,
594
+ /(?:add|implement|create)\s+(.+)/i,
595
+ ];
596
+
597
+ for (const pattern of patterns) {
598
+ const match = intent.match(pattern);
599
+ if (match) {
600
+ return match[1]
601
+ .trim()
602
+ .toLowerCase()
603
+ .replace(/\s+/g, "-")
604
+ .replace(/[^a-z0-9-]/g, "");
605
+ }
606
+ }
607
+
608
+ // 기본값: 첫 단어
609
+ return intent
610
+ .split(/\s+/)[0]
611
+ .toLowerCase()
612
+ .replace(/[^a-z0-9]/g, "") || "feature";
613
+ }
614
+
615
+ /**
616
+ * 프리셋에 따라 구조를 조정
617
+ * FSD, Clean, Hexagonal 등 프리셋별 레이어 매핑 적용
618
+ */
619
+ function adjustStructureForPreset(
620
+ structure: DirectoryProposal[],
621
+ presetDef: PresetDefinition,
622
+ preset: GuardPreset
623
+ ): DirectoryProposal[] {
624
+ // 프리셋별 경로 매핑
625
+ const pathMappings: Record<GuardPreset, Record<string, string>> = {
626
+ fsd: {
627
+ "server/domain": "src/entities",
628
+ "server/application": "src/features",
629
+ "server/infra": "src/shared/api",
630
+ "client/widgets": "src/widgets",
631
+ "client/features": "src/features",
632
+ "shared": "src/shared",
633
+ "app/api": "src/app/api",
634
+ },
635
+ clean: {
636
+ "server/domain": "src/domain",
637
+ "server/application": "src/application",
638
+ "server/infra": "src/infrastructure",
639
+ "client/widgets": "src/presentation/components",
640
+ "client/features": "src/presentation/features",
641
+ "shared": "src/shared",
642
+ "app/api": "src/interfaces/http",
643
+ },
644
+ hexagonal: {
645
+ "server/domain": "src/domain",
646
+ "server/application": "src/application",
647
+ "server/infra": "src/adapters",
648
+ "client/widgets": "src/adapters/primary/ui",
649
+ "client/features": "src/adapters/primary/ui",
650
+ "shared": "src/shared",
651
+ "app/api": "src/adapters/primary/api",
652
+ },
653
+ atomic: {
654
+ "server/domain": "src/services",
655
+ "server/application": "src/hooks",
656
+ "server/infra": "src/api",
657
+ "client/widgets": "src/components/organisms",
658
+ "client/features": "src/components/templates",
659
+ "shared": "src/utils",
660
+ "app/api": "src/api",
661
+ },
662
+ mandu: {}, // 기본값, 매핑 불필요
663
+ };
664
+
665
+ const mapping = pathMappings[preset] || {};
666
+ if (Object.keys(mapping).length === 0) {
667
+ return structure;
668
+ }
669
+
670
+ return structure.map((dir) => {
671
+ // 경로 매핑 적용
672
+ let newPath = dir.path;
673
+ for (const [from, to] of Object.entries(mapping)) {
674
+ if (dir.path.startsWith(from)) {
675
+ newPath = dir.path.replace(from, to);
676
+ break;
677
+ }
678
+ }
679
+
680
+ return {
681
+ ...dir,
682
+ path: newPath,
683
+ };
684
+ });
685
+ }
686
+
687
+ // ═══════════════════════════════════════════════════════════════════════════
688
+ // Core Functions
689
+ // ═══════════════════════════════════════════════════════════════════════════
690
+
691
+ /**
692
+ * 아키텍처 협상 수행
693
+ */
694
+ export async function negotiate(
695
+ request: NegotiationRequest,
696
+ rootDir: string
697
+ ): Promise<NegotiationResponse> {
698
+ const {
699
+ intent,
700
+ requirements = [],
701
+ constraints = [],
702
+ preset = "mandu",
703
+ context,
704
+ } = request;
705
+
706
+ // 1. 카테고리 감지
707
+ const category = request.category || detectCategory(intent);
708
+
709
+ // 2. 기능 이름 추출
710
+ const featureName = extractFeatureName(intent);
711
+
712
+ // 3. 관련 결정 검색
713
+ const categoryTags = CATEGORY_KEYWORDS[category] || [];
714
+ const searchTags = [...categoryTags.slice(0, 3), featureName];
715
+ const decisionsResult = await searchDecisions(rootDir, searchTags);
716
+
717
+ // 4. 프리셋 정의 로드 및 구조 템플릿 선택
718
+ const presetDef = getPreset(preset);
719
+ const templateFn = STRUCTURE_TEMPLATES[category] || STRUCTURE_TEMPLATES.other;
720
+ let structure = templateFn(featureName);
721
+
722
+ // 5. 프리셋에 따른 구조 조정
723
+ if (presetDef && preset !== "mandu") {
724
+ structure = adjustStructureForPreset(structure, presetDef, preset);
725
+ }
726
+
727
+ // 6. 슬롯 추출
728
+ const slots: SlotProposal[] = structure
729
+ .flatMap((dir) => dir.files)
730
+ .filter((file) => file.isSlot)
731
+ .map((file) => ({
732
+ path: file.name,
733
+ purpose: file.purpose,
734
+ constraints: file.suggestedConstraints || ["input-validation", "error-handling"],
735
+ }));
736
+
737
+ // 7. 경고 및 권장사항 생성
738
+ const warnings: string[] = [];
739
+ const recommendations: string[] = [];
740
+
741
+ // 기존 결정과 충돌 확인
742
+ for (const decision of decisionsResult.decisions) {
743
+ if (decision.status === "deprecated") {
744
+ warnings.push(`⚠️ Related decision ${decision.id} is deprecated: ${decision.title}`);
745
+ }
746
+ if (decision.status === "accepted") {
747
+ recommendations.push(`📋 Follow ${decision.id}: ${decision.decision.slice(0, 100)}...`);
748
+ }
749
+ }
750
+
751
+ // 제약 조건 기반 권장사항
752
+ if (constraints.length > 0) {
753
+ recommendations.push(`Ensure compatibility with: ${constraints.join(", ")}`);
754
+ }
755
+
756
+ // 8. 다음 단계 안내
757
+ const nextSteps = [
758
+ `1. Review the proposed structure below`,
759
+ `2. Run \`mandu_generate_scaffold\` to create files`,
760
+ `3. Implement the TODO sections in each file`,
761
+ `4. Run \`mandu_guard_heal\` to verify architecture compliance`,
762
+ ];
763
+
764
+ // 9. 파일 수 계산
765
+ const estimatedFiles = structure.reduce((sum, dir) => sum + dir.files.length, 0);
766
+
767
+ // 10. 관련 결정 포맷
768
+ const relatedDecisions: RelatedDecision[] = decisionsResult.decisions.map((d) => ({
769
+ id: d.id,
770
+ title: d.title,
771
+ summary: d.decision.slice(0, 150),
772
+ relevance: `Related to ${category} implementation`,
773
+ }));
774
+
775
+ return {
776
+ approved: true,
777
+ structure,
778
+ slots,
779
+ warnings,
780
+ recommendations,
781
+ relatedDecisions,
782
+ estimatedFiles,
783
+ preset,
784
+ nextSteps,
785
+ };
786
+ }
787
+
788
+ /**
789
+ * Scaffold 생성 (병렬 처리 최적화)
790
+ */
791
+ export async function generateScaffold(
792
+ structure: DirectoryProposal[],
793
+ rootDir: string,
794
+ options: { overwrite?: boolean; dryRun?: boolean } = {}
795
+ ): Promise<ScaffoldResult> {
796
+ const { overwrite = false, dryRun = false } = options;
797
+
798
+ const createdDirs: string[] = [];
799
+ const createdFiles: string[] = [];
800
+ const skippedFiles: string[] = [];
801
+ const errors: string[] = [];
802
+
803
+ // 1단계: 모든 디렉토리 먼저 생성 (병렬)
804
+ const dirPaths = new Set<string>();
805
+ for (const dir of structure) {
806
+ dirPaths.add(join(rootDir, dir.path));
807
+ // nested file 경로의 부모 디렉토리도 추가
808
+ for (const file of dir.files) {
809
+ dirPaths.add(dirname(join(rootDir, dir.path, file.name)));
810
+ }
811
+ }
812
+
813
+ if (!dryRun) {
814
+ const dirResults = await Promise.allSettled(
815
+ Array.from(dirPaths).map(async (dirPath) => {
816
+ await mkdir(dirPath, { recursive: true });
817
+ return dirPath;
818
+ })
819
+ );
820
+
821
+ for (const result of dirResults) {
822
+ if (result.status === "fulfilled") {
823
+ const relativePath = result.value.replace(rootDir, "").replace(/^[/\\]/, "");
824
+ if (relativePath) createdDirs.push(relativePath);
825
+ } else {
826
+ errors.push(`Failed to create directory: ${result.reason}`);
827
+ }
828
+ }
829
+ } else {
830
+ structure.forEach((dir) => createdDirs.push(dir.path));
831
+ }
832
+
833
+ // 2단계: 모든 파일 정보 수집 및 병렬 처리
834
+ interface FileTask {
835
+ filePath: string;
836
+ relativePath: string;
837
+ content: string;
838
+ }
839
+
840
+ const fileTasks: FileTask[] = [];
841
+
842
+ for (const dir of structure) {
843
+ const dirPath = join(rootDir, dir.path);
844
+
845
+ for (const file of dir.files) {
846
+ const filePath = join(dirPath, file.name);
847
+ const relativePath = join(dir.path, file.name);
848
+ const content = generateFileContent(
849
+ file.template || "util",
850
+ file.name.replace(/\.\w+$/, ""),
851
+ file.purpose
852
+ );
853
+
854
+ fileTasks.push({ filePath, relativePath, content });
855
+ }
856
+ }
857
+
858
+ // 3단계: 파일 존재 여부 확인 (병렬)
859
+ const existsResults = await Promise.allSettled(
860
+ fileTasks.map(async (task) => {
861
+ try {
862
+ await stat(task.filePath);
863
+ return { ...task, exists: true };
864
+ } catch {
865
+ return { ...task, exists: false };
866
+ }
867
+ })
868
+ );
869
+
870
+ // 4단계: 파일 쓰기 (병렬)
871
+ const writePromises: Promise<void>[] = [];
872
+
873
+ for (const result of existsResults) {
874
+ if (result.status !== "fulfilled") continue;
875
+ const { filePath, relativePath, content, exists } = result.value;
876
+
877
+ if (exists && !overwrite) {
878
+ skippedFiles.push(relativePath);
879
+ continue;
880
+ }
881
+
882
+ if (dryRun) {
883
+ createdFiles.push(relativePath);
884
+ } else {
885
+ writePromises.push(
886
+ writeFile(filePath, content, "utf-8")
887
+ .then(() => {
888
+ createdFiles.push(relativePath);
889
+ })
890
+ .catch((error) => {
891
+ errors.push(`Failed to create file ${relativePath}: ${error}`);
892
+ })
893
+ );
894
+ }
895
+ }
896
+
897
+ // 모든 쓰기 작업 완료 대기
898
+ await Promise.allSettled(writePromises);
899
+
900
+ return {
901
+ success: errors.length === 0,
902
+ createdDirs,
903
+ createdFiles,
904
+ skippedFiles,
905
+ errors,
906
+ };
907
+ }
908
+
909
+ /**
910
+ * 기존 프로젝트 구조 분석
911
+ */
912
+ export async function analyzeExistingStructure(
913
+ rootDir: string
914
+ ): Promise<{
915
+ layers: string[];
916
+ existingFeatures: string[];
917
+ recommendations: string[];
918
+ }> {
919
+ const layers: string[] = [];
920
+ const existingFeatures: string[] = [];
921
+ const recommendations: string[] = [];
922
+
923
+ // 일반적인 레이어 디렉토리 확인
924
+ const commonLayers = [
925
+ "server/domain",
926
+ "server/application",
927
+ "server/infra",
928
+ "client/features",
929
+ "client/widgets",
930
+ "client/shared",
931
+ "shared",
932
+ "app/api",
933
+ ];
934
+
935
+ for (const layer of commonLayers) {
936
+ try {
937
+ const layerPath = join(rootDir, layer);
938
+ const stats = await stat(layerPath);
939
+ if (stats.isDirectory()) {
940
+ layers.push(layer);
941
+
942
+ // 하위 디렉토리 (feature) 목록
943
+ const entries = await readdir(layerPath);
944
+ for (const entry of entries) {
945
+ const entryPath = join(layerPath, entry);
946
+ const entryStats = await stat(entryPath);
947
+ if (entryStats.isDirectory()) {
948
+ existingFeatures.push(`${layer}/${entry}`);
949
+ }
950
+ }
951
+ }
952
+ } catch {
953
+ // 레이어 없음
954
+ }
955
+ }
956
+
957
+ // 권장사항 생성
958
+ if (layers.length === 0) {
959
+ recommendations.push("No standard layers found. Consider using Mandu preset structure.");
960
+ }
961
+
962
+ if (!layers.includes("server/domain")) {
963
+ recommendations.push("Missing server/domain layer for business logic isolation.");
964
+ }
965
+
966
+ if (!layers.includes("shared")) {
967
+ recommendations.push("Consider adding shared/ for cross-cutting utilities.");
968
+ }
969
+
970
+ return {
971
+ layers,
972
+ existingFeatures,
973
+ recommendations,
974
+ };
975
+ }