@mandujs/mcp 0.9.46 → 0.12.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 +48 -1
- package/package.json +1 -1
- package/src/adapters/index.ts +20 -0
- package/src/adapters/monitor-adapter.ts +100 -0
- package/src/adapters/tool-adapter.ts +88 -0
- package/src/executor/error-handler.ts +250 -0
- package/src/executor/index.ts +22 -0
- package/src/executor/tool-executor.ts +148 -0
- package/src/hooks/config-watcher.ts +174 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/mcp-hooks.ts +227 -0
- package/src/index.ts +106 -9
- package/src/logging/index.ts +15 -0
- package/src/logging/mcp-transport.ts +134 -0
- package/src/registry/index.ts +13 -0
- package/src/registry/mcp-tool-registry.ts +298 -0
- package/src/server.ts +172 -98
- package/src/tools/guard.ts +896 -1
- package/src/tools/index.ts +133 -0
- package/src/tools/project.ts +334 -0
- package/src/utils/project.ts +32 -0
package/src/tools/guard.ts
CHANGED
|
@@ -6,8 +6,37 @@ import {
|
|
|
6
6
|
ErrorClassifier,
|
|
7
7
|
type ManduError,
|
|
8
8
|
type GeneratedMap,
|
|
9
|
+
// Self-Healing Guard imports
|
|
10
|
+
checkWithHealing,
|
|
11
|
+
applyHealing,
|
|
12
|
+
healAll,
|
|
13
|
+
explainRule,
|
|
14
|
+
type GuardConfig,
|
|
15
|
+
type ViolationType,
|
|
16
|
+
type GuardPreset,
|
|
17
|
+
// Decision Memory imports
|
|
18
|
+
searchDecisions,
|
|
19
|
+
saveDecision,
|
|
20
|
+
checkConsistency,
|
|
21
|
+
getCompactArchitecture,
|
|
22
|
+
getNextDecisionId,
|
|
23
|
+
type ArchitectureDecision,
|
|
24
|
+
type DecisionStatus,
|
|
25
|
+
// Semantic Slots imports
|
|
26
|
+
validateSlotConstraints,
|
|
27
|
+
validateSlots,
|
|
28
|
+
DEFAULT_SLOT_CONSTRAINTS,
|
|
29
|
+
API_SLOT_CONSTRAINTS,
|
|
30
|
+
READONLY_SLOT_CONSTRAINTS,
|
|
31
|
+
type SlotConstraints,
|
|
32
|
+
// Architecture Negotiation imports
|
|
33
|
+
negotiate,
|
|
34
|
+
generateScaffold,
|
|
35
|
+
analyzeExistingStructure,
|
|
36
|
+
type NegotiationRequest,
|
|
37
|
+
type FeatureCategory,
|
|
9
38
|
} from "@mandujs/core";
|
|
10
|
-
import { getProjectPaths, readJsonFile } from "../utils/project.js";
|
|
39
|
+
import { getProjectPaths, readJsonFile, readConfig } from "../utils/project.js";
|
|
11
40
|
|
|
12
41
|
export const guardToolDefinitions: Tool[] = [
|
|
13
42
|
{
|
|
@@ -40,6 +69,284 @@ export const guardToolDefinitions: Tool[] = [
|
|
|
40
69
|
required: ["errorJson"],
|
|
41
70
|
},
|
|
42
71
|
},
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// Self-Healing Guard Tools (NEW)
|
|
74
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
{
|
|
76
|
+
name: "mandu_guard_heal",
|
|
77
|
+
description:
|
|
78
|
+
"Run Self-Healing Guard: detect architecture violations and provide actionable fix suggestions with auto-fix capabilities. " +
|
|
79
|
+
"This tool not only detects violations but also explains WHY they are wrong and HOW to fix them.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
preset: {
|
|
84
|
+
type: "string",
|
|
85
|
+
enum: ["fsd", "clean", "hexagonal", "atomic", "mandu"],
|
|
86
|
+
description: "Architecture preset to use (default: from config or 'mandu')",
|
|
87
|
+
},
|
|
88
|
+
autoFix: {
|
|
89
|
+
type: "boolean",
|
|
90
|
+
description: "If true, automatically apply the primary fix for all violations",
|
|
91
|
+
},
|
|
92
|
+
file: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Specific file to check (optional, checks entire project if not specified)",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: [],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "mandu_guard_explain",
|
|
102
|
+
description:
|
|
103
|
+
"Explain a specific guard rule in detail. " +
|
|
104
|
+
"Provides WHY the rule exists, HOW to fix violations, and code examples.",
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
type: {
|
|
109
|
+
type: "string",
|
|
110
|
+
enum: ["layer-violation", "circular-dependency", "cross-slice", "deep-nesting"],
|
|
111
|
+
description: "The type of violation to explain",
|
|
112
|
+
},
|
|
113
|
+
fromLayer: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "The source layer (e.g., 'features', 'shared')",
|
|
116
|
+
},
|
|
117
|
+
toLayer: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "The target layer being imported",
|
|
120
|
+
},
|
|
121
|
+
preset: {
|
|
122
|
+
type: "string",
|
|
123
|
+
enum: ["fsd", "clean", "hexagonal", "atomic", "mandu"],
|
|
124
|
+
description: "Architecture preset for context",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
required: ["type", "fromLayer", "toLayer"],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
131
|
+
// Decision Memory Tools
|
|
132
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
+
{
|
|
134
|
+
name: "mandu_get_decisions",
|
|
135
|
+
description:
|
|
136
|
+
"Search and retrieve architecture decisions (ADRs) by tags. " +
|
|
137
|
+
"Use this before implementing features to ensure consistency with past decisions. " +
|
|
138
|
+
"Example: Before adding 'auth' feature, search for ['auth', 'security'] to find related decisions.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
tags: {
|
|
143
|
+
type: "array",
|
|
144
|
+
items: { type: "string" },
|
|
145
|
+
description: "Tags to search for (e.g., ['auth', 'cache', 'api'])",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ["tags"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "mandu_save_decision",
|
|
153
|
+
description:
|
|
154
|
+
"Save a new architecture decision record (ADR). " +
|
|
155
|
+
"Use this when making significant architectural choices that should be remembered for consistency.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {
|
|
159
|
+
title: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "Decision title (e.g., 'Use JWT for API Authentication')",
|
|
162
|
+
},
|
|
163
|
+
tags: {
|
|
164
|
+
type: "array",
|
|
165
|
+
items: { type: "string" },
|
|
166
|
+
description: "Tags for searchability (e.g., ['auth', 'api', 'security'])",
|
|
167
|
+
},
|
|
168
|
+
context: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Why this decision was needed",
|
|
171
|
+
},
|
|
172
|
+
decision: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "What was decided",
|
|
175
|
+
},
|
|
176
|
+
consequences: {
|
|
177
|
+
type: "array",
|
|
178
|
+
items: { type: "string" },
|
|
179
|
+
description: "Impact and trade-offs of this decision",
|
|
180
|
+
},
|
|
181
|
+
status: {
|
|
182
|
+
type: "string",
|
|
183
|
+
enum: ["proposed", "accepted", "deprecated", "superseded"],
|
|
184
|
+
description: "Decision status (default: proposed)",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ["title", "tags", "context", "decision", "consequences"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "mandu_check_consistency",
|
|
192
|
+
description:
|
|
193
|
+
"Check if a proposed change is consistent with existing architecture decisions. " +
|
|
194
|
+
"Use this before implementing to catch potential conflicts with past decisions.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
intent: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "What you're trying to do (e.g., 'Add Redis caching layer')",
|
|
201
|
+
},
|
|
202
|
+
tags: {
|
|
203
|
+
type: "array",
|
|
204
|
+
items: { type: "string" },
|
|
205
|
+
description: "Related tags to check against (e.g., ['cache', 'redis'])",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ["intent", "tags"],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "mandu_get_architecture",
|
|
213
|
+
description:
|
|
214
|
+
"Get a compact summary of project architecture decisions. " +
|
|
215
|
+
"Returns key decisions, tag statistics, and architecture rules for quick context.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {},
|
|
219
|
+
required: [],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
223
|
+
// Semantic Slots Tools
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
{
|
|
226
|
+
name: "mandu_validate_slot",
|
|
227
|
+
description:
|
|
228
|
+
"Validate a slot file against semantic constraints. " +
|
|
229
|
+
"Checks code lines, complexity, required/forbidden patterns, and import rules.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {
|
|
233
|
+
file: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Path to the slot file to validate",
|
|
236
|
+
},
|
|
237
|
+
preset: {
|
|
238
|
+
type: "string",
|
|
239
|
+
enum: ["default", "api", "readonly"],
|
|
240
|
+
description: "Constraint preset to use (default: 'default')",
|
|
241
|
+
},
|
|
242
|
+
constraints: {
|
|
243
|
+
type: "object",
|
|
244
|
+
description: "Custom constraints (overrides preset)",
|
|
245
|
+
properties: {
|
|
246
|
+
maxLines: { type: "number" },
|
|
247
|
+
maxCyclomaticComplexity: { type: "number" },
|
|
248
|
+
requiredPatterns: { type: "array", items: { type: "string" } },
|
|
249
|
+
forbiddenPatterns: { type: "array", items: { type: "string" } },
|
|
250
|
+
allowedImports: { type: "array", items: { type: "string" } },
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: ["file"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "mandu_get_slot_constraints",
|
|
259
|
+
description:
|
|
260
|
+
"Get recommended slot constraints for different use cases. " +
|
|
261
|
+
"Returns preset constraints that can be used with .constraints() in Filling API.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
preset: {
|
|
266
|
+
type: "string",
|
|
267
|
+
enum: ["default", "api", "readonly"],
|
|
268
|
+
description: "Constraint preset to retrieve",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
required: [],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
275
|
+
// Architecture Negotiation Tools
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
277
|
+
{
|
|
278
|
+
name: "mandu_negotiate",
|
|
279
|
+
description:
|
|
280
|
+
"Negotiate with the framework before implementing a feature. " +
|
|
281
|
+
"Describes your intent and gets back the recommended project structure, " +
|
|
282
|
+
"file templates, and related architecture decisions. " +
|
|
283
|
+
"Use this BEFORE writing code to ensure architectural consistency.",
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
intent: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "What you want to implement (e.g., '사용자 인증 기능 추가', 'Add payment integration')",
|
|
290
|
+
},
|
|
291
|
+
requirements: {
|
|
292
|
+
type: "array",
|
|
293
|
+
items: { type: "string" },
|
|
294
|
+
description: "Specific requirements (e.g., ['JWT 기반', 'OAuth 지원'])",
|
|
295
|
+
},
|
|
296
|
+
constraints: {
|
|
297
|
+
type: "array",
|
|
298
|
+
items: { type: "string" },
|
|
299
|
+
description: "Constraints to respect (e.g., ['기존 User 모델 활용', 'Redis 세션'])",
|
|
300
|
+
},
|
|
301
|
+
category: {
|
|
302
|
+
type: "string",
|
|
303
|
+
enum: ["auth", "crud", "api", "ui", "integration", "data", "util", "config", "other"],
|
|
304
|
+
description: "Feature category (auto-detected if not specified)",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
required: ["intent"],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "mandu_generate_scaffold",
|
|
312
|
+
description:
|
|
313
|
+
"Generate scaffold files from a negotiation plan. " +
|
|
314
|
+
"Creates directories and file templates based on the approved structure.",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: "object",
|
|
317
|
+
properties: {
|
|
318
|
+
intent: {
|
|
319
|
+
type: "string",
|
|
320
|
+
description: "Feature intent (used to get the structure plan)",
|
|
321
|
+
},
|
|
322
|
+
category: {
|
|
323
|
+
type: "string",
|
|
324
|
+
enum: ["auth", "crud", "api", "ui", "integration", "data", "util", "config", "other"],
|
|
325
|
+
description: "Feature category",
|
|
326
|
+
},
|
|
327
|
+
dryRun: {
|
|
328
|
+
type: "boolean",
|
|
329
|
+
description: "If true, only show what would be created without actually creating files",
|
|
330
|
+
},
|
|
331
|
+
overwrite: {
|
|
332
|
+
type: "boolean",
|
|
333
|
+
description: "If true, overwrite existing files (default: false)",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
required: ["intent"],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "mandu_analyze_structure",
|
|
341
|
+
description:
|
|
342
|
+
"Analyze the existing project structure. " +
|
|
343
|
+
"Returns detected layers, existing features, and recommendations.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: {},
|
|
347
|
+
required: [],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
43
350
|
];
|
|
44
351
|
|
|
45
352
|
export function guardTools(projectRoot: string) {
|
|
@@ -207,5 +514,593 @@ export function guardTools(projectRoot: string) {
|
|
|
207
514
|
},
|
|
208
515
|
};
|
|
209
516
|
},
|
|
517
|
+
|
|
518
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
519
|
+
// Self-Healing Guard Tools Implementation
|
|
520
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
521
|
+
|
|
522
|
+
mandu_guard_heal: async (args: Record<string, unknown>) => {
|
|
523
|
+
const {
|
|
524
|
+
preset: inputPreset,
|
|
525
|
+
autoFix = false,
|
|
526
|
+
file,
|
|
527
|
+
} = args as {
|
|
528
|
+
preset?: GuardPreset;
|
|
529
|
+
autoFix?: boolean;
|
|
530
|
+
file?: string;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// Load config to get preset
|
|
534
|
+
let config: GuardConfig = {};
|
|
535
|
+
let configLoadError: string | undefined;
|
|
536
|
+
try {
|
|
537
|
+
const projectConfig = await readConfig(projectRoot);
|
|
538
|
+
if (projectConfig?.guard) {
|
|
539
|
+
config = projectConfig.guard;
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
// 설정 로드 실패 시 경고 메시지 저장 (기본값으로 계속 진행)
|
|
543
|
+
configLoadError = `Config load warning: ${error instanceof Error ? error.message : String(error)}`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Override preset if specified
|
|
547
|
+
if (inputPreset) {
|
|
548
|
+
config.preset = inputPreset;
|
|
549
|
+
}
|
|
550
|
+
if (!config.preset) {
|
|
551
|
+
config.preset = "mandu";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Run Self-Healing check
|
|
555
|
+
const result = await checkWithHealing(config, projectRoot);
|
|
556
|
+
|
|
557
|
+
// Filter by file if specified
|
|
558
|
+
let items = result.items;
|
|
559
|
+
if (file) {
|
|
560
|
+
items = items.filter((item) =>
|
|
561
|
+
item.violation.filePath.includes(file)
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Auto-fix if requested
|
|
566
|
+
if (autoFix && items.length > 0) {
|
|
567
|
+
const healResult = await healAll({
|
|
568
|
+
...result,
|
|
569
|
+
items,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// 남은 위반 수 계산: 전체 - 성공적으로 수정된 수
|
|
573
|
+
const remaining = items.length - healResult.fixed;
|
|
574
|
+
const allFixed = remaining === 0;
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
passed: allFixed,
|
|
578
|
+
totalViolations: items.length,
|
|
579
|
+
remaining,
|
|
580
|
+
autoFix: {
|
|
581
|
+
attempted: true,
|
|
582
|
+
fixed: healResult.fixed,
|
|
583
|
+
failed: healResult.failed,
|
|
584
|
+
results: healResult.results.map((r) => ({
|
|
585
|
+
success: r.success,
|
|
586
|
+
message: r.message,
|
|
587
|
+
changedFiles: r.changedFiles,
|
|
588
|
+
})),
|
|
589
|
+
},
|
|
590
|
+
...(configLoadError && { configWarning: configLoadError }),
|
|
591
|
+
message: allFixed
|
|
592
|
+
? `✅ All ${healResult.fixed} violations fixed!`
|
|
593
|
+
: `⚠️ Fixed ${healResult.fixed}, remaining ${remaining} (failed ${healResult.failed})`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Return violations with healing suggestions
|
|
598
|
+
if (items.length === 0) {
|
|
599
|
+
return {
|
|
600
|
+
passed: true,
|
|
601
|
+
totalViolations: 0,
|
|
602
|
+
message: "✅ No architecture violations found!",
|
|
603
|
+
preset: config.preset,
|
|
604
|
+
...(configLoadError && { configWarning: configLoadError }),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
passed: false,
|
|
610
|
+
totalViolations: items.length,
|
|
611
|
+
autoFixable: items.filter((i) => i.healing.primary.autoFix).length,
|
|
612
|
+
preset: config.preset,
|
|
613
|
+
violations: items.map((item) => ({
|
|
614
|
+
// Violation info
|
|
615
|
+
type: item.violation.type,
|
|
616
|
+
file: item.violation.filePath,
|
|
617
|
+
line: item.violation.line,
|
|
618
|
+
message: item.violation.ruleDescription,
|
|
619
|
+
fromLayer: item.violation.fromLayer,
|
|
620
|
+
toLayer: item.violation.toLayer,
|
|
621
|
+
importStatement: item.violation.importStatement,
|
|
622
|
+
|
|
623
|
+
// Healing info
|
|
624
|
+
healing: {
|
|
625
|
+
primary: {
|
|
626
|
+
label: item.healing.primary.label,
|
|
627
|
+
explanation: item.healing.primary.explanation,
|
|
628
|
+
hasAutoFix: !!item.healing.primary.autoFix,
|
|
629
|
+
codeChange: item.healing.primary.before
|
|
630
|
+
? {
|
|
631
|
+
before: item.healing.primary.before,
|
|
632
|
+
after: item.healing.primary.after,
|
|
633
|
+
}
|
|
634
|
+
: undefined,
|
|
635
|
+
},
|
|
636
|
+
alternatives: item.healing.alternatives.map((alt) => ({
|
|
637
|
+
label: alt.label,
|
|
638
|
+
explanation: alt.explanation,
|
|
639
|
+
})),
|
|
640
|
+
context: {
|
|
641
|
+
layerHierarchy: item.healing.context.layerHierarchy,
|
|
642
|
+
allowedLayers: item.healing.context.allowedLayers,
|
|
643
|
+
documentation: item.healing.context.documentation,
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
})),
|
|
647
|
+
tip: "Use autoFix: true to automatically apply fixes, or review suggestions and apply manually.",
|
|
648
|
+
...(configLoadError && { configWarning: configLoadError }),
|
|
649
|
+
};
|
|
650
|
+
},
|
|
651
|
+
|
|
652
|
+
mandu_guard_explain: async (args: Record<string, unknown>) => {
|
|
653
|
+
const { type, fromLayer, toLayer, preset } = args as {
|
|
654
|
+
type: ViolationType;
|
|
655
|
+
fromLayer: string;
|
|
656
|
+
toLayer: string;
|
|
657
|
+
preset?: GuardPreset;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const explanation = explainRule(
|
|
661
|
+
type,
|
|
662
|
+
fromLayer,
|
|
663
|
+
toLayer,
|
|
664
|
+
preset ?? "mandu"
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
rule: explanation.rule,
|
|
669
|
+
explanation: {
|
|
670
|
+
why: explanation.why,
|
|
671
|
+
how: explanation.how,
|
|
672
|
+
},
|
|
673
|
+
documentation: explanation.documentation,
|
|
674
|
+
examples: {
|
|
675
|
+
bad: explanation.examples.bad,
|
|
676
|
+
good: explanation.examples.good,
|
|
677
|
+
},
|
|
678
|
+
preset: preset ?? "mandu",
|
|
679
|
+
};
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
683
|
+
// Decision Memory Tools Implementation
|
|
684
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
685
|
+
|
|
686
|
+
mandu_get_decisions: async (args: Record<string, unknown>) => {
|
|
687
|
+
const { tags } = args as { tags: string[] };
|
|
688
|
+
|
|
689
|
+
if (!tags || tags.length === 0) {
|
|
690
|
+
return {
|
|
691
|
+
error: "Tags are required",
|
|
692
|
+
tip: "Provide at least one tag to search for (e.g., ['auth', 'cache'])",
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const result = await searchDecisions(projectRoot, tags);
|
|
697
|
+
|
|
698
|
+
if (result.decisions.length === 0) {
|
|
699
|
+
return {
|
|
700
|
+
found: false,
|
|
701
|
+
message: `No decisions found for tags: ${tags.join(", ")}`,
|
|
702
|
+
searchedTags: tags,
|
|
703
|
+
tip: "Try broader tags or check spec/decisions/ directory",
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
found: true,
|
|
709
|
+
total: result.total,
|
|
710
|
+
searchedTags: tags,
|
|
711
|
+
decisions: result.decisions.map((d) => ({
|
|
712
|
+
id: d.id,
|
|
713
|
+
title: d.title,
|
|
714
|
+
status: d.status,
|
|
715
|
+
date: d.date,
|
|
716
|
+
tags: d.tags,
|
|
717
|
+
context: d.context.slice(0, 200) + (d.context.length > 200 ? "..." : ""),
|
|
718
|
+
decision: d.decision,
|
|
719
|
+
consequences: d.consequences,
|
|
720
|
+
relatedDecisions: d.relatedDecisions,
|
|
721
|
+
})),
|
|
722
|
+
tip: "Follow these decisions for consistency. Use mandu_save_decision if you make a new architectural choice.",
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
mandu_save_decision: async (args: Record<string, unknown>) => {
|
|
727
|
+
const { title, tags, context, decision, consequences, status } = args as {
|
|
728
|
+
title: string;
|
|
729
|
+
tags: string[];
|
|
730
|
+
context: string;
|
|
731
|
+
decision: string;
|
|
732
|
+
consequences: string[];
|
|
733
|
+
status?: DecisionStatus;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// Validate required fields
|
|
737
|
+
if (!title || !tags || !context || !decision || !consequences) {
|
|
738
|
+
return {
|
|
739
|
+
error: "Missing required fields",
|
|
740
|
+
required: ["title", "tags", "context", "decision", "consequences"],
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Get next ID
|
|
745
|
+
const id = await getNextDecisionId(projectRoot);
|
|
746
|
+
|
|
747
|
+
// Save decision
|
|
748
|
+
const newDecision: Omit<ArchitectureDecision, "date"> = {
|
|
749
|
+
id,
|
|
750
|
+
title,
|
|
751
|
+
status: status || "proposed",
|
|
752
|
+
tags: tags.map((t) => t.toLowerCase()),
|
|
753
|
+
context,
|
|
754
|
+
decision,
|
|
755
|
+
consequences,
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const result = await saveDecision(projectRoot, newDecision);
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
success: result.success,
|
|
762
|
+
decision: {
|
|
763
|
+
id,
|
|
764
|
+
title,
|
|
765
|
+
status: status || "proposed",
|
|
766
|
+
tags,
|
|
767
|
+
},
|
|
768
|
+
filePath: result.filePath,
|
|
769
|
+
message: result.message,
|
|
770
|
+
tip: "Decision saved. It will be found when searching for related tags.",
|
|
771
|
+
};
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
mandu_check_consistency: async (args: Record<string, unknown>) => {
|
|
775
|
+
const { intent, tags } = args as {
|
|
776
|
+
intent: string;
|
|
777
|
+
tags: string[];
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
if (!intent || !tags || tags.length === 0) {
|
|
781
|
+
return {
|
|
782
|
+
error: "Intent and tags are required",
|
|
783
|
+
tip: "Describe what you're trying to do and provide related tags",
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const result = await checkConsistency(projectRoot, intent, tags);
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
consistent: result.consistent,
|
|
791
|
+
intent,
|
|
792
|
+
checkedTags: tags,
|
|
793
|
+
relatedDecisions: result.relatedDecisions.map((d) => ({
|
|
794
|
+
id: d.id,
|
|
795
|
+
title: d.title,
|
|
796
|
+
status: d.status,
|
|
797
|
+
decision: d.decision.slice(0, 150) + "...",
|
|
798
|
+
})),
|
|
799
|
+
warnings: result.warnings,
|
|
800
|
+
suggestions: result.suggestions,
|
|
801
|
+
tip: result.consistent
|
|
802
|
+
? "No conflicts found. Proceed with implementation following the suggestions."
|
|
803
|
+
: "⚠️ Review warnings before proceeding. Some decisions may conflict.",
|
|
804
|
+
};
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
mandu_get_architecture: async () => {
|
|
808
|
+
const compact = await getCompactArchitecture(projectRoot);
|
|
809
|
+
|
|
810
|
+
if (!compact) {
|
|
811
|
+
return {
|
|
812
|
+
found: false,
|
|
813
|
+
message: "No architecture information found",
|
|
814
|
+
tip: "Save some decisions first using mandu_save_decision",
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
found: true,
|
|
820
|
+
project: compact.project,
|
|
821
|
+
lastUpdated: compact.lastUpdated,
|
|
822
|
+
summary: {
|
|
823
|
+
totalDecisions: compact.keyDecisions.length,
|
|
824
|
+
topTags: Object.entries(compact.tagCounts)
|
|
825
|
+
.sort(([, a], [, b]) => b - a)
|
|
826
|
+
.slice(0, 10)
|
|
827
|
+
.map(([tag, count]) => ({ tag, count })),
|
|
828
|
+
},
|
|
829
|
+
keyDecisions: compact.keyDecisions,
|
|
830
|
+
rules: compact.rules,
|
|
831
|
+
tip: "Use mandu_get_decisions with specific tags for detailed information.",
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
836
|
+
// Semantic Slots Tools Implementation
|
|
837
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
838
|
+
|
|
839
|
+
mandu_validate_slot: async (args: Record<string, unknown>) => {
|
|
840
|
+
const { file, preset, constraints: customConstraints } = args as {
|
|
841
|
+
file: string;
|
|
842
|
+
preset?: "default" | "api" | "readonly";
|
|
843
|
+
constraints?: SlotConstraints;
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
if (!file) {
|
|
847
|
+
return {
|
|
848
|
+
error: "File path is required",
|
|
849
|
+
tip: "Provide the path to the slot file to validate",
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// 프리셋 선택
|
|
854
|
+
let constraints: SlotConstraints;
|
|
855
|
+
if (customConstraints) {
|
|
856
|
+
constraints = customConstraints;
|
|
857
|
+
} else {
|
|
858
|
+
switch (preset) {
|
|
859
|
+
case "api":
|
|
860
|
+
constraints = API_SLOT_CONSTRAINTS;
|
|
861
|
+
break;
|
|
862
|
+
case "readonly":
|
|
863
|
+
constraints = READONLY_SLOT_CONSTRAINTS;
|
|
864
|
+
break;
|
|
865
|
+
default:
|
|
866
|
+
constraints = DEFAULT_SLOT_CONSTRAINTS;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 파일 경로 정규화 및 보안 검증 (LFI 방지)
|
|
871
|
+
const path = await import("path");
|
|
872
|
+
const rawPath = file.startsWith("/") || file.includes(":")
|
|
873
|
+
? file
|
|
874
|
+
: path.join(projectRoot, file);
|
|
875
|
+
const filePath = path.normalize(path.resolve(rawPath));
|
|
876
|
+
const normalizedRoot = path.normalize(path.resolve(projectRoot));
|
|
877
|
+
|
|
878
|
+
// 경로가 프로젝트 루트 내에 있는지 검증
|
|
879
|
+
if (!filePath.startsWith(normalizedRoot)) {
|
|
880
|
+
return {
|
|
881
|
+
error: "Access denied: File path is outside project root",
|
|
882
|
+
tip: "Only files within the project directory can be validated",
|
|
883
|
+
requestedPath: file,
|
|
884
|
+
projectRoot: projectRoot,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const result = await validateSlotConstraints(filePath, constraints);
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
valid: result.valid,
|
|
892
|
+
file: result.filePath,
|
|
893
|
+
stats: result.stats,
|
|
894
|
+
violations: result.violations.map((v) => ({
|
|
895
|
+
type: v.type,
|
|
896
|
+
severity: v.severity,
|
|
897
|
+
message: v.message,
|
|
898
|
+
suggestion: v.suggestion,
|
|
899
|
+
line: v.line,
|
|
900
|
+
})),
|
|
901
|
+
suggestions: result.suggestions,
|
|
902
|
+
constraintsUsed: constraints,
|
|
903
|
+
tip: result.valid
|
|
904
|
+
? "✅ Slot passes all constraints"
|
|
905
|
+
: "Fix violations before deployment. Use mandu_get_slot_constraints for guidance.",
|
|
906
|
+
};
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
mandu_get_slot_constraints: async (args: Record<string, unknown>) => {
|
|
910
|
+
const { preset } = args as { preset?: "default" | "api" | "readonly" };
|
|
911
|
+
|
|
912
|
+
const presets = {
|
|
913
|
+
default: {
|
|
914
|
+
name: "Default",
|
|
915
|
+
description: "Basic constraints for general slots",
|
|
916
|
+
constraints: DEFAULT_SLOT_CONSTRAINTS,
|
|
917
|
+
},
|
|
918
|
+
api: {
|
|
919
|
+
name: "API Slot",
|
|
920
|
+
description: "Constraints for API handlers with validation requirements",
|
|
921
|
+
constraints: API_SLOT_CONSTRAINTS,
|
|
922
|
+
},
|
|
923
|
+
readonly: {
|
|
924
|
+
name: "Read-only Slot",
|
|
925
|
+
description: "Strict constraints for read-only operations (no DB writes)",
|
|
926
|
+
constraints: READONLY_SLOT_CONSTRAINTS,
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
if (preset) {
|
|
931
|
+
const selected = presets[preset];
|
|
932
|
+
return {
|
|
933
|
+
preset: preset,
|
|
934
|
+
...selected,
|
|
935
|
+
usage: `
|
|
936
|
+
.constraints(${JSON.stringify(selected.constraints, null, 2)})
|
|
937
|
+
`.trim(),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
available: Object.entries(presets).map(([key, value]) => ({
|
|
943
|
+
preset: key,
|
|
944
|
+
name: value.name,
|
|
945
|
+
description: value.description,
|
|
946
|
+
constraints: value.constraints,
|
|
947
|
+
})),
|
|
948
|
+
tip: "Use these constraints with Mandu.filling().constraints({...}) to enforce slot rules.",
|
|
949
|
+
example: `
|
|
950
|
+
Mandu.filling()
|
|
951
|
+
.purpose("사용자 목록 조회 API")
|
|
952
|
+
.constraints({
|
|
953
|
+
maxLines: 50,
|
|
954
|
+
maxCyclomaticComplexity: 10,
|
|
955
|
+
requiredPatterns: ["input-validation", "error-handling"],
|
|
956
|
+
forbiddenPatterns: ["direct-db-write"],
|
|
957
|
+
allowedImports: ["server/domain/*", "shared/utils/*"],
|
|
958
|
+
})
|
|
959
|
+
.get(async (ctx) => { ... });
|
|
960
|
+
`.trim(),
|
|
961
|
+
};
|
|
962
|
+
},
|
|
963
|
+
|
|
964
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
965
|
+
// Architecture Negotiation Tools Implementation
|
|
966
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
967
|
+
|
|
968
|
+
mandu_negotiate: async (args: Record<string, unknown>) => {
|
|
969
|
+
const { intent, requirements, constraints, category } = args as {
|
|
970
|
+
intent: string;
|
|
971
|
+
requirements?: string[];
|
|
972
|
+
constraints?: string[];
|
|
973
|
+
category?: FeatureCategory;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
if (!intent) {
|
|
977
|
+
return {
|
|
978
|
+
error: "Intent is required",
|
|
979
|
+
tip: "Describe what you want to implement (e.g., '사용자 인증 기능 추가')",
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const request: NegotiationRequest = {
|
|
984
|
+
intent,
|
|
985
|
+
requirements,
|
|
986
|
+
constraints,
|
|
987
|
+
category,
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
const result = await negotiate(request, projectRoot);
|
|
991
|
+
|
|
992
|
+
return {
|
|
993
|
+
approved: result.approved,
|
|
994
|
+
intent,
|
|
995
|
+
detectedCategory: category || "auto",
|
|
996
|
+
preset: result.preset,
|
|
997
|
+
|
|
998
|
+
// Structure summary
|
|
999
|
+
structure: result.structure.map((dir) => ({
|
|
1000
|
+
path: dir.path,
|
|
1001
|
+
purpose: dir.purpose,
|
|
1002
|
+
layer: dir.layer,
|
|
1003
|
+
files: dir.files.map((f) => ({
|
|
1004
|
+
name: f.name,
|
|
1005
|
+
purpose: f.purpose,
|
|
1006
|
+
isSlot: f.isSlot || false,
|
|
1007
|
+
})),
|
|
1008
|
+
})),
|
|
1009
|
+
|
|
1010
|
+
// Slots to implement
|
|
1011
|
+
slots: result.slots,
|
|
1012
|
+
|
|
1013
|
+
// Context
|
|
1014
|
+
relatedDecisions: result.relatedDecisions,
|
|
1015
|
+
warnings: result.warnings,
|
|
1016
|
+
recommendations: result.recommendations,
|
|
1017
|
+
|
|
1018
|
+
// Summary
|
|
1019
|
+
summary: {
|
|
1020
|
+
estimatedFiles: result.estimatedFiles,
|
|
1021
|
+
slotsToImplement: result.slots.length,
|
|
1022
|
+
relatedDecisionsCount: result.relatedDecisions.length,
|
|
1023
|
+
},
|
|
1024
|
+
|
|
1025
|
+
// Next steps
|
|
1026
|
+
nextSteps: result.nextSteps,
|
|
1027
|
+
tip: "Use mandu_generate_scaffold to create the file structure, then implement the TODO sections.",
|
|
1028
|
+
};
|
|
1029
|
+
},
|
|
1030
|
+
|
|
1031
|
+
mandu_generate_scaffold: async (args: Record<string, unknown>) => {
|
|
1032
|
+
const { intent, category, dryRun = false, overwrite = false } = args as {
|
|
1033
|
+
intent: string;
|
|
1034
|
+
category?: FeatureCategory;
|
|
1035
|
+
dryRun?: boolean;
|
|
1036
|
+
overwrite?: boolean;
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
if (!intent) {
|
|
1040
|
+
return {
|
|
1041
|
+
error: "Intent is required",
|
|
1042
|
+
tip: "Provide the same intent you used with mandu_negotiate",
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// 먼저 협상하여 구조 계획 얻기
|
|
1047
|
+
const plan = await negotiate({ intent, category }, projectRoot);
|
|
1048
|
+
|
|
1049
|
+
if (!plan.approved) {
|
|
1050
|
+
return {
|
|
1051
|
+
error: "Negotiation not approved",
|
|
1052
|
+
reason: plan.rejectionReason,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Scaffold 생성
|
|
1057
|
+
const result = await generateScaffold(plan.structure, projectRoot, {
|
|
1058
|
+
dryRun,
|
|
1059
|
+
overwrite,
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
success: result.success,
|
|
1064
|
+
dryRun,
|
|
1065
|
+
created: {
|
|
1066
|
+
directories: result.createdDirs,
|
|
1067
|
+
files: result.createdFiles,
|
|
1068
|
+
},
|
|
1069
|
+
skipped: result.skippedFiles,
|
|
1070
|
+
errors: result.errors,
|
|
1071
|
+
summary: {
|
|
1072
|
+
dirsCreated: result.createdDirs.length,
|
|
1073
|
+
filesCreated: result.createdFiles.length,
|
|
1074
|
+
filesSkipped: result.skippedFiles.length,
|
|
1075
|
+
},
|
|
1076
|
+
nextSteps: [
|
|
1077
|
+
"1. Review the generated files",
|
|
1078
|
+
"2. Implement the TODO sections in each file",
|
|
1079
|
+
"3. Run mandu_guard_heal to verify architecture compliance",
|
|
1080
|
+
"4. Add tests for your implementation",
|
|
1081
|
+
],
|
|
1082
|
+
tip: dryRun
|
|
1083
|
+
? "This was a dry run. Remove dryRun: true to actually create files."
|
|
1084
|
+
: "Files created! Start implementing the TODO sections.",
|
|
1085
|
+
};
|
|
1086
|
+
},
|
|
1087
|
+
|
|
1088
|
+
mandu_analyze_structure: async () => {
|
|
1089
|
+
const result = await analyzeExistingStructure(projectRoot);
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
projectRoot,
|
|
1093
|
+
detected: {
|
|
1094
|
+
layers: result.layers,
|
|
1095
|
+
layerCount: result.layers.length,
|
|
1096
|
+
existingFeatures: result.existingFeatures,
|
|
1097
|
+
featureCount: result.existingFeatures.length,
|
|
1098
|
+
},
|
|
1099
|
+
recommendations: result.recommendations,
|
|
1100
|
+
tip: result.layers.length > 0
|
|
1101
|
+
? "Use mandu_negotiate to add new features following the existing structure."
|
|
1102
|
+
: "Use mandu_negotiate to establish your project structure.",
|
|
1103
|
+
};
|
|
1104
|
+
},
|
|
210
1105
|
};
|
|
211
1106
|
}
|