@mandujs/core 0.12.2 → 0.13.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.
Files changed (173) hide show
  1. package/README.ko.md +304 -304
  2. package/README.md +653 -653
  3. package/package.json +1 -1
  4. package/src/brain/architecture/analyzer.ts +28 -26
  5. package/src/brain/doctor/analyzer.ts +1 -1
  6. package/src/bundler/build.ts +91 -91
  7. package/src/bundler/css.ts +302 -302
  8. package/src/bundler/dev.ts +0 -1
  9. package/src/change/history.ts +3 -3
  10. package/src/change/snapshot.ts +10 -9
  11. package/src/change/transaction.ts +2 -2
  12. package/src/client/Link.tsx +227 -227
  13. package/src/client/globals.ts +44 -44
  14. package/src/client/hooks.ts +267 -267
  15. package/src/client/index.ts +5 -5
  16. package/src/client/island.ts +8 -8
  17. package/src/client/router.ts +435 -435
  18. package/src/client/runtime.ts +23 -23
  19. package/src/client/serialize.ts +404 -404
  20. package/src/client/window-state.ts +101 -101
  21. package/src/config/mandu.ts +94 -96
  22. package/src/config/validate.ts +213 -215
  23. package/src/config/watcher.ts +311 -311
  24. package/src/constants.ts +40 -40
  25. package/src/content/content-layer.ts +314 -314
  26. package/src/content/content.test.ts +433 -433
  27. package/src/content/data-store.ts +245 -245
  28. package/src/content/digest.ts +133 -133
  29. package/src/content/index.ts +164 -164
  30. package/src/content/loader-context.ts +172 -172
  31. package/src/content/loaders/api.ts +216 -216
  32. package/src/content/loaders/file.ts +169 -169
  33. package/src/content/loaders/glob.ts +252 -252
  34. package/src/content/loaders/index.ts +34 -34
  35. package/src/content/loaders/types.ts +137 -137
  36. package/src/content/meta-store.ts +209 -209
  37. package/src/content/types.ts +282 -282
  38. package/src/content/watcher.ts +135 -135
  39. package/src/contract/client-safe.test.ts +42 -42
  40. package/src/contract/client-safe.ts +114 -114
  41. package/src/contract/client.ts +16 -16
  42. package/src/contract/define.ts +459 -459
  43. package/src/contract/handler.ts +10 -10
  44. package/src/contract/normalize.test.ts +276 -276
  45. package/src/contract/normalize.ts +404 -404
  46. package/src/contract/registry.test.ts +206 -206
  47. package/src/contract/registry.ts +568 -568
  48. package/src/contract/schema.ts +48 -48
  49. package/src/contract/types.ts +58 -58
  50. package/src/contract/validator.ts +32 -32
  51. package/src/devtools/ai/context-builder.ts +375 -375
  52. package/src/devtools/ai/index.ts +25 -25
  53. package/src/devtools/ai/mcp-connector.ts +465 -465
  54. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  55. package/src/devtools/client/catchers/index.ts +18 -18
  56. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  57. package/src/devtools/client/components/index.ts +39 -39
  58. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  59. package/src/devtools/client/components/mandu-character.tsx +241 -241
  60. package/src/devtools/client/components/overlay.tsx +368 -368
  61. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  62. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  63. package/src/devtools/client/components/panel/index.ts +32 -32
  64. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  65. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  66. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  67. package/src/devtools/client/filters/context-filters.ts +282 -282
  68. package/src/devtools/client/filters/index.ts +16 -16
  69. package/src/devtools/client/index.ts +63 -63
  70. package/src/devtools/client/persistence.ts +335 -335
  71. package/src/devtools/client/state-manager.ts +478 -478
  72. package/src/devtools/design-tokens.ts +263 -263
  73. package/src/devtools/hook/create-hook.ts +207 -207
  74. package/src/devtools/hook/index.ts +13 -13
  75. package/src/devtools/index.ts +439 -439
  76. package/src/devtools/init.ts +266 -266
  77. package/src/devtools/protocol.ts +237 -237
  78. package/src/devtools/server/index.ts +17 -17
  79. package/src/devtools/server/source-context.ts +444 -444
  80. package/src/devtools/types.ts +319 -319
  81. package/src/devtools/worker/index.ts +25 -25
  82. package/src/devtools/worker/redaction-worker.ts +222 -222
  83. package/src/devtools/worker/worker-manager.ts +409 -409
  84. package/src/error/classifier.ts +2 -2
  85. package/src/error/domains.ts +265 -265
  86. package/src/error/formatter.ts +32 -32
  87. package/src/error/result.ts +46 -46
  88. package/src/error/stack-analyzer.ts +5 -0
  89. package/src/error/types.ts +6 -6
  90. package/src/errors/extractor.ts +409 -409
  91. package/src/errors/index.ts +19 -19
  92. package/src/filling/auth.ts +308 -308
  93. package/src/filling/context.ts +569 -569
  94. package/src/filling/deps.ts +238 -238
  95. package/src/generator/contract-glue.ts +2 -1
  96. package/src/generator/generate.ts +12 -10
  97. package/src/generator/index.ts +3 -3
  98. package/src/generator/templates.ts +80 -79
  99. package/src/guard/analyzer.ts +360 -360
  100. package/src/guard/ast-analyzer.ts +806 -806
  101. package/src/guard/auto-correct.ts +1 -1
  102. package/src/guard/check.ts +128 -128
  103. package/src/guard/contract-guard.ts +9 -9
  104. package/src/guard/file-type.test.ts +24 -24
  105. package/src/guard/presets/atomic.ts +70 -70
  106. package/src/guard/presets/clean.ts +77 -77
  107. package/src/guard/presets/cqrs.test.ts +35 -14
  108. package/src/guard/presets/fsd.ts +79 -79
  109. package/src/guard/presets/hexagonal.ts +68 -68
  110. package/src/guard/presets/index.ts +291 -291
  111. package/src/guard/reporter.ts +445 -445
  112. package/src/guard/rules.ts +12 -12
  113. package/src/guard/statistics.ts +578 -578
  114. package/src/guard/suggestions.ts +358 -358
  115. package/src/guard/types.ts +348 -348
  116. package/src/guard/validator.ts +834 -834
  117. package/src/guard/watcher.ts +404 -404
  118. package/src/index.ts +1 -0
  119. package/src/intent/index.ts +310 -310
  120. package/src/island/index.ts +304 -304
  121. package/src/logging/index.ts +22 -22
  122. package/src/logging/transports.ts +365 -365
  123. package/src/paths.test.ts +47 -0
  124. package/src/paths.ts +47 -0
  125. package/src/plugins/index.ts +38 -38
  126. package/src/plugins/registry.ts +377 -377
  127. package/src/plugins/types.ts +363 -363
  128. package/src/report/build.ts +1 -1
  129. package/src/report/index.ts +1 -1
  130. package/src/router/fs-patterns.ts +387 -387
  131. package/src/router/fs-routes.ts +344 -401
  132. package/src/router/fs-scanner.ts +497 -497
  133. package/src/router/fs-types.ts +270 -278
  134. package/src/router/index.ts +81 -81
  135. package/src/runtime/boundary.tsx +232 -232
  136. package/src/runtime/compose.ts +222 -222
  137. package/src/runtime/lifecycle.ts +381 -381
  138. package/src/runtime/logger.test.ts +345 -345
  139. package/src/runtime/logger.ts +677 -677
  140. package/src/runtime/router.test.ts +476 -476
  141. package/src/runtime/router.ts +105 -105
  142. package/src/runtime/security.ts +155 -155
  143. package/src/runtime/server.ts +24 -24
  144. package/src/runtime/session-key.ts +328 -328
  145. package/src/runtime/ssr.ts +367 -367
  146. package/src/runtime/streaming-ssr.ts +1245 -1245
  147. package/src/runtime/trace.ts +144 -144
  148. package/src/seo/index.ts +214 -214
  149. package/src/seo/integration/ssr.ts +307 -307
  150. package/src/seo/render/basic.ts +427 -427
  151. package/src/seo/render/index.ts +143 -143
  152. package/src/seo/render/jsonld.ts +539 -539
  153. package/src/seo/render/opengraph.ts +191 -191
  154. package/src/seo/render/robots.ts +116 -116
  155. package/src/seo/render/sitemap.ts +137 -137
  156. package/src/seo/render/twitter.ts +126 -126
  157. package/src/seo/resolve/index.ts +353 -353
  158. package/src/seo/resolve/opengraph.ts +143 -143
  159. package/src/seo/resolve/robots.ts +73 -73
  160. package/src/seo/resolve/title.ts +94 -94
  161. package/src/seo/resolve/twitter.ts +73 -73
  162. package/src/seo/resolve/url.ts +97 -97
  163. package/src/seo/routes/index.ts +290 -290
  164. package/src/seo/types.ts +575 -575
  165. package/src/slot/validator.ts +39 -39
  166. package/src/spec/index.ts +3 -3
  167. package/src/spec/load.ts +76 -76
  168. package/src/spec/lock.ts +56 -56
  169. package/src/utils/bun.ts +8 -8
  170. package/src/utils/lru-cache.ts +75 -75
  171. package/src/utils/safe-io.ts +188 -188
  172. package/src/utils/string-safe.ts +298 -298
  173. package/src/watcher/rules.ts +5 -5
@@ -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
+ }