@girardelli/architect 1.1.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 (66) hide show
  1. package/CONTRIBUTING.md +140 -0
  2. package/LICENSE +21 -0
  3. package/PROJECT_STRUCTURE.txt +168 -0
  4. package/README.md +269 -0
  5. package/dist/analyzer.d.ts +17 -0
  6. package/dist/analyzer.d.ts.map +1 -0
  7. package/dist/analyzer.js +254 -0
  8. package/dist/analyzer.js.map +1 -0
  9. package/dist/anti-patterns.d.ts +17 -0
  10. package/dist/anti-patterns.d.ts.map +1 -0
  11. package/dist/anti-patterns.js +211 -0
  12. package/dist/anti-patterns.js.map +1 -0
  13. package/dist/cli.d.ts +15 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +164 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/config.d.ts +6 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +73 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/diagram.d.ts +9 -0
  22. package/dist/diagram.d.ts.map +1 -0
  23. package/dist/diagram.js +116 -0
  24. package/dist/diagram.js.map +1 -0
  25. package/dist/html-reporter.d.ts +23 -0
  26. package/dist/html-reporter.d.ts.map +1 -0
  27. package/dist/html-reporter.js +454 -0
  28. package/dist/html-reporter.js.map +1 -0
  29. package/dist/index.d.ts +48 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +151 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/reporter.d.ts +13 -0
  34. package/dist/reporter.d.ts.map +1 -0
  35. package/dist/reporter.js +135 -0
  36. package/dist/reporter.js.map +1 -0
  37. package/dist/scanner.d.ts +25 -0
  38. package/dist/scanner.d.ts.map +1 -0
  39. package/dist/scanner.js +288 -0
  40. package/dist/scanner.js.map +1 -0
  41. package/dist/scorer.d.ts +15 -0
  42. package/dist/scorer.d.ts.map +1 -0
  43. package/dist/scorer.js +172 -0
  44. package/dist/scorer.js.map +1 -0
  45. package/dist/types.d.ts +106 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +2 -0
  48. package/dist/types.js.map +1 -0
  49. package/examples/sample-report.md +207 -0
  50. package/jest.config.js +18 -0
  51. package/package.json +70 -0
  52. package/src/analyzer.ts +310 -0
  53. package/src/anti-patterns.ts +264 -0
  54. package/src/cli.ts +183 -0
  55. package/src/config.ts +82 -0
  56. package/src/diagram.ts +144 -0
  57. package/src/html-reporter.ts +485 -0
  58. package/src/index.ts +212 -0
  59. package/src/reporter.ts +166 -0
  60. package/src/scanner.ts +298 -0
  61. package/src/scorer.ts +193 -0
  62. package/src/types.ts +114 -0
  63. package/tests/anti-patterns.test.ts +94 -0
  64. package/tests/scanner.test.ts +55 -0
  65. package/tests/scorer.test.ts +80 -0
  66. package/tsconfig.json +24 -0
@@ -0,0 +1,485 @@
1
+ import { AnalysisReport, AntiPattern } from './types.js';
2
+
3
+ /**
4
+ * Gera relatórios HTML visuais premium a partir de AnalysisReport
5
+ */
6
+ export class HtmlReportGenerator {
7
+ generateHtml(report: AnalysisReport): string {
8
+ const grouped = this.groupAntiPatterns(report.antiPatterns);
9
+ const sugGrouped = this.groupSuggestions(report.suggestions);
10
+
11
+ return `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>Architect Report — ${this.escapeHtml(report.projectInfo.name)}</title>
17
+ ${this.getStyles()}
18
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"><\/script>
19
+ </head>
20
+ <body>
21
+ ${this.renderHeader(report)}
22
+ <div class="container">
23
+ ${this.renderScoreHero(report)}
24
+ ${this.renderStats(report)}
25
+ ${this.renderLayers(report)}
26
+ ${this.renderAntiPatterns(report, grouped)}
27
+ ${this.renderDiagram(report)}
28
+ ${this.renderSuggestions(sugGrouped)}
29
+ </div>
30
+ ${this.renderFooter()}
31
+ <script>
32
+ mermaid.initialize({ theme: 'default', startOnLoad: true });
33
+ <\/script>
34
+ </body>
35
+ </html>`;
36
+ }
37
+
38
+ private scoreColor(score: number): string {
39
+ if (score >= 70) return '#22c55e';
40
+ if (score >= 50) return '#f59e0b';
41
+ return '#ef4444';
42
+ }
43
+
44
+ private scoreEmoji(score: number): string {
45
+ if (score >= 70) return '✅';
46
+ if (score >= 50) return '⚠️';
47
+ return '❌';
48
+ }
49
+
50
+ private scoreLabel(score: number): string {
51
+ if (score >= 90) return 'Excellent';
52
+ if (score >= 70) return 'Good';
53
+ if (score >= 50) return 'Needs Attention';
54
+ if (score >= 30) return 'Poor';
55
+ return 'Critical';
56
+ }
57
+
58
+ private escapeHtml(text: string): string {
59
+ return text
60
+ .replace(/&/g, '&amp;')
61
+ .replace(/</g, '&lt;')
62
+ .replace(/>/g, '&gt;')
63
+ .replace(/"/g, '&quot;');
64
+ }
65
+
66
+ private groupAntiPatterns(
67
+ antiPatterns: AntiPattern[]
68
+ ): Record<string, { count: number; severity: string; locations: string[]; suggestion: string }> {
69
+ const grouped: Record<string, { count: number; severity: string; locations: string[]; suggestion: string }> = {};
70
+ for (const p of antiPatterns) {
71
+ if (!grouped[p.name]) {
72
+ grouped[p.name] = { count: 0, severity: p.severity, locations: [], suggestion: p.suggestion };
73
+ }
74
+ grouped[p.name].count++;
75
+ if (grouped[p.name].locations.length < 10) {
76
+ grouped[p.name].locations.push(p.location);
77
+ }
78
+ }
79
+ return grouped;
80
+ }
81
+
82
+ private groupSuggestions(
83
+ suggestions: Array<{ priority: string; title: string; description: string; impact: string }>
84
+ ): Array<{ priority: string; title: string; description: string; impact: string; count: number }> {
85
+ const map = new Map<string, { priority: string; title: string; description: string; impact: string; count: number }>();
86
+ for (const s of suggestions) {
87
+ const key = `${s.title}|${s.priority}`;
88
+ if (!map.has(key)) {
89
+ map.set(key, { ...s, count: 0 });
90
+ }
91
+ map.get(key)!.count++;
92
+ }
93
+ return Array.from(map.values()).slice(0, 15);
94
+ }
95
+
96
+ private renderHeader(report: AnalysisReport): string {
97
+ const date = new Date(report.timestamp).toLocaleDateString('en-US', {
98
+ year: 'numeric',
99
+ month: 'long',
100
+ day: 'numeric',
101
+ });
102
+ return `
103
+ <div class="header">
104
+ <h1>🏗️ Architect</h1>
105
+ <p class="subtitle">Architecture Analysis Report</p>
106
+ <div class="meta">
107
+ <span>📂 <strong>${this.escapeHtml(report.projectInfo.name)}</strong></span>
108
+ <span>📁 <strong>${report.projectInfo.totalFiles}</strong> files</span>
109
+ <span>📝 <strong>${report.projectInfo.totalLines.toLocaleString()}</strong> lines</span>
110
+ <span>💻 <strong>${report.projectInfo.primaryLanguages.join(', ')}</strong></span>
111
+ ${report.projectInfo.frameworks.length > 0 ? `<span>🔧 <strong>${report.projectInfo.frameworks.join(', ')}</strong></span>` : ''}
112
+ <span>📅 <strong>${date}</strong></span>
113
+ </div>
114
+ </div>`;
115
+ }
116
+
117
+ private renderScoreHero(report: AnalysisReport): string {
118
+ const overall = report.score.overall;
119
+ const circumference = 2 * Math.PI * 85;
120
+ const offset = circumference * (1 - overall / 100);
121
+
122
+ const breakdownItems = Object.entries(report.score.breakdown)
123
+ .map(
124
+ ([name, score]) => `
125
+ <div class="score-item">
126
+ <div class="name">${name}</div>
127
+ <div class="val" style="color: ${this.scoreColor(score)}">${score} ${this.scoreEmoji(score)}</div>
128
+ <div class="bar-container">
129
+ <div class="bar" style="width: ${score}%; background: ${this.scoreColor(score)}"></div>
130
+ </div>
131
+ </div>`
132
+ )
133
+ .join('');
134
+
135
+ return `
136
+ <div class="score-hero">
137
+ <div class="score-circle">
138
+ <svg viewBox="0 0 200 200" width="180" height="180">
139
+ <circle class="bg" cx="100" cy="100" r="85" />
140
+ <circle class="fg" cx="100" cy="100" r="85"
141
+ stroke="${this.scoreColor(overall)}"
142
+ stroke-dasharray="${circumference}"
143
+ stroke-dashoffset="${offset}" />
144
+ </svg>
145
+ <div class="score-value">
146
+ <div class="number" style="color: ${this.scoreColor(overall)}">${overall}</div>
147
+ <div class="label">/ 100</div>
148
+ <div class="grade">${this.scoreLabel(overall)}</div>
149
+ </div>
150
+ </div>
151
+ <div class="score-breakdown">
152
+ ${breakdownItems}
153
+ </div>
154
+ </div>`;
155
+ }
156
+
157
+ private renderStats(report: AnalysisReport): string {
158
+ return `
159
+ <div class="stats-grid">
160
+ <div class="stat-card">
161
+ <div class="value">${report.projectInfo.totalFiles}</div>
162
+ <div class="label">Files Scanned</div>
163
+ </div>
164
+ <div class="stat-card">
165
+ <div class="value">${report.projectInfo.totalLines.toLocaleString()}</div>
166
+ <div class="label">Lines of Code</div>
167
+ </div>
168
+ <div class="stat-card">
169
+ <div class="value">${report.antiPatterns.length}</div>
170
+ <div class="label">Anti-Patterns</div>
171
+ </div>
172
+ <div class="stat-card">
173
+ <div class="value">${report.dependencyGraph.edges.length}</div>
174
+ <div class="label">Dependencies</div>
175
+ </div>
176
+ </div>`;
177
+ }
178
+
179
+ private renderLayers(report: AnalysisReport): string {
180
+ if (report.layers.length === 0) return '';
181
+
182
+ const layerColors: Record<string, string> = {
183
+ API: '#ec4899',
184
+ Service: '#3b82f6',
185
+ Data: '#10b981',
186
+ UI: '#f59e0b',
187
+ Infrastructure: '#8b5cf6',
188
+ };
189
+
190
+ const cards = report.layers
191
+ .map((l) => {
192
+ const color = layerColors[l.name] || '#64748b';
193
+ return `
194
+ <div class="layer-card" style="--layer-color: ${color}">
195
+ <div class="count" style="color: ${color}">${l.files.length}</div>
196
+ <div class="name">${l.name}</div>
197
+ <div class="desc">${this.escapeHtml(l.description)}</div>
198
+ </div>`;
199
+ })
200
+ .join('');
201
+
202
+ return `
203
+ <h2 class="section-title">📐 Architectural Layers</h2>
204
+ <div class="layers-grid">${cards}</div>`;
205
+ }
206
+
207
+ private renderAntiPatterns(
208
+ report: AnalysisReport,
209
+ grouped: Record<string, { count: number; severity: string; locations: string[]; suggestion: string }>
210
+ ): string {
211
+ if (report.antiPatterns.length === 0) {
212
+ return `
213
+ <h2 class="section-title">✅ Anti-Patterns</h2>
214
+ <div class="card success-card">
215
+ <p>No significant anti-patterns detected. Excellent architecture!</p>
216
+ </div>`;
217
+ }
218
+
219
+ const rows = Object.entries(grouped)
220
+ .sort((a, b) => b[1].count - a[1].count)
221
+ .map(
222
+ ([name, data]) => `
223
+ <tr>
224
+ <td><strong>${this.escapeHtml(name)}</strong></td>
225
+ <td class="count-cell">${data.count}</td>
226
+ <td><span class="severity-badge severity-${data.severity}">${data.severity}</span></td>
227
+ <td><small class="suggestion">${this.escapeHtml(data.suggestion)}</small></td>
228
+ <td><div class="locations">${data.locations
229
+ .slice(0, 5)
230
+ .map((l) => `<code>${this.escapeHtml(l)}</code>`)
231
+ .join(' ')}${data.locations.length > 5 ? ` <em>+${data.count - 5} more</em>` : ''}</div></td>
232
+ </tr>`
233
+ )
234
+ .join('');
235
+
236
+ return `
237
+ <h2 class="section-title">⚠️ Anti-Patterns (${report.antiPatterns.length})</h2>
238
+ <div class="card">
239
+ <table>
240
+ <thead>
241
+ <tr>
242
+ <th>Pattern</th>
243
+ <th>Count</th>
244
+ <th>Severity</th>
245
+ <th>Suggestion</th>
246
+ <th>Locations</th>
247
+ </tr>
248
+ </thead>
249
+ <tbody>${rows}</tbody>
250
+ </table>
251
+ </div>`;
252
+ }
253
+
254
+ private renderDiagram(report: AnalysisReport): string {
255
+ if (!report.diagram.mermaid) return '';
256
+
257
+ return `
258
+ <h2 class="section-title">📊 Architecture Diagram</h2>
259
+ <div class="card">
260
+ <div class="mermaid-container">
261
+ <pre class="mermaid">${this.escapeHtml(report.diagram.mermaid)}</pre>
262
+ </div>
263
+ </div>`;
264
+ }
265
+
266
+ private renderSuggestions(
267
+ suggestions: Array<{ priority: string; title: string; description: string; impact: string; count: number }>
268
+ ): string {
269
+ if (suggestions.length === 0) return '';
270
+
271
+ const rows = suggestions
272
+ .map(
273
+ (s, i) => `
274
+ <tr>
275
+ <td>${i + 1}</td>
276
+ <td><span class="severity-badge severity-${s.priority}">${s.priority}</span></td>
277
+ <td>
278
+ <strong>${this.escapeHtml(s.title)}</strong>
279
+ ${s.count > 1 ? `<span class="count-badge">×${s.count}</span>` : ''}
280
+ <br/><small class="suggestion">${this.escapeHtml(s.description)}</small>
281
+ </td>
282
+ <td class="impact">${this.escapeHtml(s.impact)}</td>
283
+ </tr>`
284
+ )
285
+ .join('');
286
+
287
+ return `
288
+ <h2 class="section-title">💡 Refactoring Suggestions</h2>
289
+ <div class="card">
290
+ <table>
291
+ <thead>
292
+ <tr>
293
+ <th>#</th>
294
+ <th>Priority</th>
295
+ <th>Suggestion</th>
296
+ <th>Impact</th>
297
+ </tr>
298
+ </thead>
299
+ <tbody>${rows}</tbody>
300
+ </table>
301
+ </div>`;
302
+ }
303
+
304
+ private renderFooter(): string {
305
+ return `
306
+ <div class="footer">
307
+ <p>Generated by <a href="https://github.com/camilogivago/architect">🏗️ Architect</a> — AI-powered architecture analysis</p>
308
+ <p>By <strong>Camilo Girardelli</strong> · <a href="https://girardelli.tech">Girardelli Tecnologia</a></p>
309
+ </div>`;
310
+ }
311
+
312
+ private getStyles(): string {
313
+ return `<style>
314
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
315
+
316
+ * { margin: 0; padding: 0; box-sizing: border-box; }
317
+
318
+ body {
319
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
320
+ background: #0f172a;
321
+ color: #e2e8f0;
322
+ line-height: 1.6;
323
+ min-height: 100vh;
324
+ }
325
+
326
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
327
+
328
+ /* ── Header ── */
329
+ .header {
330
+ text-align: center;
331
+ padding: 3rem 2rem;
332
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 50%, #1e1b4b 100%);
333
+ border-bottom: 1px solid #334155;
334
+ margin-bottom: 2rem;
335
+ }
336
+ .header h1 {
337
+ font-size: 2.5rem;
338
+ font-weight: 900;
339
+ background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
340
+ -webkit-background-clip: text;
341
+ -webkit-text-fill-color: transparent;
342
+ margin-bottom: 0.5rem;
343
+ }
344
+ .header .subtitle { color: #94a3b8; font-size: 1.1rem; font-weight: 300; }
345
+ .header .meta {
346
+ margin-top: 1rem;
347
+ display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap;
348
+ }
349
+ .header .meta span {
350
+ background: #1e293b; padding: 0.4rem 1rem; border-radius: 99px;
351
+ font-size: 0.85rem; color: #94a3b8; border: 1px solid #334155;
352
+ }
353
+ .header .meta span strong { color: #e2e8f0; }
354
+
355
+ /* ── Score Hero ── */
356
+ .score-hero {
357
+ display: flex; align-items: center; justify-content: center; gap: 3rem;
358
+ padding: 2.5rem;
359
+ background: linear-gradient(135deg, #1e293b, #1e1b4b);
360
+ border-radius: 24px; border: 1px solid #334155;
361
+ margin-bottom: 2rem; flex-wrap: wrap;
362
+ }
363
+ .score-circle { position: relative; width: 180px; height: 180px; }
364
+ .score-circle svg { transform: rotate(-90deg); }
365
+ .score-circle circle { fill: none; stroke-width: 10; stroke-linecap: round; }
366
+ .score-circle .bg { stroke: #334155; }
367
+ .score-circle .fg { transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1); }
368
+ .score-value {
369
+ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
370
+ text-align: center;
371
+ }
372
+ .score-value .number { font-size: 3rem; font-weight: 900; line-height: 1; }
373
+ .score-value .label { font-size: 0.85rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 2px; }
374
+ .score-value .grade { font-size: 0.75rem; color: #64748b; margin-top: 4px; text-transform: uppercase; letter-spacing: 1px; }
375
+
376
+ .score-breakdown { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
377
+ .score-item {
378
+ padding: 1rem 1.5rem; background: rgba(255,255,255,0.03);
379
+ border-radius: 12px; border: 1px solid #334155; min-width: 200px;
380
+ }
381
+ .score-item .name { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; margin-bottom: 0.3rem; }
382
+ .score-item .bar-container { background: #1e293b; border-radius: 99px; height: 8px; margin-top: 0.5rem; overflow: hidden; }
383
+ .score-item .bar { height: 100%; border-radius: 99px; transition: width 1.5s cubic-bezier(0.4, 0, 0.2, 1); }
384
+ .score-item .val { font-size: 1.5rem; font-weight: 700; }
385
+
386
+ /* ── Stats Grid ── */
387
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
388
+ .stat-card {
389
+ background: linear-gradient(135deg, #1e293b, #0f172a);
390
+ border: 1px solid #334155; border-radius: 16px; padding: 1.5rem; text-align: center;
391
+ }
392
+ .stat-card .value {
393
+ font-size: 2rem; font-weight: 800;
394
+ background: linear-gradient(135deg, #818cf8, #c084fc);
395
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
396
+ }
397
+ .stat-card .label { font-size: 0.85rem; color: #94a3b8; margin-top: 0.3rem; }
398
+
399
+ /* ── Section Title ── */
400
+ .section-title {
401
+ font-size: 1.4rem; font-weight: 700; margin: 2.5rem 0 1rem;
402
+ display: flex; align-items: center; gap: 0.5rem;
403
+ }
404
+
405
+ /* ── Cards ── */
406
+ .card {
407
+ background: #1e293b; border-radius: 16px; border: 1px solid #334155;
408
+ padding: 1.5rem; margin-bottom: 1rem; overflow-x: auto;
409
+ }
410
+ .success-card { border-color: #22c55e40; color: #22c55e; text-align: center; padding: 2rem; font-size: 1.1rem; }
411
+
412
+ /* ── Layers Grid ── */
413
+ .layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
414
+ .layer-card {
415
+ background: linear-gradient(135deg, #1e293b, #0f172a);
416
+ border: 1px solid #334155; border-radius: 16px; padding: 1.5rem;
417
+ text-align: center; position: relative; overflow: hidden;
418
+ }
419
+ .layer-card::before {
420
+ content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
421
+ background: var(--layer-color, #64748b);
422
+ }
423
+ .layer-card .count { font-size: 2.5rem; font-weight: 900; line-height: 1; }
424
+ .layer-card .name { font-size: 1rem; color: #94a3b8; margin-top: 0.3rem; font-weight: 600; }
425
+ .layer-card .desc { font-size: 0.75rem; color: #475569; margin-top: 0.5rem; }
426
+
427
+ /* ── Tables ── */
428
+ table { width: 100%; border-collapse: collapse; }
429
+ th, td { text-align: left; padding: 0.75rem 1rem; border-bottom: 1px solid #334155; }
430
+ th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: #64748b; font-weight: 600; }
431
+ .count-cell { font-weight: 700; font-size: 1.1rem; }
432
+ .impact { color: #94a3b8; font-size: 0.85rem; }
433
+ .suggestion { color: #64748b; font-size: 0.8rem; }
434
+
435
+ .severity-badge {
436
+ display: inline-block; padding: 0.2rem 0.6rem; border-radius: 99px;
437
+ font-size: 0.72rem; font-weight: 600; letter-spacing: 0.5px;
438
+ }
439
+ .severity-CRITICAL { background: #dc262620; color: #ef4444; border: 1px solid #ef444440; }
440
+ .severity-HIGH { background: #f59e0b20; color: #f59e0b; border: 1px solid #f59e0b40; }
441
+ .severity-MEDIUM { background: #3b82f620; color: #60a5fa; border: 1px solid #60a5fa40; }
442
+ .severity-LOW { background: #22c55e20; color: #22c55e; border: 1px solid #22c55e40; }
443
+
444
+ .count-badge {
445
+ display: inline-block; background: #818cf820; color: #818cf8; padding: 0.1rem 0.4rem;
446
+ border-radius: 99px; font-size: 0.7rem; margin-left: 0.5rem; font-weight: 600;
447
+ }
448
+
449
+ .locations { font-size: 0.75rem; color: #64748b; }
450
+ .locations code { background: #0f172a; padding: 1px 4px; border-radius: 3px; font-size: 0.7rem; }
451
+
452
+ /* ── Mermaid ── */
453
+ .mermaid-container {
454
+ background: #f8fafc; border-radius: 12px; padding: 2rem; text-align: center; color: #0f172a;
455
+ }
456
+
457
+ /* ── Footer ── */
458
+ .footer {
459
+ text-align: center; padding: 2rem; color: #475569; font-size: 0.85rem;
460
+ border-top: 1px solid #1e293b; margin-top: 3rem;
461
+ }
462
+ .footer a { color: #818cf8; text-decoration: none; }
463
+ .footer a:hover { text-decoration: underline; }
464
+
465
+ /* ── Responsive ── */
466
+ @media (max-width: 768px) {
467
+ .score-hero { flex-direction: column; gap: 1.5rem; }
468
+ .score-breakdown { grid-template-columns: 1fr; }
469
+ .header h1 { font-size: 1.8rem; }
470
+ .container { padding: 1rem; }
471
+ }
472
+
473
+ /* ── Print ── */
474
+ @media print {
475
+ body { background: white; color: #1e293b; }
476
+ .header { background: white; border-bottom: 2px solid #e2e8f0; }
477
+ .header h1 { -webkit-text-fill-color: #4f46e5; }
478
+ .card, .stat-card, .score-hero, .layer-card, .score-item {
479
+ background: white; border-color: #e2e8f0;
480
+ }
481
+ .mermaid-container { border: 1px solid #e2e8f0; }
482
+ }
483
+ </style>`;
484
+ }
485
+ }
package/src/index.ts ADDED
@@ -0,0 +1,212 @@
1
+ import { ProjectScanner } from './scanner.js';
2
+ import { ArchitectureAnalyzer } from './analyzer.js';
3
+ import { AntiPatternDetector } from './anti-patterns.js';
4
+ import { ArchitectureScorer } from './scorer.js';
5
+ import { DiagramGenerator } from './diagram.js';
6
+ import { ReportGenerator } from './reporter.js';
7
+ import { HtmlReportGenerator } from './html-reporter.js';
8
+ import { ConfigLoader } from './config.js';
9
+ import { AnalysisReport } from './types.js';
10
+ import { relative } from 'path';
11
+
12
+ export interface ArchitectCommand {
13
+ analyze: (path: string) => Promise<AnalysisReport>;
14
+ diagram: (path: string) => Promise<string>;
15
+ score: (path: string) => Promise<{ overall: number; breakdown: Record<string, number> }>;
16
+ antiPatterns: (path: string) => Promise<Array<{ name: string; severity: string; description: string }>>;
17
+ layers: (path: string) => Promise<Array<{ name: string; files: string[] }>>;
18
+ }
19
+
20
+ class Architect implements ArchitectCommand {
21
+ async analyze(projectPath: string): Promise<AnalysisReport> {
22
+ const config = ConfigLoader.loadConfig(projectPath);
23
+
24
+ const scanner = new ProjectScanner(projectPath, config);
25
+ const projectInfo = scanner.scan();
26
+
27
+ if (!projectInfo.fileTree) {
28
+ throw new Error('Failed to scan project');
29
+ }
30
+
31
+ const analyzer = new ArchitectureAnalyzer(projectPath);
32
+ const dependencies = new Map();
33
+
34
+ for (const [file, imports] of analyzer
35
+ .analyzeDependencies(projectInfo.fileTree)
36
+ .reduce(
37
+ (map, edge) => {
38
+ if (!map.has(edge.from)) {
39
+ map.set(edge.from, new Set());
40
+ }
41
+ map.get(edge.from)!.add(edge.to);
42
+ return map;
43
+ },
44
+ new Map<string, Set<string>>()
45
+ )
46
+ .entries()) {
47
+ dependencies.set(file, imports);
48
+ }
49
+
50
+ const edges = analyzer.analyzeDependencies(projectInfo.fileTree);
51
+ const layers = analyzer.detectLayers(projectInfo.fileTree);
52
+
53
+ const detector = new AntiPatternDetector(config);
54
+ const antiPatterns = detector.detect(projectInfo.fileTree, dependencies);
55
+
56
+ const scorer = new ArchitectureScorer();
57
+ const score = scorer.score(edges, antiPatterns, projectInfo.totalFiles);
58
+
59
+ const diagramGenerator = new DiagramGenerator();
60
+ const layerDiagram = diagramGenerator.generateLayerDiagram(layers);
61
+
62
+ const suggestions = this.generateSuggestions(antiPatterns, score);
63
+
64
+ const report: AnalysisReport = {
65
+ timestamp: new Date().toISOString(),
66
+ projectInfo,
67
+ score,
68
+ antiPatterns,
69
+ layers,
70
+ dependencyGraph: {
71
+ nodes: Array.from(new Set([...edges.map((e) => e.from), ...edges.map((e) => e.to)])),
72
+ edges,
73
+ },
74
+ suggestions,
75
+ diagram: {
76
+ mermaid: layerDiagram,
77
+ type: 'layer',
78
+ },
79
+ };
80
+
81
+ // Normalize paths to be relative to project root
82
+ return this.relativizePaths(report, projectPath);
83
+ }
84
+
85
+ private relativizePaths(report: AnalysisReport, basePath: string): AnalysisReport {
86
+ const rel = (p: string): string => {
87
+ if (p.startsWith('/') || p.startsWith('\\')) {
88
+ return relative(basePath, p) || p;
89
+ }
90
+ return p;
91
+ };
92
+
93
+ report.antiPatterns = report.antiPatterns.map((p) => ({
94
+ ...p,
95
+ location: rel(p.location),
96
+ affectedFiles: p.affectedFiles?.map(rel),
97
+ }));
98
+
99
+ report.layers = report.layers.map((l) => ({
100
+ ...l,
101
+ files: l.files.map(rel),
102
+ }));
103
+
104
+ report.dependencyGraph.nodes = report.dependencyGraph.nodes.map(rel);
105
+ report.dependencyGraph.edges = report.dependencyGraph.edges.map((e) => ({
106
+ ...e,
107
+ from: rel(e.from),
108
+ to: rel(e.to),
109
+ }));
110
+
111
+ return report;
112
+ }
113
+
114
+ async diagram(projectPath: string): Promise<string> {
115
+ const config = ConfigLoader.loadConfig(projectPath);
116
+ const scanner = new ProjectScanner(projectPath, config);
117
+ const projectInfo = scanner.scan();
118
+
119
+ if (!projectInfo.fileTree) {
120
+ throw new Error('Failed to scan project');
121
+ }
122
+
123
+ const analyzer = new ArchitectureAnalyzer(projectPath);
124
+ const edges = analyzer.analyzeDependencies(projectInfo.fileTree);
125
+ const layers = analyzer.detectLayers(projectInfo.fileTree);
126
+
127
+ const generator = new DiagramGenerator();
128
+ return generator.generateComponentDiagram(edges, layers);
129
+ }
130
+
131
+ async score(
132
+ projectPath: string
133
+ ): Promise<{ overall: number; breakdown: Record<string, number> }> {
134
+ const report = await this.analyze(projectPath);
135
+ return {
136
+ overall: report.score.overall,
137
+ breakdown: report.score.breakdown,
138
+ };
139
+ }
140
+
141
+ async antiPatterns(
142
+ projectPath: string
143
+ ): Promise<Array<{ name: string; severity: string; description: string }>> {
144
+ const report = await this.analyze(projectPath);
145
+ return report.antiPatterns.map((p) => ({
146
+ name: p.name,
147
+ severity: p.severity,
148
+ description: p.description,
149
+ }));
150
+ }
151
+
152
+ async layers(
153
+ projectPath: string
154
+ ): Promise<Array<{ name: string; files: string[] }>> {
155
+ const report = await this.analyze(projectPath);
156
+ return report.layers.map((l) => ({
157
+ name: l.name,
158
+ files: l.files,
159
+ }));
160
+ }
161
+
162
+ private generateSuggestions(
163
+ antiPatterns: Array<{ name: string; severity: string; description: string; suggestion: string }>,
164
+ score: { overall: number; breakdown: Record<string, number> }
165
+ ) {
166
+ const suggestions: Array<{ priority: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; title: string; description: string; impact: string }> = [];
167
+
168
+ for (const pattern of antiPatterns) {
169
+ const priority = pattern.severity === 'CRITICAL' ? 'CRITICAL' as const : 'HIGH' as const;
170
+ suggestions.push({
171
+ priority,
172
+ title: pattern.name,
173
+ description: pattern.suggestion,
174
+ impact: `Addressing this ${pattern.name} will improve overall architecture score`,
175
+ });
176
+ }
177
+
178
+ if (score.breakdown.coupling < 70) {
179
+ suggestions.push({
180
+ priority: 'HIGH',
181
+ title: 'Reduce Coupling',
182
+ description: 'Use dependency injection and invert control to reduce module interdependencies',
183
+ impact: 'Can improve coupling score by 15-20 points',
184
+ });
185
+ }
186
+
187
+ if (score.breakdown.cohesion < 70) {
188
+ suggestions.push({
189
+ priority: 'MEDIUM',
190
+ title: 'Improve Cohesion',
191
+ description: 'Group related functionality closer together; consider extracting utility modules',
192
+ impact: 'Can improve cohesion score by 10-15 points',
193
+ });
194
+ }
195
+
196
+ return suggestions;
197
+ }
198
+ }
199
+
200
+ export const architect = new Architect();
201
+
202
+ export {
203
+ ProjectScanner,
204
+ ArchitectureAnalyzer,
205
+ AntiPatternDetector,
206
+ ArchitectureScorer,
207
+ DiagramGenerator,
208
+ ReportGenerator,
209
+ HtmlReportGenerator,
210
+ ConfigLoader,
211
+ };
212
+