@mandujs/core 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.ko.md +4 -4
  2. package/README.md +653 -653
  3. package/package.json +1 -1
  4. package/src/bundler/build.ts +91 -91
  5. package/src/bundler/css.ts +302 -302
  6. package/src/client/Link.tsx +227 -227
  7. package/src/client/globals.ts +44 -44
  8. package/src/client/hooks.ts +267 -267
  9. package/src/client/index.ts +5 -5
  10. package/src/client/island.ts +8 -8
  11. package/src/client/router.ts +435 -435
  12. package/src/client/runtime.ts +23 -23
  13. package/src/client/serialize.ts +404 -404
  14. package/src/client/window-state.ts +101 -101
  15. package/src/config/mandu.ts +9 -0
  16. package/src/config/validate.ts +12 -0
  17. package/src/config/watcher.ts +311 -311
  18. package/src/constants.ts +40 -40
  19. package/src/content/content-layer.ts +314 -314
  20. package/src/content/content.test.ts +433 -433
  21. package/src/content/data-store.ts +245 -245
  22. package/src/content/digest.ts +133 -133
  23. package/src/content/index.ts +164 -164
  24. package/src/content/loader-context.ts +172 -172
  25. package/src/content/loaders/api.ts +216 -216
  26. package/src/content/loaders/file.ts +169 -169
  27. package/src/content/loaders/glob.ts +252 -252
  28. package/src/content/loaders/index.ts +34 -34
  29. package/src/content/loaders/types.ts +137 -137
  30. package/src/content/meta-store.ts +209 -209
  31. package/src/content/types.ts +282 -282
  32. package/src/content/watcher.ts +135 -135
  33. package/src/contract/client-safe.test.ts +42 -42
  34. package/src/contract/client-safe.ts +114 -114
  35. package/src/contract/client.ts +16 -16
  36. package/src/contract/define.ts +459 -459
  37. package/src/contract/handler.ts +10 -10
  38. package/src/contract/normalize.test.ts +276 -276
  39. package/src/contract/normalize.ts +404 -404
  40. package/src/contract/registry.test.ts +206 -206
  41. package/src/contract/registry.ts +568 -568
  42. package/src/contract/schema.ts +48 -48
  43. package/src/contract/types.ts +58 -58
  44. package/src/contract/validator.ts +32 -32
  45. package/src/devtools/ai/context-builder.ts +375 -375
  46. package/src/devtools/ai/index.ts +25 -25
  47. package/src/devtools/ai/mcp-connector.ts +465 -465
  48. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  49. package/src/devtools/client/catchers/index.ts +18 -18
  50. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  51. package/src/devtools/client/components/index.ts +39 -39
  52. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  53. package/src/devtools/client/components/mandu-character.tsx +241 -241
  54. package/src/devtools/client/components/overlay.tsx +368 -368
  55. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  56. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  57. package/src/devtools/client/components/panel/index.ts +32 -32
  58. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  59. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  60. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  61. package/src/devtools/client/filters/context-filters.ts +282 -282
  62. package/src/devtools/client/filters/index.ts +16 -16
  63. package/src/devtools/client/index.ts +63 -63
  64. package/src/devtools/client/persistence.ts +335 -335
  65. package/src/devtools/client/state-manager.ts +478 -478
  66. package/src/devtools/design-tokens.ts +263 -263
  67. package/src/devtools/hook/create-hook.ts +207 -207
  68. package/src/devtools/hook/index.ts +13 -13
  69. package/src/devtools/index.ts +439 -439
  70. package/src/devtools/init.ts +266 -266
  71. package/src/devtools/protocol.ts +237 -237
  72. package/src/devtools/server/index.ts +17 -17
  73. package/src/devtools/server/source-context.ts +444 -444
  74. package/src/devtools/types.ts +319 -319
  75. package/src/devtools/worker/index.ts +25 -25
  76. package/src/devtools/worker/redaction-worker.ts +222 -222
  77. package/src/devtools/worker/worker-manager.ts +409 -409
  78. package/src/error/domains.ts +265 -265
  79. package/src/error/result.ts +46 -46
  80. package/src/error/types.ts +6 -6
  81. package/src/errors/extractor.ts +409 -409
  82. package/src/errors/index.ts +19 -19
  83. package/src/filling/auth.ts +308 -308
  84. package/src/filling/context.ts +24 -1
  85. package/src/filling/deps.ts +238 -238
  86. package/src/filling/index.ts +2 -0
  87. package/src/filling/sse.test.ts +168 -0
  88. package/src/filling/sse.ts +162 -0
  89. package/src/generator/index.ts +3 -3
  90. package/src/guard/analyzer.ts +360 -360
  91. package/src/guard/ast-analyzer.ts +806 -806
  92. package/src/guard/contract-guard.ts +9 -9
  93. package/src/guard/file-type.test.ts +24 -24
  94. package/src/guard/presets/atomic.ts +70 -70
  95. package/src/guard/presets/clean.ts +77 -77
  96. package/src/guard/presets/fsd.ts +79 -79
  97. package/src/guard/presets/hexagonal.ts +68 -68
  98. package/src/guard/presets/index.ts +291 -291
  99. package/src/guard/reporter.ts +445 -445
  100. package/src/guard/rules.ts +12 -12
  101. package/src/guard/statistics.ts +578 -578
  102. package/src/guard/suggestions.ts +358 -358
  103. package/src/guard/types.ts +348 -348
  104. package/src/guard/validator.ts +834 -834
  105. package/src/guard/watcher.ts +404 -404
  106. package/src/index.ts +6 -1
  107. package/src/intent/index.ts +310 -310
  108. package/src/island/index.ts +304 -304
  109. package/src/logging/index.ts +22 -22
  110. package/src/logging/transports.ts +365 -365
  111. package/src/plugins/index.ts +38 -38
  112. package/src/plugins/registry.ts +377 -377
  113. package/src/plugins/types.ts +363 -363
  114. package/src/report/index.ts +1 -1
  115. package/src/router/fs-patterns.ts +387 -387
  116. package/src/router/fs-scanner.ts +497 -497
  117. package/src/runtime/boundary.tsx +232 -232
  118. package/src/runtime/compose.ts +222 -222
  119. package/src/runtime/escape.ts +44 -0
  120. package/src/runtime/lifecycle.ts +381 -381
  121. package/src/runtime/logger.test.ts +345 -345
  122. package/src/runtime/logger.ts +677 -677
  123. package/src/runtime/router.test.ts +476 -476
  124. package/src/runtime/router.ts +105 -105
  125. package/src/runtime/security.ts +155 -155
  126. package/src/runtime/server.ts +257 -0
  127. package/src/runtime/session-key.ts +328 -328
  128. package/src/runtime/ssr.ts +16 -21
  129. package/src/runtime/streaming-ssr.ts +24 -33
  130. package/src/runtime/trace.ts +144 -144
  131. package/src/seo/index.ts +214 -214
  132. package/src/seo/integration/ssr.ts +307 -307
  133. package/src/seo/render/basic.ts +427 -427
  134. package/src/seo/render/index.ts +143 -143
  135. package/src/seo/render/jsonld.ts +539 -539
  136. package/src/seo/render/opengraph.ts +191 -191
  137. package/src/seo/render/robots.ts +116 -116
  138. package/src/seo/render/sitemap.ts +137 -137
  139. package/src/seo/render/twitter.ts +126 -126
  140. package/src/seo/resolve/index.ts +353 -353
  141. package/src/seo/resolve/opengraph.ts +143 -143
  142. package/src/seo/resolve/robots.ts +73 -73
  143. package/src/seo/resolve/title.ts +94 -94
  144. package/src/seo/resolve/twitter.ts +73 -73
  145. package/src/seo/resolve/url.ts +97 -97
  146. package/src/seo/routes/index.ts +290 -290
  147. package/src/seo/types.ts +575 -575
  148. package/src/slot/validator.ts +39 -39
  149. package/src/spec/index.ts +3 -3
  150. package/src/spec/load.ts +76 -76
  151. package/src/spec/lock.ts +56 -56
  152. package/src/utils/bun.ts +8 -8
  153. package/src/utils/lru-cache.ts +75 -75
  154. package/src/utils/safe-io.ts +188 -188
  155. package/src/utils/string-safe.ts +298 -298
@@ -1,578 +1,578 @@
1
- /**
2
- * Mandu Guard Statistics
3
- *
4
- * 위반 통계 및 트렌드 분석
5
- */
6
-
7
- import { writeFile, readFile, mkdir } from "fs/promises";
8
- import { dirname, join } from "path";
9
- import type {
10
- Violation,
11
- ViolationReport,
12
- ViolationType,
13
- Severity,
14
- GuardPreset,
15
- } from "./types";
16
-
17
- // ═══════════════════════════════════════════════════════════════════════════
18
- // Types
19
- // ═══════════════════════════════════════════════════════════════════════════
20
-
21
- /**
22
- * 단일 스캔 기록
23
- */
24
- export interface ScanRecord {
25
- /** 스캔 ID */
26
- id: string;
27
- /** 스캔 시간 */
28
- timestamp: number;
29
- /** 프리셋 */
30
- preset?: GuardPreset;
31
- /** 분석된 파일 수 */
32
- filesAnalyzed: number;
33
- /** 총 위반 수 */
34
- totalViolations: number;
35
- /** 심각도별 카운트 */
36
- bySeverity: Record<Severity, number>;
37
- /** 타입별 카운트 */
38
- byType: Record<ViolationType, number>;
39
- /** 레이어별 카운트 */
40
- byLayer: Record<string, number>;
41
- /** 가장 많은 위반 파일 */
42
- hotspots: Array<{ file: string; count: number }>;
43
- }
44
-
45
- /**
46
- * 통계 저장소
47
- */
48
- export interface StatisticsStore {
49
- /** 버전 */
50
- version: number;
51
- /** 프로젝트 이름 */
52
- projectName?: string;
53
- /** 스캔 기록 */
54
- records: ScanRecord[];
55
- /** 마지막 업데이트 */
56
- lastUpdated: number;
57
- }
58
-
59
- /**
60
- * 트렌드 분석
61
- */
62
- export interface TrendAnalysis {
63
- /** 분석 기간 */
64
- period: {
65
- start: number;
66
- end: number;
67
- days: number;
68
- };
69
- /** 위반 변화량 */
70
- violationDelta: number;
71
- /** 위반 변화율 (%) */
72
- violationChangePercent: number;
73
- /** 개선/악화 */
74
- trend: "improving" | "stable" | "degrading";
75
- /** 레이어별 트렌드 */
76
- byLayer: Record<string, { delta: number; trend: "improving" | "stable" | "degrading" }>;
77
- /** 권장 사항 */
78
- recommendations: string[];
79
- }
80
-
81
- /**
82
- * 레이어별 통계
83
- */
84
- export interface LayerStatistics {
85
- /** 레이어 이름 */
86
- name: string;
87
- /** 총 위반 수 */
88
- totalViolations: number;
89
- /** 위반 원인 레이어 수 */
90
- asSource: number;
91
- /** 위반 대상 레이어 수 */
92
- asTarget: number;
93
- /** 가장 많이 위반한 타겟 */
94
- topTargets: Array<{ layer: string; count: number }>;
95
- /** 건강도 점수 (0-100) */
96
- healthScore: number;
97
- }
98
-
99
- // ═══════════════════════════════════════════════════════════════════════════
100
- // Statistics Generation
101
- // ═══════════════════════════════════════════════════════════════════════════
102
-
103
- /**
104
- * 리포트에서 스캔 기록 생성
105
- */
106
- export function createScanRecord(
107
- report: ViolationReport,
108
- preset?: GuardPreset
109
- ): ScanRecord {
110
- // 레이어별 카운트
111
- const byLayer: Record<string, number> = {};
112
- for (const v of report.violations) {
113
- byLayer[v.fromLayer] = (byLayer[v.fromLayer] || 0) + 1;
114
- }
115
-
116
- // 핫스팟 (가장 많은 위반 파일)
117
- const fileCounts: Record<string, number> = {};
118
- for (const v of report.violations) {
119
- fileCounts[v.filePath] = (fileCounts[v.filePath] || 0) + 1;
120
- }
121
-
122
- const hotspots = Object.entries(fileCounts)
123
- .sort((a, b) => b[1] - a[1])
124
- .slice(0, 5)
125
- .map(([file, count]) => ({ file, count }));
126
-
127
- return {
128
- id: generateId(),
129
- timestamp: Date.now(),
130
- preset,
131
- filesAnalyzed: report.filesAnalyzed,
132
- totalViolations: report.totalViolations,
133
- bySeverity: report.bySeverity,
134
- byType: report.byType,
135
- byLayer,
136
- hotspots,
137
- };
138
- }
139
-
140
- /**
141
- * 레이어별 통계 계산
142
- */
143
- export function calculateLayerStatistics(
144
- violations: Violation[],
145
- layers: string[]
146
- ): LayerStatistics[] {
147
- const stats: Map<string, LayerStatistics> = new Map();
148
-
149
- // 초기화
150
- for (const layer of layers) {
151
- stats.set(layer, {
152
- name: layer,
153
- totalViolations: 0,
154
- asSource: 0,
155
- asTarget: 0,
156
- topTargets: [],
157
- healthScore: 100,
158
- });
159
- }
160
-
161
- // 위반 집계
162
- const targetCounts: Map<string, Map<string, number>> = new Map();
163
-
164
- for (const v of violations) {
165
- // Source 카운트
166
- const sourceStat = stats.get(v.fromLayer);
167
- if (sourceStat) {
168
- sourceStat.asSource++;
169
- sourceStat.totalViolations++;
170
-
171
- // 타겟 카운트
172
- if (!targetCounts.has(v.fromLayer)) {
173
- targetCounts.set(v.fromLayer, new Map());
174
- }
175
- const targets = targetCounts.get(v.fromLayer)!;
176
- targets.set(v.toLayer, (targets.get(v.toLayer) || 0) + 1);
177
- }
178
-
179
- // Target 카운트
180
- const targetStat = stats.get(v.toLayer);
181
- if (targetStat) {
182
- targetStat.asTarget++;
183
- }
184
- }
185
-
186
- // 건강도 점수 및 Top targets 계산
187
- for (const [layer, stat] of stats) {
188
- // 건강도: 위반이 많을수록 낮음
189
- const maxViolations = 20; // 20개 이상이면 0점
190
- stat.healthScore = Math.max(0, Math.round(100 - (stat.asSource / maxViolations) * 100));
191
-
192
- // Top targets
193
- const targets = targetCounts.get(layer);
194
- if (targets) {
195
- stat.topTargets = Array.from(targets.entries())
196
- .sort((a, b) => b[1] - a[1])
197
- .slice(0, 3)
198
- .map(([layer, count]) => ({ layer, count }));
199
- }
200
- }
201
-
202
- return Array.from(stats.values());
203
- }
204
-
205
- /**
206
- * 트렌드 분석
207
- */
208
- export function analyzeTrend(
209
- records: ScanRecord[],
210
- days: number = 7
211
- ): TrendAnalysis | null {
212
- if (records.length < 2) {
213
- return null;
214
- }
215
-
216
- const now = Date.now();
217
- const periodStart = now - days * 24 * 60 * 60 * 1000;
218
-
219
- // 기간 내 기록 필터링
220
- const periodRecords = records.filter((r) => r.timestamp >= periodStart);
221
-
222
- if (periodRecords.length < 2) {
223
- return null;
224
- }
225
-
226
- // 가장 오래된 것과 가장 최근 것 비교
227
- const oldest = periodRecords[0];
228
- const newest = periodRecords[periodRecords.length - 1];
229
-
230
- const violationDelta = newest.totalViolations - oldest.totalViolations;
231
- const violationChangePercent =
232
- oldest.totalViolations === 0
233
- ? 0
234
- : Math.round((violationDelta / oldest.totalViolations) * 100);
235
-
236
- const trend: TrendAnalysis["trend"] =
237
- violationDelta < -2
238
- ? "improving"
239
- : violationDelta > 2
240
- ? "degrading"
241
- : "stable";
242
-
243
- // 레이어별 트렌드
244
- const allLayers = new Set([
245
- ...Object.keys(oldest.byLayer || {}),
246
- ...Object.keys(newest.byLayer || {}),
247
- ]);
248
-
249
- const byLayer: TrendAnalysis["byLayer"] = {};
250
- for (const layer of allLayers) {
251
- const oldCount = oldest.byLayer?.[layer] || 0;
252
- const newCount = newest.byLayer?.[layer] || 0;
253
- const delta = newCount - oldCount;
254
-
255
- byLayer[layer] = {
256
- delta,
257
- trend: delta < -1 ? "improving" : delta > 1 ? "degrading" : "stable",
258
- };
259
- }
260
-
261
- // 권장 사항 생성
262
- const recommendations: string[] = [];
263
-
264
- if (trend === "degrading") {
265
- recommendations.push("위반이 증가하고 있습니다. 새 코드 리뷰를 강화하세요.");
266
- }
267
-
268
- const degradingLayers = Object.entries(byLayer)
269
- .filter(([_, v]) => v.trend === "degrading")
270
- .map(([k, _]) => k);
271
-
272
- if (degradingLayers.length > 0) {
273
- recommendations.push(`주의 필요 레이어: ${degradingLayers.join(", ")}`);
274
- }
275
-
276
- const hotspot = newest.hotspots?.[0];
277
- if (hotspot && hotspot.count > 5) {
278
- recommendations.push(`핫스팟: ${hotspot.file} (${hotspot.count}개 위반) - 리팩토링 고려`);
279
- }
280
-
281
- if (recommendations.length === 0 && trend === "improving") {
282
- recommendations.push("잘하고 있습니다! 아키텍처 품질이 개선되고 있습니다.");
283
- }
284
-
285
- return {
286
- period: {
287
- start: oldest.timestamp,
288
- end: newest.timestamp,
289
- days,
290
- },
291
- violationDelta,
292
- violationChangePercent,
293
- trend,
294
- byLayer,
295
- recommendations,
296
- };
297
- }
298
-
299
- // ═══════════════════════════════════════════════════════════════════════════
300
- // Statistics Storage
301
- // ═══════════════════════════════════════════════════════════════════════════
302
-
303
- const STATS_FILE = ".mandu/guard-stats.json";
304
- const MAX_RECORDS = 100;
305
-
306
- /**
307
- * 통계 저장소 로드
308
- */
309
- export async function loadStatistics(rootDir: string): Promise<StatisticsStore> {
310
- const filePath = join(rootDir, STATS_FILE);
311
-
312
- try {
313
- const content = await readFile(filePath, "utf-8");
314
- return JSON.parse(content);
315
- } catch {
316
- return {
317
- version: 1,
318
- records: [],
319
- lastUpdated: Date.now(),
320
- };
321
- }
322
- }
323
-
324
- /**
325
- * 통계 저장소 저장
326
- */
327
- export async function saveStatistics(
328
- rootDir: string,
329
- store: StatisticsStore
330
- ): Promise<void> {
331
- const filePath = join(rootDir, STATS_FILE);
332
-
333
- // 디렉토리 생성
334
- await mkdir(dirname(filePath), { recursive: true });
335
-
336
- // 레코드 수 제한
337
- if (store.records.length > MAX_RECORDS) {
338
- store.records = store.records.slice(-MAX_RECORDS);
339
- }
340
-
341
- store.lastUpdated = Date.now();
342
-
343
- await writeFile(filePath, JSON.stringify(store, null, 2));
344
- }
345
-
346
- /**
347
- * 스캔 기록 추가
348
- */
349
- export async function addScanRecord(
350
- rootDir: string,
351
- record: ScanRecord
352
- ): Promise<void> {
353
- const store = await loadStatistics(rootDir);
354
- store.records.push(record);
355
- await saveStatistics(rootDir, store);
356
- }
357
-
358
- // ═══════════════════════════════════════════════════════════════════════════
359
- // Report Generation
360
- // ═══════════════════════════════════════════════════════════════════════════
361
-
362
- /**
363
- * 마크다운 리포트 생성 (Guard Report)
364
- */
365
- export function generateGuardMarkdownReport(
366
- report: ViolationReport,
367
- trend?: TrendAnalysis | null,
368
- layerStats?: LayerStatistics[]
369
- ): string {
370
- const lines: string[] = [];
371
-
372
- lines.push("# 🛡️ Mandu Guard Report");
373
- lines.push("");
374
- lines.push(`*Generated: ${new Date().toISOString()}*`);
375
- lines.push("");
376
-
377
- // 요약
378
- lines.push("## 📊 Summary");
379
- lines.push("");
380
- lines.push(`| Metric | Value |`);
381
- lines.push(`|--------|-------|`);
382
- lines.push(`| Files Analyzed | ${report.filesAnalyzed} |`);
383
- lines.push(`| Total Violations | ${report.totalViolations} |`);
384
- lines.push(`| Errors | ${report.bySeverity.error} |`);
385
- lines.push(`| Warnings | ${report.bySeverity.warn} |`);
386
- lines.push(`| Info | ${report.bySeverity.info} |`);
387
- lines.push(`| Analysis Time | ${report.analysisTime}ms |`);
388
- lines.push("");
389
-
390
- // 트렌드
391
- if (trend) {
392
- lines.push("## 📈 Trend Analysis");
393
- lines.push("");
394
-
395
- const trendEmoji =
396
- trend.trend === "improving"
397
- ? "📉"
398
- : trend.trend === "degrading"
399
- ? "📈"
400
- : "➡️";
401
-
402
- lines.push(`**Status:** ${trendEmoji} ${trend.trend.toUpperCase()}`);
403
- lines.push("");
404
- lines.push(`- Violation change: ${trend.violationDelta >= 0 ? "+" : ""}${trend.violationDelta}`);
405
- lines.push(`- Change rate: ${trend.violationChangePercent >= 0 ? "+" : ""}${trend.violationChangePercent}%`);
406
- lines.push(`- Period: ${trend.period.days} days`);
407
- lines.push("");
408
-
409
- if (trend.recommendations.length > 0) {
410
- lines.push("### 💡 Recommendations");
411
- lines.push("");
412
- for (const rec of trend.recommendations) {
413
- lines.push(`- ${rec}`);
414
- }
415
- lines.push("");
416
- }
417
- }
418
-
419
- // 레이어 통계
420
- if (layerStats && layerStats.length > 0) {
421
- lines.push("## 🏗️ Layer Health");
422
- lines.push("");
423
- lines.push(`| Layer | Health | Violations | Top Target |`);
424
- lines.push(`|-------|--------|------------|------------|`);
425
-
426
- for (const stat of layerStats) {
427
- const healthEmoji =
428
- stat.healthScore >= 80
429
- ? "🟢"
430
- : stat.healthScore >= 50
431
- ? "🟡"
432
- : "🔴";
433
-
434
- const topTarget = stat.topTargets[0];
435
- const topTargetStr = topTarget ? `${topTarget.layer} (${topTarget.count})` : "-";
436
-
437
- lines.push(
438
- `| ${stat.name} | ${healthEmoji} ${stat.healthScore}% | ${stat.asSource} | ${topTargetStr} |`
439
- );
440
- }
441
- lines.push("");
442
- }
443
-
444
- // 위반 상세
445
- if (report.violations.length > 0) {
446
- lines.push("## ❌ Violations");
447
- lines.push("");
448
-
449
- // 타입별 그룹화
450
- const byType = new Map<ViolationType, Violation[]>();
451
- for (const v of report.violations) {
452
- if (!byType.has(v.type)) {
453
- byType.set(v.type, []);
454
- }
455
- byType.get(v.type)!.push(v);
456
- }
457
-
458
- for (const [type, violations] of byType) {
459
- lines.push(`### ${getTypeTitle(type)} (${violations.length})`);
460
- lines.push("");
461
-
462
- for (const v of violations.slice(0, 10)) {
463
- lines.push(`- **${v.filePath}:${v.line}**`);
464
- lines.push(` - \`${v.fromLayer}\` → \`${v.toLayer}\``);
465
- lines.push(` - ${v.importStatement}`);
466
- if (v.suggestions.length > 0) {
467
- lines.push(` - 💡 ${v.suggestions[0]}`);
468
- }
469
- lines.push("");
470
- }
471
-
472
- if (violations.length > 10) {
473
- lines.push(`*... and ${violations.length - 10} more*`);
474
- lines.push("");
475
- }
476
- }
477
- }
478
-
479
- // 결론
480
- lines.push("---");
481
- lines.push("");
482
-
483
- if (report.totalViolations === 0) {
484
- lines.push("✅ **All clear!** No architecture violations detected.");
485
- } else if (report.bySeverity.error > 0) {
486
- lines.push(`❌ **Action required:** ${report.bySeverity.error} error(s) must be fixed.`);
487
- } else {
488
- lines.push(`⚠️ **Review needed:** ${report.totalViolations} issue(s) found.`);
489
- }
490
-
491
- return lines.join("\n");
492
- }
493
-
494
- /**
495
- * HTML 리포트 생성
496
- */
497
- export function generateHTMLReport(
498
- report: ViolationReport,
499
- trend?: TrendAnalysis | null,
500
- layerStats?: LayerStatistics[]
501
- ): string {
502
- const markdown = generateGuardMarkdownReport(report, trend, layerStats);
503
-
504
- // 간단한 마크다운 → HTML 변환
505
- let html = markdown
506
- .replace(/^# (.+)$/gm, "<h1>$1</h1>")
507
- .replace(/^## (.+)$/gm, "<h2>$1</h2>")
508
- .replace(/^### (.+)$/gm, "<h3>$1</h3>")
509
- .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
510
- .replace(/\*(.+?)\*/g, "<em>$1</em>")
511
- .replace(/`(.+?)`/g, "<code>$1</code>")
512
- .replace(/^- (.+)$/gm, "<li>$1</li>")
513
- .replace(/\n\n/g, "</p><p>")
514
- .replace(/\|(.+)\|/g, (match) => {
515
- const cells = match
516
- .split("|")
517
- .filter((c) => c.trim())
518
- .map((c) => `<td>${c.trim()}</td>`)
519
- .join("");
520
- return `<tr>${cells}</tr>`;
521
- });
522
-
523
- return `<!DOCTYPE html>
524
- <html>
525
- <head>
526
- <meta charset="UTF-8">
527
- <title>Mandu Guard Report</title>
528
- <style>
529
- body {
530
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
531
- max-width: 900px;
532
- margin: 0 auto;
533
- padding: 2rem;
534
- background: #f5f5f5;
535
- }
536
- h1 { color: #333; border-bottom: 2px solid #0066cc; }
537
- h2 { color: #0066cc; margin-top: 2rem; }
538
- table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
539
- th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
540
- code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; }
541
- li { margin: 0.5rem 0; }
542
- .summary { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
543
- </style>
544
- </head>
545
- <body>
546
- <div class="summary">
547
- ${html}
548
- </div>
549
- </body>
550
- </html>`;
551
- }
552
-
553
- // ═══════════════════════════════════════════════════════════════════════════
554
- // Helpers
555
- // ═══════════════════════════════════════════════════════════════════════════
556
-
557
- function generateId(): string {
558
- return `scan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
559
- }
560
-
561
- function getTypeTitle(type: ViolationType): string {
562
- switch (type) {
563
- case "layer-violation":
564
- return "Layer Violations";
565
- case "circular-dependency":
566
- return "Circular Dependencies";
567
- case "cross-slice":
568
- return "Cross-Slice Dependencies";
569
- case "deep-nesting":
570
- return "Deep Nesting";
571
- case "file-type":
572
- return "File Type Violations";
573
- case "invalid-shared-segment":
574
- return "Shared Segment Violations";
575
- default:
576
- return "Violations";
577
- }
578
- }
1
+ /**
2
+ * Mandu Guard Statistics
3
+ *
4
+ * 위반 통계 및 트렌드 분석
5
+ */
6
+
7
+ import { writeFile, readFile, mkdir } from "fs/promises";
8
+ import { dirname, join } from "path";
9
+ import type {
10
+ Violation,
11
+ ViolationReport,
12
+ ViolationType,
13
+ Severity,
14
+ GuardPreset,
15
+ } from "./types";
16
+
17
+ // ═══════════════════════════════════════════════════════════════════════════
18
+ // Types
19
+ // ═══════════════════════════════════════════════════════════════════════════
20
+
21
+ /**
22
+ * 단일 스캔 기록
23
+ */
24
+ export interface ScanRecord {
25
+ /** 스캔 ID */
26
+ id: string;
27
+ /** 스캔 시간 */
28
+ timestamp: number;
29
+ /** 프리셋 */
30
+ preset?: GuardPreset;
31
+ /** 분석된 파일 수 */
32
+ filesAnalyzed: number;
33
+ /** 총 위반 수 */
34
+ totalViolations: number;
35
+ /** 심각도별 카운트 */
36
+ bySeverity: Record<Severity, number>;
37
+ /** 타입별 카운트 */
38
+ byType: Record<ViolationType, number>;
39
+ /** 레이어별 카운트 */
40
+ byLayer: Record<string, number>;
41
+ /** 가장 많은 위반 파일 */
42
+ hotspots: Array<{ file: string; count: number }>;
43
+ }
44
+
45
+ /**
46
+ * 통계 저장소
47
+ */
48
+ export interface StatisticsStore {
49
+ /** 버전 */
50
+ version: number;
51
+ /** 프로젝트 이름 */
52
+ projectName?: string;
53
+ /** 스캔 기록 */
54
+ records: ScanRecord[];
55
+ /** 마지막 업데이트 */
56
+ lastUpdated: number;
57
+ }
58
+
59
+ /**
60
+ * 트렌드 분석
61
+ */
62
+ export interface TrendAnalysis {
63
+ /** 분석 기간 */
64
+ period: {
65
+ start: number;
66
+ end: number;
67
+ days: number;
68
+ };
69
+ /** 위반 변화량 */
70
+ violationDelta: number;
71
+ /** 위반 변화율 (%) */
72
+ violationChangePercent: number;
73
+ /** 개선/악화 */
74
+ trend: "improving" | "stable" | "degrading";
75
+ /** 레이어별 트렌드 */
76
+ byLayer: Record<string, { delta: number; trend: "improving" | "stable" | "degrading" }>;
77
+ /** 권장 사항 */
78
+ recommendations: string[];
79
+ }
80
+
81
+ /**
82
+ * 레이어별 통계
83
+ */
84
+ export interface LayerStatistics {
85
+ /** 레이어 이름 */
86
+ name: string;
87
+ /** 총 위반 수 */
88
+ totalViolations: number;
89
+ /** 위반 원인 레이어 수 */
90
+ asSource: number;
91
+ /** 위반 대상 레이어 수 */
92
+ asTarget: number;
93
+ /** 가장 많이 위반한 타겟 */
94
+ topTargets: Array<{ layer: string; count: number }>;
95
+ /** 건강도 점수 (0-100) */
96
+ healthScore: number;
97
+ }
98
+
99
+ // ═══════════════════════════════════════════════════════════════════════════
100
+ // Statistics Generation
101
+ // ═══════════════════════════════════════════════════════════════════════════
102
+
103
+ /**
104
+ * 리포트에서 스캔 기록 생성
105
+ */
106
+ export function createScanRecord(
107
+ report: ViolationReport,
108
+ preset?: GuardPreset
109
+ ): ScanRecord {
110
+ // 레이어별 카운트
111
+ const byLayer: Record<string, number> = {};
112
+ for (const v of report.violations) {
113
+ byLayer[v.fromLayer] = (byLayer[v.fromLayer] || 0) + 1;
114
+ }
115
+
116
+ // 핫스팟 (가장 많은 위반 파일)
117
+ const fileCounts: Record<string, number> = {};
118
+ for (const v of report.violations) {
119
+ fileCounts[v.filePath] = (fileCounts[v.filePath] || 0) + 1;
120
+ }
121
+
122
+ const hotspots = Object.entries(fileCounts)
123
+ .sort((a, b) => b[1] - a[1])
124
+ .slice(0, 5)
125
+ .map(([file, count]) => ({ file, count }));
126
+
127
+ return {
128
+ id: generateId(),
129
+ timestamp: Date.now(),
130
+ preset,
131
+ filesAnalyzed: report.filesAnalyzed,
132
+ totalViolations: report.totalViolations,
133
+ bySeverity: report.bySeverity,
134
+ byType: report.byType,
135
+ byLayer,
136
+ hotspots,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * 레이어별 통계 계산
142
+ */
143
+ export function calculateLayerStatistics(
144
+ violations: Violation[],
145
+ layers: string[]
146
+ ): LayerStatistics[] {
147
+ const stats: Map<string, LayerStatistics> = new Map();
148
+
149
+ // 초기화
150
+ for (const layer of layers) {
151
+ stats.set(layer, {
152
+ name: layer,
153
+ totalViolations: 0,
154
+ asSource: 0,
155
+ asTarget: 0,
156
+ topTargets: [],
157
+ healthScore: 100,
158
+ });
159
+ }
160
+
161
+ // 위반 집계
162
+ const targetCounts: Map<string, Map<string, number>> = new Map();
163
+
164
+ for (const v of violations) {
165
+ // Source 카운트
166
+ const sourceStat = stats.get(v.fromLayer);
167
+ if (sourceStat) {
168
+ sourceStat.asSource++;
169
+ sourceStat.totalViolations++;
170
+
171
+ // 타겟 카운트
172
+ if (!targetCounts.has(v.fromLayer)) {
173
+ targetCounts.set(v.fromLayer, new Map());
174
+ }
175
+ const targets = targetCounts.get(v.fromLayer)!;
176
+ targets.set(v.toLayer, (targets.get(v.toLayer) || 0) + 1);
177
+ }
178
+
179
+ // Target 카운트
180
+ const targetStat = stats.get(v.toLayer);
181
+ if (targetStat) {
182
+ targetStat.asTarget++;
183
+ }
184
+ }
185
+
186
+ // 건강도 점수 및 Top targets 계산
187
+ for (const [layer, stat] of stats) {
188
+ // 건강도: 위반이 많을수록 낮음
189
+ const maxViolations = 20; // 20개 이상이면 0점
190
+ stat.healthScore = Math.max(0, Math.round(100 - (stat.asSource / maxViolations) * 100));
191
+
192
+ // Top targets
193
+ const targets = targetCounts.get(layer);
194
+ if (targets) {
195
+ stat.topTargets = Array.from(targets.entries())
196
+ .sort((a, b) => b[1] - a[1])
197
+ .slice(0, 3)
198
+ .map(([layer, count]) => ({ layer, count }));
199
+ }
200
+ }
201
+
202
+ return Array.from(stats.values());
203
+ }
204
+
205
+ /**
206
+ * 트렌드 분석
207
+ */
208
+ export function analyzeTrend(
209
+ records: ScanRecord[],
210
+ days: number = 7
211
+ ): TrendAnalysis | null {
212
+ if (records.length < 2) {
213
+ return null;
214
+ }
215
+
216
+ const now = Date.now();
217
+ const periodStart = now - days * 24 * 60 * 60 * 1000;
218
+
219
+ // 기간 내 기록 필터링
220
+ const periodRecords = records.filter((r) => r.timestamp >= periodStart);
221
+
222
+ if (periodRecords.length < 2) {
223
+ return null;
224
+ }
225
+
226
+ // 가장 오래된 것과 가장 최근 것 비교
227
+ const oldest = periodRecords[0];
228
+ const newest = periodRecords[periodRecords.length - 1];
229
+
230
+ const violationDelta = newest.totalViolations - oldest.totalViolations;
231
+ const violationChangePercent =
232
+ oldest.totalViolations === 0
233
+ ? 0
234
+ : Math.round((violationDelta / oldest.totalViolations) * 100);
235
+
236
+ const trend: TrendAnalysis["trend"] =
237
+ violationDelta < -2
238
+ ? "improving"
239
+ : violationDelta > 2
240
+ ? "degrading"
241
+ : "stable";
242
+
243
+ // 레이어별 트렌드
244
+ const allLayers = new Set([
245
+ ...Object.keys(oldest.byLayer || {}),
246
+ ...Object.keys(newest.byLayer || {}),
247
+ ]);
248
+
249
+ const byLayer: TrendAnalysis["byLayer"] = {};
250
+ for (const layer of allLayers) {
251
+ const oldCount = oldest.byLayer?.[layer] || 0;
252
+ const newCount = newest.byLayer?.[layer] || 0;
253
+ const delta = newCount - oldCount;
254
+
255
+ byLayer[layer] = {
256
+ delta,
257
+ trend: delta < -1 ? "improving" : delta > 1 ? "degrading" : "stable",
258
+ };
259
+ }
260
+
261
+ // 권장 사항 생성
262
+ const recommendations: string[] = [];
263
+
264
+ if (trend === "degrading") {
265
+ recommendations.push("위반이 증가하고 있습니다. 새 코드 리뷰를 강화하세요.");
266
+ }
267
+
268
+ const degradingLayers = Object.entries(byLayer)
269
+ .filter(([_, v]) => v.trend === "degrading")
270
+ .map(([k, _]) => k);
271
+
272
+ if (degradingLayers.length > 0) {
273
+ recommendations.push(`주의 필요 레이어: ${degradingLayers.join(", ")}`);
274
+ }
275
+
276
+ const hotspot = newest.hotspots?.[0];
277
+ if (hotspot && hotspot.count > 5) {
278
+ recommendations.push(`핫스팟: ${hotspot.file} (${hotspot.count}개 위반) - 리팩토링 고려`);
279
+ }
280
+
281
+ if (recommendations.length === 0 && trend === "improving") {
282
+ recommendations.push("잘하고 있습니다! 아키텍처 품질이 개선되고 있습니다.");
283
+ }
284
+
285
+ return {
286
+ period: {
287
+ start: oldest.timestamp,
288
+ end: newest.timestamp,
289
+ days,
290
+ },
291
+ violationDelta,
292
+ violationChangePercent,
293
+ trend,
294
+ byLayer,
295
+ recommendations,
296
+ };
297
+ }
298
+
299
+ // ═══════════════════════════════════════════════════════════════════════════
300
+ // Statistics Storage
301
+ // ═══════════════════════════════════════════════════════════════════════════
302
+
303
+ const STATS_FILE = ".mandu/guard-stats.json";
304
+ const MAX_RECORDS = 100;
305
+
306
+ /**
307
+ * 통계 저장소 로드
308
+ */
309
+ export async function loadStatistics(rootDir: string): Promise<StatisticsStore> {
310
+ const filePath = join(rootDir, STATS_FILE);
311
+
312
+ try {
313
+ const content = await readFile(filePath, "utf-8");
314
+ return JSON.parse(content);
315
+ } catch {
316
+ return {
317
+ version: 1,
318
+ records: [],
319
+ lastUpdated: Date.now(),
320
+ };
321
+ }
322
+ }
323
+
324
+ /**
325
+ * 통계 저장소 저장
326
+ */
327
+ export async function saveStatistics(
328
+ rootDir: string,
329
+ store: StatisticsStore
330
+ ): Promise<void> {
331
+ const filePath = join(rootDir, STATS_FILE);
332
+
333
+ // 디렉토리 생성
334
+ await mkdir(dirname(filePath), { recursive: true });
335
+
336
+ // 레코드 수 제한
337
+ if (store.records.length > MAX_RECORDS) {
338
+ store.records = store.records.slice(-MAX_RECORDS);
339
+ }
340
+
341
+ store.lastUpdated = Date.now();
342
+
343
+ await writeFile(filePath, JSON.stringify(store, null, 2));
344
+ }
345
+
346
+ /**
347
+ * 스캔 기록 추가
348
+ */
349
+ export async function addScanRecord(
350
+ rootDir: string,
351
+ record: ScanRecord
352
+ ): Promise<void> {
353
+ const store = await loadStatistics(rootDir);
354
+ store.records.push(record);
355
+ await saveStatistics(rootDir, store);
356
+ }
357
+
358
+ // ═══════════════════════════════════════════════════════════════════════════
359
+ // Report Generation
360
+ // ═══════════════════════════════════════════════════════════════════════════
361
+
362
+ /**
363
+ * 마크다운 리포트 생성 (Guard Report)
364
+ */
365
+ export function generateGuardMarkdownReport(
366
+ report: ViolationReport,
367
+ trend?: TrendAnalysis | null,
368
+ layerStats?: LayerStatistics[]
369
+ ): string {
370
+ const lines: string[] = [];
371
+
372
+ lines.push("# 🛡️ Mandu Guard Report");
373
+ lines.push("");
374
+ lines.push(`*Generated: ${new Date().toISOString()}*`);
375
+ lines.push("");
376
+
377
+ // 요약
378
+ lines.push("## 📊 Summary");
379
+ lines.push("");
380
+ lines.push(`| Metric | Value |`);
381
+ lines.push(`|--------|-------|`);
382
+ lines.push(`| Files Analyzed | ${report.filesAnalyzed} |`);
383
+ lines.push(`| Total Violations | ${report.totalViolations} |`);
384
+ lines.push(`| Errors | ${report.bySeverity.error} |`);
385
+ lines.push(`| Warnings | ${report.bySeverity.warn} |`);
386
+ lines.push(`| Info | ${report.bySeverity.info} |`);
387
+ lines.push(`| Analysis Time | ${report.analysisTime}ms |`);
388
+ lines.push("");
389
+
390
+ // 트렌드
391
+ if (trend) {
392
+ lines.push("## 📈 Trend Analysis");
393
+ lines.push("");
394
+
395
+ const trendEmoji =
396
+ trend.trend === "improving"
397
+ ? "📉"
398
+ : trend.trend === "degrading"
399
+ ? "📈"
400
+ : "➡️";
401
+
402
+ lines.push(`**Status:** ${trendEmoji} ${trend.trend.toUpperCase()}`);
403
+ lines.push("");
404
+ lines.push(`- Violation change: ${trend.violationDelta >= 0 ? "+" : ""}${trend.violationDelta}`);
405
+ lines.push(`- Change rate: ${trend.violationChangePercent >= 0 ? "+" : ""}${trend.violationChangePercent}%`);
406
+ lines.push(`- Period: ${trend.period.days} days`);
407
+ lines.push("");
408
+
409
+ if (trend.recommendations.length > 0) {
410
+ lines.push("### 💡 Recommendations");
411
+ lines.push("");
412
+ for (const rec of trend.recommendations) {
413
+ lines.push(`- ${rec}`);
414
+ }
415
+ lines.push("");
416
+ }
417
+ }
418
+
419
+ // 레이어 통계
420
+ if (layerStats && layerStats.length > 0) {
421
+ lines.push("## 🏗️ Layer Health");
422
+ lines.push("");
423
+ lines.push(`| Layer | Health | Violations | Top Target |`);
424
+ lines.push(`|-------|--------|------------|------------|`);
425
+
426
+ for (const stat of layerStats) {
427
+ const healthEmoji =
428
+ stat.healthScore >= 80
429
+ ? "🟢"
430
+ : stat.healthScore >= 50
431
+ ? "🟡"
432
+ : "🔴";
433
+
434
+ const topTarget = stat.topTargets[0];
435
+ const topTargetStr = topTarget ? `${topTarget.layer} (${topTarget.count})` : "-";
436
+
437
+ lines.push(
438
+ `| ${stat.name} | ${healthEmoji} ${stat.healthScore}% | ${stat.asSource} | ${topTargetStr} |`
439
+ );
440
+ }
441
+ lines.push("");
442
+ }
443
+
444
+ // 위반 상세
445
+ if (report.violations.length > 0) {
446
+ lines.push("## ❌ Violations");
447
+ lines.push("");
448
+
449
+ // 타입별 그룹화
450
+ const byType = new Map<ViolationType, Violation[]>();
451
+ for (const v of report.violations) {
452
+ if (!byType.has(v.type)) {
453
+ byType.set(v.type, []);
454
+ }
455
+ byType.get(v.type)!.push(v);
456
+ }
457
+
458
+ for (const [type, violations] of byType) {
459
+ lines.push(`### ${getTypeTitle(type)} (${violations.length})`);
460
+ lines.push("");
461
+
462
+ for (const v of violations.slice(0, 10)) {
463
+ lines.push(`- **${v.filePath}:${v.line}**`);
464
+ lines.push(` - \`${v.fromLayer}\` → \`${v.toLayer}\``);
465
+ lines.push(` - ${v.importStatement}`);
466
+ if (v.suggestions.length > 0) {
467
+ lines.push(` - 💡 ${v.suggestions[0]}`);
468
+ }
469
+ lines.push("");
470
+ }
471
+
472
+ if (violations.length > 10) {
473
+ lines.push(`*... and ${violations.length - 10} more*`);
474
+ lines.push("");
475
+ }
476
+ }
477
+ }
478
+
479
+ // 결론
480
+ lines.push("---");
481
+ lines.push("");
482
+
483
+ if (report.totalViolations === 0) {
484
+ lines.push("✅ **All clear!** No architecture violations detected.");
485
+ } else if (report.bySeverity.error > 0) {
486
+ lines.push(`❌ **Action required:** ${report.bySeverity.error} error(s) must be fixed.`);
487
+ } else {
488
+ lines.push(`⚠️ **Review needed:** ${report.totalViolations} issue(s) found.`);
489
+ }
490
+
491
+ return lines.join("\n");
492
+ }
493
+
494
+ /**
495
+ * HTML 리포트 생성
496
+ */
497
+ export function generateHTMLReport(
498
+ report: ViolationReport,
499
+ trend?: TrendAnalysis | null,
500
+ layerStats?: LayerStatistics[]
501
+ ): string {
502
+ const markdown = generateGuardMarkdownReport(report, trend, layerStats);
503
+
504
+ // 간단한 마크다운 → HTML 변환
505
+ let html = markdown
506
+ .replace(/^# (.+)$/gm, "<h1>$1</h1>")
507
+ .replace(/^## (.+)$/gm, "<h2>$1</h2>")
508
+ .replace(/^### (.+)$/gm, "<h3>$1</h3>")
509
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
510
+ .replace(/\*(.+?)\*/g, "<em>$1</em>")
511
+ .replace(/`(.+?)`/g, "<code>$1</code>")
512
+ .replace(/^- (.+)$/gm, "<li>$1</li>")
513
+ .replace(/\n\n/g, "</p><p>")
514
+ .replace(/\|(.+)\|/g, (match) => {
515
+ const cells = match
516
+ .split("|")
517
+ .filter((c) => c.trim())
518
+ .map((c) => `<td>${c.trim()}</td>`)
519
+ .join("");
520
+ return `<tr>${cells}</tr>`;
521
+ });
522
+
523
+ return `<!DOCTYPE html>
524
+ <html>
525
+ <head>
526
+ <meta charset="UTF-8">
527
+ <title>Mandu Guard Report</title>
528
+ <style>
529
+ body {
530
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
531
+ max-width: 900px;
532
+ margin: 0 auto;
533
+ padding: 2rem;
534
+ background: #f5f5f5;
535
+ }
536
+ h1 { color: #333; border-bottom: 2px solid #0066cc; }
537
+ h2 { color: #0066cc; margin-top: 2rem; }
538
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
539
+ th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
540
+ code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; }
541
+ li { margin: 0.5rem 0; }
542
+ .summary { background: white; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
543
+ </style>
544
+ </head>
545
+ <body>
546
+ <div class="summary">
547
+ ${html}
548
+ </div>
549
+ </body>
550
+ </html>`;
551
+ }
552
+
553
+ // ═══════════════════════════════════════════════════════════════════════════
554
+ // Helpers
555
+ // ═══════════════════════════════════════════════════════════════════════════
556
+
557
+ function generateId(): string {
558
+ return `scan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
559
+ }
560
+
561
+ function getTypeTitle(type: ViolationType): string {
562
+ switch (type) {
563
+ case "layer-violation":
564
+ return "Layer Violations";
565
+ case "circular-dependency":
566
+ return "Circular Dependencies";
567
+ case "cross-slice":
568
+ return "Cross-Slice Dependencies";
569
+ case "deep-nesting":
570
+ return "Deep Nesting";
571
+ case "file-type":
572
+ return "File Type Violations";
573
+ case "invalid-shared-segment":
574
+ return "Shared Segment Violations";
575
+ default:
576
+ return "Violations";
577
+ }
578
+ }