@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.
@@ -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
  }