@mandujs/core 0.9.31 → 0.9.37

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.
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Mandu Guard Reporter
3
+ *
4
+ * 에이전트 친화적 경고 출력
5
+ */
6
+
7
+ import type {
8
+ Violation,
9
+ ViolationReport,
10
+ Severity,
11
+ LayerDefinition,
12
+ GuardPreset,
13
+ } from "./types";
14
+ import {
15
+ getDocumentationLink,
16
+ toAgentFormat,
17
+ type AgentViolationFormat,
18
+ } from "./suggestions";
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ // ANSI Colors
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+
24
+ const colors = {
25
+ reset: "\x1b[0m",
26
+ bold: "\x1b[1m",
27
+ dim: "\x1b[2m",
28
+ red: "\x1b[31m",
29
+ green: "\x1b[32m",
30
+ yellow: "\x1b[33m",
31
+ blue: "\x1b[34m",
32
+ magenta: "\x1b[35m",
33
+ cyan: "\x1b[36m",
34
+ white: "\x1b[37m",
35
+ bgRed: "\x1b[41m",
36
+ bgYellow: "\x1b[43m",
37
+ bgBlue: "\x1b[44m",
38
+ };
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+ // Formatting Helpers
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+
44
+ const SEPARATOR = "━".repeat(60);
45
+
46
+ function getSeverityIcon(severity: Severity): string {
47
+ switch (severity) {
48
+ case "error":
49
+ return "🚨";
50
+ case "warn":
51
+ return "⚠️";
52
+ case "info":
53
+ return "ℹ️";
54
+ }
55
+ }
56
+
57
+ function getSeverityColor(severity: Severity): string {
58
+ switch (severity) {
59
+ case "error":
60
+ return colors.red;
61
+ case "warn":
62
+ return colors.yellow;
63
+ case "info":
64
+ return colors.blue;
65
+ }
66
+ }
67
+
68
+ function getSeverityLabel(severity: Severity): string {
69
+ switch (severity) {
70
+ case "error":
71
+ return "ERROR";
72
+ case "warn":
73
+ return "WARNING";
74
+ case "info":
75
+ return "INFO";
76
+ }
77
+ }
78
+
79
+ // ═══════════════════════════════════════════════════════════════════════════
80
+ // Violation Formatting
81
+ // ═══════════════════════════════════════════════════════════════════════════
82
+
83
+ /**
84
+ * 단일 위반 포맷팅 (에이전트 친화적)
85
+ */
86
+ export function formatViolation(
87
+ violation: Violation,
88
+ hierarchy?: string[]
89
+ ): string {
90
+ const icon = getSeverityIcon(violation.severity);
91
+ const color = getSeverityColor(violation.severity);
92
+ const label = getSeverityLabel(violation.severity);
93
+
94
+ const lines: string[] = [
95
+ "",
96
+ `${color}${SEPARATOR}${colors.reset}`,
97
+ `${icon} ${color}${colors.bold}ARCHITECTURE VIOLATION DETECTED${colors.reset}`,
98
+ `${color}${SEPARATOR}${colors.reset}`,
99
+ "",
100
+ `${colors.dim}📁 File:${colors.reset} ${violation.filePath}`,
101
+ `${colors.dim}📍 Line:${colors.reset} ${violation.line}, ${colors.dim}Column:${colors.reset} ${violation.column}`,
102
+ `${colors.red}❌ Violation:${colors.reset} ${violation.importStatement}`,
103
+ "",
104
+ `${color}🔴 Rule:${colors.reset} ${violation.ruleName}`,
105
+ ` ${violation.ruleDescription}`,
106
+ "",
107
+ ];
108
+
109
+ // 레이어 계층 시각화
110
+ if (hierarchy && hierarchy.length > 0) {
111
+ lines.push(`${colors.cyan}📊 Layer Hierarchy:${colors.reset}`);
112
+ lines.push(formatHierarchy(hierarchy, violation.fromLayer, violation.toLayer));
113
+ lines.push("");
114
+ }
115
+
116
+ // 허용된 레이어
117
+ if (violation.allowedLayers.length > 0) {
118
+ lines.push(`${colors.green}✅ Allowed imports from "${violation.fromLayer}":${colors.reset}`);
119
+ for (const layer of violation.allowedLayers) {
120
+ lines.push(` • @/${layer}/*`);
121
+ }
122
+ lines.push("");
123
+ }
124
+
125
+ // 해결 제안
126
+ if (violation.suggestions.length > 0) {
127
+ lines.push(`${colors.magenta}💡 Suggestions:${colors.reset}`);
128
+ violation.suggestions.forEach((suggestion, i) => {
129
+ lines.push(` ${i + 1}. ${suggestion}`);
130
+ });
131
+ lines.push("");
132
+ }
133
+
134
+ lines.push(`${color}${SEPARATOR}${colors.reset}`);
135
+
136
+ return lines.join("\n");
137
+ }
138
+
139
+ /**
140
+ * 레이어 계층 시각화
141
+ */
142
+ function formatHierarchy(
143
+ hierarchy: string[],
144
+ fromLayer: string,
145
+ toLayer: string
146
+ ): string {
147
+ const fromIndex = hierarchy.indexOf(fromLayer);
148
+ const toIndex = hierarchy.indexOf(toLayer);
149
+
150
+ // 계층 화살표 생성
151
+ const layerLine = hierarchy.join(" → ");
152
+ let visualization = ` ${layerLine}`;
153
+
154
+ // 위반 방향 표시
155
+ if (fromIndex !== -1 && toIndex !== -1) {
156
+ if (fromIndex > toIndex) {
157
+ visualization += `\n ${" ".repeat(getPositionOffset(hierarchy, toLayer))}↑`;
158
+ visualization += ` ${" ".repeat(getPositionOffset(hierarchy, fromLayer) - getPositionOffset(hierarchy, toLayer) - 2)}↓`;
159
+ visualization += `\n ${colors.red}(violation: ${fromLayer} importing UP)${colors.reset}`;
160
+ } else {
161
+ visualization += `\n ${colors.red}(violation: importing restricted layer)${colors.reset}`;
162
+ }
163
+ }
164
+
165
+ return visualization;
166
+ }
167
+
168
+ /**
169
+ * 레이어 위치 오프셋 계산
170
+ */
171
+ function getPositionOffset(hierarchy: string[], layer: string): number {
172
+ let offset = 0;
173
+ for (const l of hierarchy) {
174
+ if (l === layer) break;
175
+ offset += l.length + 4; // " → " = 4 chars
176
+ }
177
+ return offset;
178
+ }
179
+
180
+ // ═══════════════════════════════════════════════════════════════════════════
181
+ // Report Formatting
182
+ // ═══════════════════════════════════════════════════════════════════════════
183
+
184
+ /**
185
+ * 전체 리포트 포맷팅
186
+ */
187
+ export function formatReport(
188
+ report: ViolationReport,
189
+ hierarchy?: string[]
190
+ ): string {
191
+ const lines: string[] = [];
192
+
193
+ // 헤더
194
+ lines.push("");
195
+ lines.push(`${colors.bold}${colors.cyan}╔══════════════════════════════════════════════════════════╗${colors.reset}`);
196
+ lines.push(`${colors.bold}${colors.cyan}║${colors.reset} 🛡️ Mandu Guard Report ${colors.bold}${colors.cyan}║${colors.reset}`);
197
+ lines.push(`${colors.bold}${colors.cyan}╚══════════════════════════════════════════════════════════╝${colors.reset}`);
198
+ lines.push("");
199
+
200
+ // 요약
201
+ lines.push(`${colors.dim}📊 Summary:${colors.reset}`);
202
+ lines.push(` Files analyzed: ${report.filesAnalyzed}`);
203
+ lines.push(` Analysis time: ${report.analysisTime}ms`);
204
+ lines.push(` Total violations: ${report.totalViolations}`);
205
+ lines.push("");
206
+
207
+ // 심각도별 카운트
208
+ if (report.totalViolations > 0) {
209
+ lines.push(`${colors.dim}📈 By Severity:${colors.reset}`);
210
+ if (report.bySeverity.error > 0) {
211
+ lines.push(` ${colors.red}🚨 Errors: ${report.bySeverity.error}${colors.reset}`);
212
+ }
213
+ if (report.bySeverity.warn > 0) {
214
+ lines.push(` ${colors.yellow}⚠️ Warnings: ${report.bySeverity.warn}${colors.reset}`);
215
+ }
216
+ if (report.bySeverity.info > 0) {
217
+ lines.push(` ${colors.blue}ℹ️ Info: ${report.bySeverity.info}${colors.reset}`);
218
+ }
219
+ lines.push("");
220
+ }
221
+
222
+ // 각 위반 출력
223
+ for (const violation of report.violations) {
224
+ lines.push(formatViolation(violation, hierarchy));
225
+ }
226
+
227
+ // 결과
228
+ if (report.totalViolations === 0) {
229
+ lines.push(`${colors.green}✅ No architecture violations found!${colors.reset}`);
230
+ } else {
231
+ lines.push(`${colors.red}❌ ${report.totalViolations} violation(s) found. Please fix them.${colors.reset}`);
232
+ }
233
+
234
+ lines.push("");
235
+
236
+ return lines.join("\n");
237
+ }
238
+
239
+ /**
240
+ * 간략한 위반 요약 (한 줄)
241
+ */
242
+ export function formatViolationSummary(violation: Violation): string {
243
+ const icon = getSeverityIcon(violation.severity);
244
+ return `${icon} ${violation.filePath}:${violation.line} - ${violation.fromLayer} → ${violation.toLayer} (${violation.ruleName})`;
245
+ }
246
+
247
+ // ═══════════════════════════════════════════════════════════════════════════
248
+ // Console Output
249
+ // ═══════════════════════════════════════════════════════════════════════════
250
+
251
+ /**
252
+ * 위반 콘솔 출력
253
+ */
254
+ export function printViolation(
255
+ violation: Violation,
256
+ hierarchy?: string[]
257
+ ): void {
258
+ console.log(formatViolation(violation, hierarchy));
259
+ }
260
+
261
+ /**
262
+ * 리포트 콘솔 출력
263
+ */
264
+ export function printReport(
265
+ report: ViolationReport,
266
+ hierarchy?: string[]
267
+ ): void {
268
+ console.log(formatReport(report, hierarchy));
269
+ }
270
+
271
+ /**
272
+ * 실시간 위반 알림 (짧은 형식)
273
+ */
274
+ export function printRealtimeViolation(violation: Violation): void {
275
+ const icon = getSeverityIcon(violation.severity);
276
+ const color = getSeverityColor(violation.severity);
277
+
278
+ console.log("");
279
+ console.log(`${color}${SEPARATOR}${colors.reset}`);
280
+ console.log(`${icon} ${color}${colors.bold}ARCHITECTURE VIOLATION${colors.reset}`);
281
+ console.log(`${colors.dim}File:${colors.reset} ${violation.filePath}:${violation.line}`);
282
+ console.log(`${colors.red}${violation.fromLayer} → ${violation.toLayer}${colors.reset} (not allowed)`);
283
+ console.log(`${colors.green}Allowed:${colors.reset} ${violation.allowedLayers.join(", ") || "none"}`);
284
+ if (violation.suggestions.length > 0) {
285
+ console.log(`${colors.magenta}Fix:${colors.reset} ${violation.suggestions[0]}`);
286
+ }
287
+ console.log(`${color}${SEPARATOR}${colors.reset}`);
288
+ }
289
+
290
+ // ═══════════════════════════════════════════════════════════════════════════
291
+ // JSON Output (CI/CD)
292
+ // ═══════════════════════════════════════════════════════════════════════════
293
+
294
+ /**
295
+ * JSON 형식 리포트
296
+ */
297
+ export function formatReportAsJSON(report: ViolationReport): string {
298
+ return JSON.stringify(report, null, 2);
299
+ }
300
+
301
+ /**
302
+ * 위반을 GitHub Actions 형식으로 출력
303
+ */
304
+ export function formatForGitHubActions(violation: Violation): string {
305
+ const level = violation.severity === "error" ? "error" : "warning";
306
+ return `::${level} file=${violation.filePath},line=${violation.line},col=${violation.column}::${violation.ruleName}: ${violation.ruleDescription}`;
307
+ }
308
+
309
+ // ═══════════════════════════════════════════════════════════════════════════
310
+ // Agent-Optimized Output
311
+ // ═══════════════════════════════════════════════════════════════════════════
312
+
313
+ /**
314
+ * 에이전트 최적화 형식으로 위반 출력
315
+ *
316
+ * AI Agent가 파싱하고 자동 수정하기 쉬운 형식
317
+ */
318
+ export function formatViolationForAgent(
319
+ violation: Violation,
320
+ preset?: GuardPreset
321
+ ): string {
322
+ const agentFormat = toAgentFormat(violation, preset);
323
+ const lines: string[] = [];
324
+
325
+ lines.push("");
326
+ lines.push(`${colors.cyan}┌─ GUARD VIOLATION ──────────────────────────────────────────┐${colors.reset}`);
327
+ lines.push(`${colors.cyan}│${colors.reset}`);
328
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.red}[${agentFormat.severity.toUpperCase()}]${colors.reset} ${agentFormat.rule.name}`);
329
+ lines.push(`${colors.cyan}│${colors.reset}`);
330
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.dim}FILE:${colors.reset} ${agentFormat.location.file}:${agentFormat.location.line}:${agentFormat.location.column}`);
331
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.dim}RULE:${colors.reset} ${agentFormat.violation.fromLayer} → ${agentFormat.violation.toLayer} (NOT ALLOWED)`);
332
+ lines.push(`${colors.cyan}│${colors.reset}`);
333
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.yellow}VIOLATION:${colors.reset}`);
334
+ lines.push(`${colors.cyan}│${colors.reset} ${agentFormat.violation.importStatement}`);
335
+ lines.push(`${colors.cyan}│${colors.reset}`);
336
+
337
+ // 수정 방법
338
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.green}FIX:${colors.reset}`);
339
+ lines.push(`${colors.cyan}│${colors.reset} ${agentFormat.fix.primary}`);
340
+
341
+ if (agentFormat.fix.codeChange) {
342
+ lines.push(`${colors.cyan}│${colors.reset}`);
343
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.magenta}CODE CHANGE:${colors.reset}`);
344
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.red}- ${agentFormat.fix.codeChange.before}${colors.reset}`);
345
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.green}+ ${agentFormat.fix.codeChange.after}${colors.reset}`);
346
+ }
347
+
348
+ // 허용된 import
349
+ if (agentFormat.allowed.length > 0) {
350
+ lines.push(`${colors.cyan}│${colors.reset}`);
351
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.blue}ALLOWED:${colors.reset} ${agentFormat.allowed.join(", ")}`);
352
+ }
353
+
354
+ // 문서 링크
355
+ lines.push(`${colors.cyan}│${colors.reset}`);
356
+ lines.push(`${colors.cyan}│${colors.reset} ${colors.dim}DOCS:${colors.reset} ${agentFormat.rule.documentation}`);
357
+ lines.push(`${colors.cyan}│${colors.reset}`);
358
+ lines.push(`${colors.cyan}└────────────────────────────────────────────────────────────┘${colors.reset}`);
359
+
360
+ return lines.join("\n");
361
+ }
362
+
363
+ /**
364
+ * 에이전트용 JSON 포맷
365
+ */
366
+ export function formatViolationAsAgentJSON(
367
+ violation: Violation,
368
+ preset?: GuardPreset
369
+ ): string {
370
+ return JSON.stringify(toAgentFormat(violation, preset), null, 2);
371
+ }
372
+
373
+ /**
374
+ * 여러 위반을 에이전트 형식으로 출력
375
+ */
376
+ export function formatReportForAgent(
377
+ report: ViolationReport,
378
+ preset?: GuardPreset
379
+ ): string {
380
+ const lines: string[] = [];
381
+
382
+ lines.push("");
383
+ lines.push(`${colors.bold}${colors.cyan}╔══════════════════════════════════════════════════════════════╗${colors.reset}`);
384
+ lines.push(`${colors.bold}${colors.cyan}║${colors.reset} 🛡️ MANDU GUARD ANALYSIS ${colors.bold}${colors.cyan}║${colors.reset}`);
385
+ lines.push(`${colors.bold}${colors.cyan}╚══════════════════════════════════════════════════════════════╝${colors.reset}`);
386
+ lines.push("");
387
+
388
+ // 요약 (에이전트가 빠르게 파악할 수 있도록)
389
+ lines.push(`${colors.dim}SUMMARY:${colors.reset}`);
390
+ lines.push(` files_analyzed: ${report.filesAnalyzed}`);
391
+ lines.push(` total_violations: ${report.totalViolations}`);
392
+ lines.push(` errors: ${report.bySeverity.error}`);
393
+ lines.push(` warnings: ${report.bySeverity.warn}`);
394
+ lines.push(` info: ${report.bySeverity.info}`);
395
+ lines.push("");
396
+
397
+ if (report.totalViolations === 0) {
398
+ lines.push(`${colors.green}✅ ALL CLEAR - No architecture violations detected${colors.reset}`);
399
+ lines.push("");
400
+ return lines.join("\n");
401
+ }
402
+
403
+ // 위반별 상세
404
+ lines.push(`${colors.yellow}VIOLATIONS:${colors.reset}`);
405
+ lines.push("");
406
+
407
+ for (const violation of report.violations) {
408
+ lines.push(formatViolationForAgent(violation, preset));
409
+ }
410
+
411
+ // 액션 요약
412
+ lines.push("");
413
+ lines.push(`${colors.bold}ACTION REQUIRED:${colors.reset}`);
414
+ if (report.bySeverity.error > 0) {
415
+ lines.push(` ${colors.red}• Fix ${report.bySeverity.error} error(s) before continuing${colors.reset}`);
416
+ }
417
+ if (report.bySeverity.warn > 0) {
418
+ lines.push(` ${colors.yellow}• Consider fixing ${report.bySeverity.warn} warning(s)${colors.reset}`);
419
+ }
420
+ lines.push("");
421
+
422
+ return lines.join("\n");
423
+ }
424
+
425
+ /**
426
+ * 전체 리포트를 에이전트 JSON으로
427
+ */
428
+ export function formatReportAsAgentJSON(
429
+ report: ViolationReport,
430
+ preset?: GuardPreset
431
+ ): string {
432
+ const agentReport = {
433
+ summary: {
434
+ filesAnalyzed: report.filesAnalyzed,
435
+ totalViolations: report.totalViolations,
436
+ analysisTime: report.analysisTime,
437
+ bySeverity: report.bySeverity,
438
+ byType: report.byType,
439
+ },
440
+ violations: report.violations.map((v) => toAgentFormat(v, preset)),
441
+ actionRequired: report.bySeverity.error > 0,
442
+ };
443
+
444
+ return JSON.stringify(agentReport, null, 2);
445
+ }