@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.
- package/README.md +79 -10
- package/package.json +1 -1
- package/src/brain/doctor/config-analyzer.ts +498 -0
- package/src/brain/doctor/index.ts +10 -0
- package/src/change/snapshot.ts +46 -1
- package/src/change/types.ts +13 -0
- package/src/config/index.ts +9 -2
- package/src/config/mcp-ref.ts +348 -0
- package/src/config/mcp-status.ts +348 -0
- package/src/config/metadata.test.ts +308 -0
- package/src/config/metadata.ts +293 -0
- package/src/config/symbols.ts +144 -0
- package/src/config/validate.ts +122 -65
- package/src/config/watcher.ts +311 -0
- package/src/contract/index.ts +26 -25
- package/src/contract/protection.ts +364 -0
- package/src/error/domains.ts +265 -0
- package/src/error/index.ts +25 -13
- package/src/errors/extractor.ts +409 -0
- package/src/errors/index.ts +19 -0
- package/src/filling/context.ts +29 -1
- package/src/filling/deps.ts +238 -0
- package/src/filling/filling.ts +94 -8
- package/src/filling/index.ts +18 -0
- package/src/guard/analyzer.ts +7 -2
- package/src/guard/config-guard.ts +281 -0
- package/src/guard/decision-memory.test.ts +293 -0
- package/src/guard/decision-memory.ts +532 -0
- package/src/guard/healing.test.ts +259 -0
- package/src/guard/healing.ts +874 -0
- package/src/guard/index.ts +119 -0
- package/src/guard/negotiation.test.ts +282 -0
- package/src/guard/negotiation.ts +975 -0
- package/src/guard/semantic-slots.test.ts +379 -0
- package/src/guard/semantic-slots.ts +796 -0
- package/src/index.ts +4 -1
- package/src/lockfile/generate.ts +259 -0
- package/src/lockfile/index.ts +186 -0
- package/src/lockfile/lockfile.test.ts +410 -0
- package/src/lockfile/types.ts +184 -0
- package/src/lockfile/validate.ts +308 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/transports.ts +365 -0
- package/src/plugins/index.ts +38 -0
- package/src/plugins/registry.ts +377 -0
- package/src/plugins/types.ts +363 -0
- package/src/runtime/security.ts +155 -0
- package/src/runtime/server.ts +318 -256
- package/src/runtime/session-key.ts +328 -0
- package/src/utils/differ.test.ts +342 -0
- package/src/utils/differ.ts +482 -0
- package/src/utils/hasher.test.ts +326 -0
- package/src/utils/hasher.ts +319 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/safe-io.ts +188 -0
- 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/
|
|
76
|
-
├── guard/
|
|
77
|
-
├──
|
|
78
|
-
├──
|
|
79
|
-
├──
|
|
80
|
-
|
|
81
|
-
├──
|
|
82
|
-
├──
|
|
83
|
-
├──
|
|
84
|
-
|
|
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
|
@@ -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";
|
package/src/change/snapshot.ts
CHANGED
|
@@ -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,
|