@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
package/README.md CHANGED
@@ -72,16 +72,20 @@ const handlers = Mandu.handler(userContract, {
72
72
 
73
73
  ```
74
74
  @mandujs/core
75
- ├── router/ # FS Routes - file-system based routing
76
- ├── guard/ # Mandu Guard - architecture enforcement
77
- ├── runtime/ # Server, SSR, streaming
78
- ├── filling/ # Handler chain API (Mandu.filling())
79
- ├── contract/ # Type-safe API contracts
80
- ├── bundler/ # Client bundling, HMR
81
- ├── client/ # Island hydration, client router
82
- ├── brain/ # Doctor, Watcher, Architecture analyzer
83
- ├── change/ # Transaction & history
84
- └── spec/ # Manifest schema & validation
75
+ ├── router/ # FS Routes - file-system based routing
76
+ ├── guard/ # Mandu Guard - architecture enforcement
77
+ ├── healing # Self-Healing Guard with auto-fix
78
+ ├── decision-memory # ADR storage (RFC-001)
79
+ ├── semantic-slots # Constraint validation (RFC-001)
80
+ │ └── negotiation # AI-Framework dialog (RFC-001)
81
+ ├── runtime/ # Server, SSR, streaming
82
+ ├── filling/ # Handler chain API (Mandu.filling())
83
+ ├── contract/ # Type-safe API contracts
84
+ ├── bundler/ # Client bundling, HMR
85
+ ├── client/ # Island hydration, client router
86
+ ├── brain/ # Doctor, Watcher, Architecture analyzer
87
+ ├── change/ # Transaction & history
88
+ └── spec/ # Manifest schema & validation
85
89
  ```
86
90
 
87
91
  ---
@@ -201,6 +205,71 @@ console.log(trend.trend); // "improving" | "stable" | "degrading"
201
205
  const markdown = generateGuardMarkdownReport(report, trend);
202
206
  ```
203
207
 
208
+ ### Self-Healing Guard (RFC-001) 🆕
209
+
210
+ ```typescript
211
+ import { checkWithHealing, healAll, explainRule } from "@mandujs/core/guard";
212
+
213
+ // Detect violations with fix suggestions
214
+ const result = await checkWithHealing({ preset: "mandu" }, process.cwd());
215
+
216
+ // Auto-fix all fixable violations
217
+ if (result.items.length > 0) {
218
+ const healResult = await healAll(result);
219
+ console.log(`Fixed: ${healResult.fixed}, Failed: ${healResult.failed}`);
220
+ }
221
+
222
+ // Explain any rule
223
+ const explanation = explainRule("layer-dependency");
224
+ console.log(explanation.description, explanation.examples);
225
+ ```
226
+
227
+ ### Decision Memory (RFC-001) 🆕
228
+
229
+ ```typescript
230
+ import {
231
+ searchDecisions,
232
+ saveDecision,
233
+ checkConsistency,
234
+ getCompactArchitecture
235
+ } from "@mandujs/core/guard";
236
+
237
+ // Search past decisions
238
+ const results = await searchDecisions(rootDir, ["auth", "jwt"]);
239
+
240
+ // Save new decision (ADR)
241
+ await saveDecision(rootDir, {
242
+ id: "ADR-002",
243
+ title: "Use PostgreSQL",
244
+ status: "accepted",
245
+ context: "Need relational database",
246
+ decision: "Use PostgreSQL with Drizzle ORM",
247
+ consequences: ["Need to manage migrations"],
248
+ tags: ["database", "orm"]
249
+ });
250
+
251
+ // Check implementation consistency
252
+ const consistency = await checkConsistency(rootDir);
253
+ ```
254
+
255
+ ### Architecture Negotiation (RFC-001) 🆕
256
+
257
+ ```typescript
258
+ import { negotiate, generateScaffold } from "@mandujs/core/guard";
259
+
260
+ // AI negotiates with framework before implementation
261
+ const plan = await negotiate({
262
+ intent: "Add user authentication",
263
+ requirements: ["JWT based", "Refresh tokens"],
264
+ constraints: ["Use existing User model"]
265
+ }, projectRoot);
266
+
267
+ if (plan.approved) {
268
+ // Generate scaffold files
269
+ await generateScaffold(plan.structure, projectRoot);
270
+ }
271
+ ```
272
+
204
273
  ---
205
274
 
206
275
  ## Filling API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.9.46",
3
+ "version": "0.11.0",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,498 @@
1
+ /**
2
+ * Brain Config Analyzer - Lockfile 불일치 원인 분석
3
+ *
4
+ * 설정 변경 사항을 분석하고 원인과 해결책을 제안
5
+ *
6
+ * @see docs/plans/09_lockfile_integration_plan.md
7
+ */
8
+
9
+ import type { ConfigDiff } from "../../utils/differ";
10
+ import type { LLMAdapter } from "../adapters/base";
11
+
12
+ /**
13
+ * 변경 항목 (내부용)
14
+ */
15
+ interface ChangeItem {
16
+ path: string;
17
+ value?: unknown;
18
+ oldValue?: unknown;
19
+ newValue?: unknown;
20
+ }
21
+
22
+ // ============================================
23
+ // 타입
24
+ // ============================================
25
+
26
+ export type ConfigIssueCategory =
27
+ | "security" // 민감정보 변경
28
+ | "mcp" // MCP 서버 설정 변경
29
+ | "server" // 서버 설정 변경
30
+ | "guard" // Guard 설정 변경
31
+ | "general"; // 일반 변경
32
+
33
+ export type ConfigIssueSeverity = "low" | "medium" | "high" | "critical";
34
+
35
+ export interface ConfigMismatchAnalysis {
36
+ /** 변경 카테고리 */
37
+ category: ConfigIssueCategory;
38
+ /** 심각도 */
39
+ severity: ConfigIssueSeverity;
40
+ /** 변경된 필드 경로 */
41
+ path: string;
42
+ /** 근본 원인 설명 */
43
+ rootCause: string;
44
+ /** 제안 사항 */
45
+ suggestions: string[];
46
+ /** 자동 수정 가능 여부 */
47
+ autoFixable: boolean;
48
+ }
49
+
50
+ export interface ConfigAnalysisReport {
51
+ /** 전체 분석 결과 */
52
+ analyses: ConfigMismatchAnalysis[];
53
+ /** 요약 */
54
+ summary: string;
55
+ /** 권장 조치 */
56
+ recommendedAction: "update-lockfile" | "revert-config" | "review-required";
57
+ /** 분석 시각 */
58
+ timestamp: string;
59
+ }
60
+
61
+ // ============================================
62
+ // 카테고리 판별
63
+ // ============================================
64
+
65
+ const SENSITIVE_PATTERNS = [
66
+ /apikey/i,
67
+ /secret/i,
68
+ /token/i,
69
+ /password/i,
70
+ /credential/i,
71
+ /auth/i,
72
+ /private/i,
73
+ ];
74
+
75
+ const MCP_PATTERNS = [
76
+ /^mcpServers/,
77
+ /^mcp\./,
78
+ ];
79
+
80
+ const SERVER_PATTERNS = [
81
+ /^server\./,
82
+ /^port$/,
83
+ /^host$/,
84
+ ];
85
+
86
+ const GUARD_PATTERNS = [
87
+ /^guard\./,
88
+ /^preset$/,
89
+ ];
90
+
91
+ /**
92
+ * 변경 경로에서 카테고리 판별
93
+ */
94
+ function categorizeChange(path: string): ConfigIssueCategory {
95
+ if (SENSITIVE_PATTERNS.some(p => p.test(path))) {
96
+ return "security";
97
+ }
98
+ if (MCP_PATTERNS.some(p => p.test(path))) {
99
+ return "mcp";
100
+ }
101
+ if (SERVER_PATTERNS.some(p => p.test(path))) {
102
+ return "server";
103
+ }
104
+ if (GUARD_PATTERNS.some(p => p.test(path))) {
105
+ return "guard";
106
+ }
107
+ return "general";
108
+ }
109
+
110
+ /**
111
+ * 카테고리별 기본 심각도
112
+ */
113
+ function getDefaultSeverity(category: ConfigIssueCategory): ConfigIssueSeverity {
114
+ switch (category) {
115
+ case "security":
116
+ return "critical";
117
+ case "mcp":
118
+ return "medium";
119
+ case "server":
120
+ return "medium";
121
+ case "guard":
122
+ return "low";
123
+ case "general":
124
+ return "low";
125
+ }
126
+ }
127
+
128
+ // ============================================
129
+ // 분석 함수
130
+ // ============================================
131
+
132
+ /**
133
+ * 단일 변경 분석
134
+ */
135
+ function analyzeChange(
136
+ change: ChangeItem,
137
+ changeType: "added" | "modified" | "removed"
138
+ ): ConfigMismatchAnalysis {
139
+ const category = categorizeChange(change.path);
140
+ const severity = getDefaultSeverity(category);
141
+
142
+ const suggestions = generateSuggestionsForPath(category, change.path, changeType);
143
+ const rootCause = generateRootCauseForPath(category, change.path, changeType);
144
+
145
+ return {
146
+ category,
147
+ severity,
148
+ path: change.path,
149
+ rootCause,
150
+ suggestions,
151
+ autoFixable: category === "general",
152
+ };
153
+ }
154
+
155
+ /**
156
+ * 근본 원인 생성
157
+ */
158
+ function generateRootCauseForPath(
159
+ category: ConfigIssueCategory,
160
+ path: string,
161
+ changeType: "added" | "modified" | "removed"
162
+ ): string {
163
+ const action = changeType === "added"
164
+ ? "추가되었습니다"
165
+ : changeType === "removed"
166
+ ? "삭제되었습니다"
167
+ : "변경되었습니다";
168
+
169
+ switch (category) {
170
+ case "security":
171
+ return `민감 정보 필드 '${path}'가 ${action}. 보안 검토가 필요합니다.`;
172
+
173
+ case "mcp":
174
+ return `MCP 서버 설정 '${path}'가 ${action}. AI 에이전트 통합에 영향을 줄 수 있습니다.`;
175
+
176
+ case "server":
177
+ return `서버 설정 '${path}'가 ${action}. 배포 환경에 영향을 줄 수 있습니다.`;
178
+
179
+ case "guard":
180
+ return `Guard 설정 '${path}'가 ${action}. 아키텍처 검증 규칙이 변경됩니다.`;
181
+
182
+ case "general":
183
+ default:
184
+ return `설정 '${path}'가 ${action}.`;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 제안 사항 생성
190
+ */
191
+ function generateSuggestionsForPath(
192
+ category: ConfigIssueCategory,
193
+ path: string,
194
+ changeType: "added" | "modified" | "removed"
195
+ ): string[] {
196
+ const suggestions: string[] = [];
197
+
198
+ switch (category) {
199
+ case "security":
200
+ suggestions.push("민감 정보는 환경 변수를 통해 주입하는 것을 권장합니다.");
201
+ suggestions.push(".env 파일에 보관하고 .gitignore에 추가하세요.");
202
+ suggestions.push("의도한 변경이라면 보안 검토 후 'mandu lock'을 실행하세요.");
203
+ break;
204
+
205
+ case "mcp":
206
+ suggestions.push("MCP 서버 설정 변경 시 에이전트 연결을 확인하세요.");
207
+ if (changeType === "added") {
208
+ suggestions.push("새 MCP 서버가 정상적으로 연결되는지 확인하세요.");
209
+ } else if (changeType === "removed") {
210
+ suggestions.push("삭제된 MCP 서버를 사용하는 기능이 없는지 확인하세요.");
211
+ }
212
+ suggestions.push("의도한 변경이라면 'mandu lock'을 실행하세요.");
213
+ break;
214
+
215
+ case "server":
216
+ suggestions.push("서버 설정 변경 시 배포 환경과의 호환성을 확인하세요.");
217
+ if (path.includes("port")) {
218
+ suggestions.push("포트 변경 시 방화벽 규칙도 업데이트하세요.");
219
+ }
220
+ suggestions.push("의도한 변경이라면 'mandu lock'을 실행하세요.");
221
+ break;
222
+
223
+ case "guard":
224
+ suggestions.push("Guard 설정 변경 시 기존 코드가 새 규칙을 위반하지 않는지 확인하세요.");
225
+ suggestions.push("'mandu guard'를 실행하여 위반 사항을 확인하세요.");
226
+ suggestions.push("의도한 변경이라면 'mandu lock'을 실행하세요.");
227
+ break;
228
+
229
+ case "general":
230
+ default:
231
+ suggestions.push("의도한 변경이라면 'mandu lock'을 실행하세요.");
232
+ suggestions.push("의도하지 않은 변경이라면 설정을 원복하세요.");
233
+ break;
234
+ }
235
+
236
+ return suggestions;
237
+ }
238
+
239
+ // ============================================
240
+ // 메인 분석 함수
241
+ // ============================================
242
+
243
+ /**
244
+ * 설정 불일치 분석 (템플릿 기반)
245
+ *
246
+ * @param diff 설정 차이
247
+ * @returns 분석 보고서
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * const diff = diffConfig(oldConfig, newConfig);
252
+ * const report = analyzeConfigMismatch(diff);
253
+ * console.log(report.summary);
254
+ * ```
255
+ */
256
+ export function analyzeConfigMismatch(diff: ConfigDiff): ConfigAnalysisReport {
257
+ const analyses: ConfigMismatchAnalysis[] = [];
258
+
259
+ // MCP 서버 변경 분석
260
+ for (const name of diff.mcpServers.added) {
261
+ analyses.push(analyzeChange({ path: `mcpServers.${name}` }, "added"));
262
+ }
263
+ for (const name of diff.mcpServers.removed) {
264
+ analyses.push(analyzeChange({ path: `mcpServers.${name}` }, "removed"));
265
+ }
266
+ for (const item of diff.mcpServers.modified) {
267
+ analyses.push(analyzeChange({ path: `mcpServers.${item.name}` }, "modified"));
268
+ }
269
+
270
+ // 프로젝트 설정 변경 분석
271
+ for (const path of diff.projectConfig.added) {
272
+ analyses.push(analyzeChange({ path }, "added"));
273
+ }
274
+ for (const path of diff.projectConfig.removed) {
275
+ analyses.push(analyzeChange({ path }, "removed"));
276
+ }
277
+ for (const item of diff.projectConfig.modified) {
278
+ analyses.push(analyzeChange({ path: item.key }, "modified"));
279
+ }
280
+
281
+ // 심각도별 정렬 (critical > high > medium > low)
282
+ const severityOrder: Record<ConfigIssueSeverity, number> = {
283
+ critical: 4,
284
+ high: 3,
285
+ medium: 2,
286
+ low: 1,
287
+ };
288
+ analyses.sort((a, b) => severityOrder[b.severity] - severityOrder[a.severity]);
289
+
290
+ // 요약 생성
291
+ const summary = generateSummary(analyses);
292
+
293
+ // 권장 조치 결정
294
+ const recommendedAction = determineRecommendedAction(analyses);
295
+
296
+ return {
297
+ analyses,
298
+ summary,
299
+ recommendedAction,
300
+ timestamp: new Date().toISOString(),
301
+ };
302
+ }
303
+
304
+ /**
305
+ * 요약 생성
306
+ */
307
+ function generateSummary(analyses: ConfigMismatchAnalysis[]): string {
308
+ if (analyses.length === 0) {
309
+ return "변경 사항이 없습니다.";
310
+ }
311
+
312
+ const bySeverity = {
313
+ critical: analyses.filter(a => a.severity === "critical").length,
314
+ high: analyses.filter(a => a.severity === "high").length,
315
+ medium: analyses.filter(a => a.severity === "medium").length,
316
+ low: analyses.filter(a => a.severity === "low").length,
317
+ };
318
+
319
+ const parts: string[] = [];
320
+ if (bySeverity.critical > 0) parts.push(`심각 ${bySeverity.critical}개`);
321
+ if (bySeverity.high > 0) parts.push(`높음 ${bySeverity.high}개`);
322
+ if (bySeverity.medium > 0) parts.push(`중간 ${bySeverity.medium}개`);
323
+ if (bySeverity.low > 0) parts.push(`낮음 ${bySeverity.low}개`);
324
+
325
+ return `총 ${analyses.length}개 변경 감지 (${parts.join(", ")})`;
326
+ }
327
+
328
+ /**
329
+ * 권장 조치 결정
330
+ */
331
+ function determineRecommendedAction(
332
+ analyses: ConfigMismatchAnalysis[]
333
+ ): "update-lockfile" | "revert-config" | "review-required" {
334
+ const hasCritical = analyses.some(a => a.severity === "critical");
335
+ const hasHigh = analyses.some(a => a.severity === "high");
336
+ const hasSecurity = analyses.some(a => a.category === "security");
337
+
338
+ if (hasCritical || hasSecurity) {
339
+ return "review-required";
340
+ }
341
+
342
+ if (hasHigh) {
343
+ return "review-required";
344
+ }
345
+
346
+ // 모두 자동 수정 가능하면 lockfile 업데이트
347
+ if (analyses.every(a => a.autoFixable)) {
348
+ return "update-lockfile";
349
+ }
350
+
351
+ return "review-required";
352
+ }
353
+
354
+ // ============================================
355
+ // 포맷팅
356
+ // ============================================
357
+
358
+ /**
359
+ * 분석 보고서를 콘솔 출력용 문자열로 변환
360
+ */
361
+ export function formatConfigAnalysisReport(report: ConfigAnalysisReport): string {
362
+ const lines: string[] = [];
363
+
364
+ lines.push("═══════════════════════════════════════");
365
+ lines.push("🩺 Config Mismatch Analysis");
366
+ lines.push("═══════════════════════════════════════");
367
+ lines.push("");
368
+ lines.push(`📊 ${report.summary}`);
369
+ lines.push("");
370
+
371
+ if (report.analyses.length > 0) {
372
+ lines.push("변경 사항:");
373
+ lines.push("───────────────────────────────────────");
374
+
375
+ for (const analysis of report.analyses) {
376
+ const icon = getSeverityIcon(analysis.severity);
377
+ lines.push(`${icon} [${analysis.category.toUpperCase()}] ${analysis.path}`);
378
+ lines.push(` ${analysis.rootCause}`);
379
+
380
+ if (analysis.suggestions.length > 0) {
381
+ lines.push(` 제안:`);
382
+ for (const suggestion of analysis.suggestions.slice(0, 2)) {
383
+ lines.push(` • ${suggestion}`);
384
+ }
385
+ }
386
+ lines.push("");
387
+ }
388
+ }
389
+
390
+ lines.push("───────────────────────────────────────");
391
+ lines.push(`권장 조치: ${formatRecommendedAction(report.recommendedAction)}`);
392
+
393
+ return lines.join("\n");
394
+ }
395
+
396
+ function getSeverityIcon(severity: ConfigIssueSeverity): string {
397
+ switch (severity) {
398
+ case "critical": return "🚨";
399
+ case "high": return "🔴";
400
+ case "medium": return "🟡";
401
+ case "low": return "🟢";
402
+ }
403
+ }
404
+
405
+ function formatRecommendedAction(
406
+ action: "update-lockfile" | "revert-config" | "review-required"
407
+ ): string {
408
+ switch (action) {
409
+ case "update-lockfile":
410
+ return "'mandu lock'을 실행하여 lockfile을 업데이트하세요.";
411
+ case "revert-config":
412
+ return "설정을 이전 상태로 원복하세요.";
413
+ case "review-required":
414
+ return "변경 사항을 검토한 후 적절한 조치를 취하세요.";
415
+ }
416
+ }
417
+
418
+ // ============================================
419
+ // LLM 기반 분석 (선택적)
420
+ // ============================================
421
+
422
+ /**
423
+ * LLM을 사용한 심층 분석
424
+ *
425
+ * @param diff 설정 차이
426
+ * @param adapter LLM 어댑터
427
+ * @returns 분석 보고서
428
+ */
429
+ export async function analyzeConfigMismatchWithLLM(
430
+ diff: ConfigDiff,
431
+ adapter: LLMAdapter
432
+ ): Promise<ConfigAnalysisReport> {
433
+ // 기본 템플릿 분석 수행
434
+ const baseReport = analyzeConfigMismatch(diff);
435
+
436
+ // LLM 사용 불가 시 기본 보고서 반환
437
+ const status = await adapter.checkStatus();
438
+ if (!status.available) {
439
+ return baseReport;
440
+ }
441
+
442
+ // LLM 프롬프트 생성
443
+ const prompt = buildConfigAnalysisPrompt(diff, baseReport);
444
+
445
+ try {
446
+ const messages = [{ role: "user" as const, content: prompt }];
447
+ const response = await adapter.complete(messages);
448
+
449
+ // LLM 응답 파싱 및 병합
450
+ const content = response.content ?? "";
451
+ return mergeWithLLMAnalysis(baseReport, content);
452
+ } catch {
453
+ // LLM 분석 실패 시 기본 보고서 반환
454
+ return baseReport;
455
+ }
456
+ }
457
+
458
+ function buildConfigAnalysisPrompt(diff: ConfigDiff, baseReport: ConfigAnalysisReport): string {
459
+ const mcpAdded = diff.mcpServers.added.map(n => `- mcpServers.${n}`).join("\n") || "없음";
460
+ const mcpRemoved = diff.mcpServers.removed.map(n => `- mcpServers.${n}`).join("\n") || "없음";
461
+ const mcpModified = diff.mcpServers.modified.map(m => `- mcpServers.${m.name}`).join("\n") || "없음";
462
+
463
+ const configAdded = diff.projectConfig.added.map(p => `- ${p}`).join("\n") || "없음";
464
+ const configRemoved = diff.projectConfig.removed.map(p => `- ${p}`).join("\n") || "없음";
465
+ const configModified = diff.projectConfig.modified.map(m => `- ${m.key}`).join("\n") || "없음";
466
+
467
+ return `다음 설정 변경 사항을 분석하고 추가 인사이트를 제공해주세요.
468
+
469
+ 변경 요약: ${baseReport.summary}
470
+
471
+ MCP 서버 변경:
472
+ - 추가: ${mcpAdded}
473
+ - 삭제: ${mcpRemoved}
474
+ - 수정: ${mcpModified}
475
+
476
+ 프로젝트 설정 변경:
477
+ - 추가: ${configAdded}
478
+ - 삭제: ${configRemoved}
479
+ - 수정: ${configModified}
480
+
481
+ 다음을 분석해주세요:
482
+ 1. 이 변경이 시스템에 미칠 수 있는 영향
483
+ 2. 잠재적인 문제점
484
+ 3. 추가 제안 사항
485
+
486
+ 응답은 간결하게 해주세요.`;
487
+ }
488
+
489
+ function mergeWithLLMAnalysis(
490
+ baseReport: ConfigAnalysisReport,
491
+ llmResponse: string
492
+ ): ConfigAnalysisReport {
493
+ // LLM 응답을 요약에 추가
494
+ return {
495
+ ...baseReport,
496
+ summary: `${baseReport.summary}\n\n🤖 AI 분석: ${llmResponse.slice(0, 200)}...`,
497
+ };
498
+ }
@@ -38,3 +38,13 @@ export {
38
38
  formatDoctorReport,
39
39
  type ReportFormat,
40
40
  } from "./reporter";
41
+
42
+ export {
43
+ analyzeConfigMismatch,
44
+ analyzeConfigMismatchWithLLM,
45
+ formatConfigAnalysisReport,
46
+ type ConfigMismatchAnalysis,
47
+ type ConfigAnalysisReport,
48
+ type ConfigIssueCategory,
49
+ type ConfigIssueSeverity,
50
+ } from "./config-analyzer";
@@ -1,7 +1,15 @@
1
1
  import path from "path";
2
- import type { Snapshot, RestoreResult } from "./types";
2
+ import type { Snapshot, RestoreResult, ConfigSnapshot } from "./types";
3
3
  import type { RoutesManifest } from "../spec/schema";
4
4
  import type { SpecLock } from "../spec/lock";
5
+ import {
6
+ readLockfile,
7
+ writeLockfile,
8
+ generateLockfile,
9
+ LOCKFILE_PATH,
10
+ readMcpConfig,
11
+ } from "../lockfile";
12
+ import { validateAndReport } from "../config";
5
13
 
6
14
  const SPEC_DIR = "spec";
7
15
  const MANIFEST_FILE = "routes.manifest.json";
@@ -84,6 +92,29 @@ export async function createSnapshot(rootDir: string): Promise<Snapshot> {
84
92
  // Slot 내용 수집
85
93
  const slotContents = await collectSlotContents(rootDir);
86
94
 
95
+ // Config 스냅샷 생성 (lockfile 포함)
96
+ let configSnapshot: ConfigSnapshot | undefined;
97
+ try {
98
+ const config = await validateAndReport(rootDir);
99
+ if (config) {
100
+ const existingLockfile = await readLockfile(rootDir);
101
+ let mcpConfig: Record<string, unknown> | null = null;
102
+ try {
103
+ mcpConfig = await readMcpConfig(rootDir);
104
+ } catch {
105
+ // ignore MCP config errors in snapshot
106
+ }
107
+ const lockfile =
108
+ existingLockfile ?? generateLockfile(config, { includeSnapshot: true }, mcpConfig);
109
+ configSnapshot = {
110
+ lockfile,
111
+ configHash: lockfile.configHash,
112
+ };
113
+ }
114
+ } catch {
115
+ // Config 로드 실패 시 스킵
116
+ }
117
+
87
118
  const id = generateSnapshotId();
88
119
 
89
120
  return {
@@ -92,6 +123,7 @@ export async function createSnapshot(rootDir: string): Promise<Snapshot> {
92
123
  manifest,
93
124
  lock,
94
125
  slotContents,
126
+ configSnapshot,
95
127
  };
96
128
  }
97
129
 
@@ -183,6 +215,19 @@ export async function restoreSnapshot(rootDir: string, snapshot: Snapshot): Prom
183
215
  }
184
216
  }
185
217
 
218
+ // 4. Config Lockfile 복원 (있는 경우)
219
+ if (snapshot.configSnapshot) {
220
+ try {
221
+ await writeLockfile(rootDir, snapshot.configSnapshot.lockfile);
222
+ restoredFiles.push(LOCKFILE_PATH);
223
+ } catch (error) {
224
+ failedFiles.push(LOCKFILE_PATH);
225
+ errors.push(
226
+ `Failed to restore config lockfile: ${error instanceof Error ? error.message : String(error)}`
227
+ );
228
+ }
229
+ }
230
+
186
231
  return {
187
232
  success: failedFiles.length === 0,
188
233
  restoredFiles,