@mandujs/core 0.12.1 → 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 (177) hide show
  1. package/README.ko.md +304 -304
  2. package/README.md +653 -653
  3. package/package.json +8 -8
  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/healing.ts +2 -0
  106. package/src/guard/index.ts +2 -0
  107. package/src/guard/negotiation.ts +430 -4
  108. package/src/guard/presets/atomic.ts +70 -70
  109. package/src/guard/presets/clean.ts +77 -77
  110. package/src/guard/presets/cqrs.test.ts +175 -0
  111. package/src/guard/presets/cqrs.ts +107 -0
  112. package/src/guard/presets/fsd.ts +79 -79
  113. package/src/guard/presets/hexagonal.ts +68 -68
  114. package/src/guard/presets/index.ts +291 -288
  115. package/src/guard/reporter.ts +445 -445
  116. package/src/guard/rules.ts +12 -12
  117. package/src/guard/statistics.ts +578 -578
  118. package/src/guard/suggestions.ts +358 -352
  119. package/src/guard/types.ts +348 -347
  120. package/src/guard/validator.ts +834 -834
  121. package/src/guard/watcher.ts +404 -404
  122. package/src/index.ts +1 -0
  123. package/src/intent/index.ts +310 -310
  124. package/src/island/index.ts +304 -304
  125. package/src/logging/index.ts +22 -22
  126. package/src/logging/transports.ts +365 -365
  127. package/src/paths.test.ts +47 -0
  128. package/src/paths.ts +47 -0
  129. package/src/plugins/index.ts +38 -38
  130. package/src/plugins/registry.ts +377 -377
  131. package/src/plugins/types.ts +363 -363
  132. package/src/report/build.ts +1 -1
  133. package/src/report/index.ts +1 -1
  134. package/src/router/fs-patterns.ts +387 -387
  135. package/src/router/fs-routes.ts +344 -401
  136. package/src/router/fs-scanner.ts +497 -497
  137. package/src/router/fs-types.ts +270 -278
  138. package/src/router/index.ts +81 -81
  139. package/src/runtime/boundary.tsx +232 -232
  140. package/src/runtime/compose.ts +222 -222
  141. package/src/runtime/lifecycle.ts +381 -381
  142. package/src/runtime/logger.test.ts +345 -345
  143. package/src/runtime/logger.ts +677 -677
  144. package/src/runtime/router.test.ts +476 -476
  145. package/src/runtime/router.ts +105 -105
  146. package/src/runtime/security.ts +155 -155
  147. package/src/runtime/server.ts +24 -24
  148. package/src/runtime/session-key.ts +328 -328
  149. package/src/runtime/ssr.ts +367 -367
  150. package/src/runtime/streaming-ssr.ts +1245 -1245
  151. package/src/runtime/trace.ts +144 -144
  152. package/src/seo/index.ts +214 -214
  153. package/src/seo/integration/ssr.ts +307 -307
  154. package/src/seo/render/basic.ts +427 -427
  155. package/src/seo/render/index.ts +143 -143
  156. package/src/seo/render/jsonld.ts +539 -539
  157. package/src/seo/render/opengraph.ts +191 -191
  158. package/src/seo/render/robots.ts +116 -116
  159. package/src/seo/render/sitemap.ts +137 -137
  160. package/src/seo/render/twitter.ts +126 -126
  161. package/src/seo/resolve/index.ts +353 -353
  162. package/src/seo/resolve/opengraph.ts +143 -143
  163. package/src/seo/resolve/robots.ts +73 -73
  164. package/src/seo/resolve/title.ts +94 -94
  165. package/src/seo/resolve/twitter.ts +73 -73
  166. package/src/seo/resolve/url.ts +97 -97
  167. package/src/seo/routes/index.ts +290 -290
  168. package/src/seo/types.ts +575 -575
  169. package/src/slot/validator.ts +39 -39
  170. package/src/spec/index.ts +3 -3
  171. package/src/spec/load.ts +76 -76
  172. package/src/spec/lock.ts +56 -56
  173. package/src/utils/bun.ts +8 -8
  174. package/src/utils/lru-cache.ts +75 -75
  175. package/src/utils/safe-io.ts +188 -188
  176. package/src/utils/string-safe.ts +298 -298
  177. 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
+ }