@girardelli/architect 1.2.0 → 1.3.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.
@@ -1,7 +1,8 @@
1
1
  import { AnalysisReport, AntiPattern } from './types.js';
2
2
 
3
3
  /**
4
- * Gera relatórios HTML visuais premium a partir de AnalysisReport
4
+ * Generates premium visual HTML reports from AnalysisReport.
5
+ * Features: D3.js force graph, bubble charts, radar chart, animated counters.
5
6
  */
6
7
  export class HtmlReportGenerator {
7
8
  generateHtml(report: AnalysisReport): string {
@@ -15,22 +16,22 @@ export class HtmlReportGenerator {
15
16
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
17
  <title>Architect Report — ${this.escapeHtml(report.projectInfo.name)}</title>
17
18
  ${this.getStyles()}
18
- <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"><\/script>
19
+ <script src="https://cdn.jsdelivr.net/npm/d3@7"><\/script>
19
20
  </head>
20
21
  <body>
21
22
  ${this.renderHeader(report)}
22
23
  <div class="container">
23
24
  ${this.renderScoreHero(report)}
25
+ ${this.renderRadarChart(report)}
24
26
  ${this.renderStats(report)}
25
27
  ${this.renderLayers(report)}
28
+ ${this.renderDependencyGraph(report)}
29
+ ${this.renderAntiPatternBubbles(report, grouped)}
26
30
  ${this.renderAntiPatterns(report, grouped)}
27
- ${this.renderDiagram(report)}
28
31
  ${this.renderSuggestions(sugGrouped)}
29
32
  </div>
30
33
  ${this.renderFooter()}
31
- <script>
32
- mermaid.initialize({ theme: 'default', startOnLoad: true });
33
- <\/script>
34
+ ${this.getScripts(report)}
34
35
  </body>
35
36
  </html>`;
36
37
  }
@@ -143,7 +144,7 @@ ${this.renderFooter()}
143
144
  stroke-dashoffset="${offset}" />
144
145
  </svg>
145
146
  <div class="score-value">
146
- <div class="number" style="color: ${this.scoreColor(overall)}">${overall}</div>
147
+ <div class="number score-counter" data-target="${overall}" style="color: ${this.scoreColor(overall)}">0</div>
147
148
  <div class="label">/ 100</div>
148
149
  <div class="grade">${this.scoreLabel(overall)}</div>
149
150
  </div>
@@ -154,23 +155,35 @@ ${this.renderFooter()}
154
155
  </div>`;
155
156
  }
156
157
 
158
+ /**
159
+ * Radar chart for the 4 score components
160
+ */
161
+ private renderRadarChart(report: AnalysisReport): string {
162
+ const entries = Object.entries(report.score.breakdown);
163
+ return `
164
+ <h2 class="section-title">🎯 Health Radar</h2>
165
+ <div class="card" style="display: flex; justify-content: center;">
166
+ <svg id="radar-chart" width="350" height="350" viewBox="0 0 350 350"></svg>
167
+ </div>`;
168
+ }
169
+
157
170
  private renderStats(report: AnalysisReport): string {
158
171
  return `
159
172
  <div class="stats-grid">
160
173
  <div class="stat-card">
161
- <div class="value">${report.projectInfo.totalFiles}</div>
174
+ <div class="value stat-counter" data-target="${report.projectInfo.totalFiles}">0</div>
162
175
  <div class="label">Files Scanned</div>
163
176
  </div>
164
177
  <div class="stat-card">
165
- <div class="value">${report.projectInfo.totalLines.toLocaleString()}</div>
178
+ <div class="value stat-counter" data-target="${report.projectInfo.totalLines}">0</div>
166
179
  <div class="label">Lines of Code</div>
167
180
  </div>
168
181
  <div class="stat-card">
169
- <div class="value">${report.antiPatterns.length}</div>
182
+ <div class="value stat-counter" data-target="${report.antiPatterns.length}">0</div>
170
183
  <div class="label">Anti-Patterns</div>
171
184
  </div>
172
185
  <div class="stat-card">
173
- <div class="value">${report.dependencyGraph.edges.length}</div>
186
+ <div class="value stat-counter" data-target="${report.dependencyGraph.edges.length}">0</div>
174
187
  <div class="label">Dependencies</div>
175
188
  </div>
176
189
  </div>`;
@@ -204,7 +217,60 @@ ${this.renderFooter()}
204
217
  <div class="layers-grid">${cards}</div>`;
205
218
  }
206
219
 
207
- private renderAntiPatterns(
220
+ /**
221
+ * Interactive D3.js force-directed dependency graph
222
+ */
223
+ private renderDependencyGraph(report: AnalysisReport): string {
224
+ if (report.dependencyGraph.edges.length === 0) return '';
225
+
226
+ // Build node data with connection counts
227
+ const connectionCount: Record<string, number> = {};
228
+ for (const edge of report.dependencyGraph.edges) {
229
+ connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
230
+ connectionCount[edge.to] = (connectionCount[edge.to] || 0) + 1;
231
+ }
232
+
233
+ const layerMap: Record<string, string> = {};
234
+ for (const layer of report.layers) {
235
+ for (const file of layer.files) {
236
+ layerMap[file] = layer.name;
237
+ }
238
+ }
239
+
240
+ const nodes = report.dependencyGraph.nodes.map(n => ({
241
+ id: n,
242
+ name: n.split('/').pop() || n,
243
+ connections: connectionCount[n] || 0,
244
+ layer: layerMap[n] || 'Other',
245
+ }));
246
+
247
+ const links = report.dependencyGraph.edges.map(e => ({
248
+ source: e.from,
249
+ target: e.to,
250
+ }));
251
+
252
+ return `
253
+ <h2 class="section-title">🔗 Dependency Graph</h2>
254
+ <div class="card graph-card">
255
+ <div class="graph-legend">
256
+ <span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
257
+ <span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
258
+ <span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
259
+ <span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
260
+ <span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
261
+ <span class="legend-item"><span class="legend-dot" style="background: #64748b"></span> Other</span>
262
+ </div>
263
+ <div id="dep-graph" style="width:100%; min-height:400px;"></div>
264
+ <div class="graph-hint">🖱️ Drag nodes to explore • Node size = number of connections</div>
265
+ </div>
266
+ <script type="application/json" id="graph-nodes">${JSON.stringify(nodes)}<\/script>
267
+ <script type="application/json" id="graph-links">${JSON.stringify(links)}<\/script>`;
268
+ }
269
+
270
+ /**
271
+ * Bubble chart for anti-patterns — bigger = more severe
272
+ */
273
+ private renderAntiPatternBubbles(
208
274
  report: AnalysisReport,
209
275
  grouped: Record<string, { count: number; severity: string; locations: string[]; suggestion: string }>
210
276
  ): string {
@@ -216,6 +282,36 @@ ${this.renderFooter()}
216
282
  </div>`;
217
283
  }
218
284
 
285
+ const severityWeight: Record<string, number> = {
286
+ CRITICAL: 80, HIGH: 60, MEDIUM: 40, LOW: 25,
287
+ };
288
+
289
+ const severityColor: Record<string, string> = {
290
+ CRITICAL: '#ef4444', HIGH: '#f59e0b', MEDIUM: '#60a5fa', LOW: '#22c55e',
291
+ };
292
+
293
+ const bubbles = Object.entries(grouped).map(([name, data]) => ({
294
+ name,
295
+ count: data.count,
296
+ severity: data.severity,
297
+ radius: (severityWeight[data.severity] || 30) + data.count * 8,
298
+ color: severityColor[data.severity] || '#64748b',
299
+ }));
300
+
301
+ return `
302
+ <h2 class="section-title">🫧 Anti-Pattern Impact Map</h2>
303
+ <div class="card" style="display:flex; justify-content:center;">
304
+ <div id="bubble-chart" style="width:100%; min-height:300px;"></div>
305
+ </div>
306
+ <script type="application/json" id="bubble-data">${JSON.stringify(bubbles)}<\/script>`;
307
+ }
308
+
309
+ private renderAntiPatterns(
310
+ report: AnalysisReport,
311
+ grouped: Record<string, { count: number; severity: string; locations: string[]; suggestion: string }>
312
+ ): string {
313
+ if (report.antiPatterns.length === 0) return '';
314
+
219
315
  const rows = Object.entries(grouped)
220
316
  .sort((a, b) => b[1].count - a[1].count)
221
317
  .map(
@@ -234,7 +330,7 @@ ${this.renderFooter()}
234
330
  .join('');
235
331
 
236
332
  return `
237
- <h2 class="section-title">⚠️ Anti-Patterns (${report.antiPatterns.length})</h2>
333
+ <h2 class="section-title">⚠️ Anti-Pattern Details (${report.antiPatterns.length})</h2>
238
334
  <div class="card">
239
335
  <table>
240
336
  <thead>
@@ -251,18 +347,6 @@ ${this.renderFooter()}
251
347
  </div>`;
252
348
  }
253
349
 
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
350
  private renderSuggestions(
267
351
  suggestions: Array<{ priority: string; title: string; description: string; impact: string; count: number }>
268
352
  ): string {
@@ -304,11 +388,266 @@ ${this.renderFooter()}
304
388
  private renderFooter(): string {
305
389
  return `
306
390
  <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>
391
+ <p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect</a> — AI-powered architecture analysis</p>
392
+ <p>By <strong>Camilo Girardelli</strong> · <a href="https://www.girardellitecnologia.com">Girardelli Tecnologia</a></p>
309
393
  </div>`;
310
394
  }
311
395
 
396
+ /**
397
+ * All JavaScript for D3.js visualizations, animated counters, and radar chart
398
+ */
399
+ private getScripts(report: AnalysisReport): string {
400
+ const breakdown = report.score.breakdown;
401
+ return `<script>
402
+ // ── Animated Counters ──
403
+ document.addEventListener('DOMContentLoaded', () => {
404
+ const counters = document.querySelectorAll('.score-counter, .stat-counter');
405
+ const observer = new IntersectionObserver((entries) => {
406
+ entries.forEach(entry => {
407
+ if (entry.isIntersecting) {
408
+ const el = entry.target;
409
+ const target = parseInt(el.dataset.target || '0');
410
+ animateCounter(el, target);
411
+ observer.unobserve(el);
412
+ }
413
+ });
414
+ }, { threshold: 0.5 });
415
+
416
+ counters.forEach(c => observer.observe(c));
417
+ });
418
+
419
+ function animateCounter(el, target) {
420
+ const duration = 1500;
421
+ const start = performance.now();
422
+ const update = (now) => {
423
+ const elapsed = now - start;
424
+ const progress = Math.min(elapsed / duration, 1);
425
+ const ease = 1 - Math.pow(1 - progress, 3);
426
+ el.textContent = Math.round(target * ease).toLocaleString();
427
+ if (progress < 1) requestAnimationFrame(update);
428
+ };
429
+ requestAnimationFrame(update);
430
+ }
431
+
432
+ // ── Radar Chart ──
433
+ (function() {
434
+ const data = [
435
+ { axis: 'Modularity', value: ${breakdown.modularity} },
436
+ { axis: 'Coupling', value: ${breakdown.coupling} },
437
+ { axis: 'Cohesion', value: ${breakdown.cohesion} },
438
+ { axis: 'Layering', value: ${breakdown.layering} },
439
+ ];
440
+
441
+ const svg = d3.select('#radar-chart');
442
+ const w = 350, h = 350, cx = w/2, cy = h/2, maxR = 120;
443
+ const levels = 5;
444
+ const total = data.length;
445
+ const angleSlice = (Math.PI * 2) / total;
446
+
447
+ // Grid circles
448
+ for (let i = 1; i <= levels; i++) {
449
+ const r = (maxR / levels) * i;
450
+ svg.append('circle')
451
+ .attr('cx', cx).attr('cy', cy).attr('r', r)
452
+ .attr('fill', 'none').attr('stroke', '#334155').attr('stroke-width', 0.5)
453
+ .attr('stroke-dasharray', '4,4');
454
+
455
+ svg.append('text')
456
+ .attr('x', cx + 4).attr('y', cy - r + 4)
457
+ .text(Math.round(100 / levels * i))
458
+ .attr('fill', '#475569').attr('font-size', '10px');
459
+ }
460
+
461
+ // Axis lines
462
+ data.forEach((d, i) => {
463
+ const angle = angleSlice * i - Math.PI/2;
464
+ const x = cx + Math.cos(angle) * (maxR + 20);
465
+ const y = cy + Math.sin(angle) * (maxR + 20);
466
+
467
+ svg.append('line')
468
+ .attr('x1', cx).attr('y1', cy).attr('x2', cx + Math.cos(angle) * maxR).attr('y2', cy + Math.sin(angle) * maxR)
469
+ .attr('stroke', '#334155').attr('stroke-width', 1);
470
+
471
+ svg.append('text')
472
+ .attr('x', x).attr('y', y)
473
+ .attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
474
+ .attr('fill', '#94a3b8').attr('font-size', '12px').attr('font-weight', '600')
475
+ .text(d.axis);
476
+ });
477
+
478
+ // Data polygon
479
+ const points = data.map((d, i) => {
480
+ const angle = angleSlice * i - Math.PI/2;
481
+ const r = (d.value / 100) * maxR;
482
+ return [cx + Math.cos(angle) * r, cy + Math.sin(angle) * r];
483
+ });
484
+
485
+ const pointsStr = points.map(p => p.join(',')).join(' ');
486
+
487
+ svg.append('polygon')
488
+ .attr('points', pointsStr)
489
+ .attr('fill', 'rgba(129, 140, 248, 0.15)')
490
+ .attr('stroke', '#818cf8').attr('stroke-width', 2);
491
+
492
+ // Data dots
493
+ points.forEach((p, i) => {
494
+ const color = data[i].value >= 70 ? '#22c55e' : data[i].value >= 50 ? '#f59e0b' : '#ef4444';
495
+ svg.append('circle')
496
+ .attr('cx', p[0]).attr('cy', p[1]).attr('r', 5)
497
+ .attr('fill', color).attr('stroke', '#0f172a').attr('stroke-width', 2);
498
+
499
+ svg.append('text')
500
+ .attr('x', p[0]).attr('y', p[1] - 12)
501
+ .attr('text-anchor', 'middle')
502
+ .attr('fill', color).attr('font-size', '12px').attr('font-weight', '700')
503
+ .text(data[i].value);
504
+ });
505
+ })();
506
+
507
+ // ── D3 Force Dependency Graph ──
508
+ (function() {
509
+ const nodesEl = document.getElementById('graph-nodes');
510
+ const linksEl = document.getElementById('graph-links');
511
+ if (!nodesEl || !linksEl) return;
512
+
513
+ const nodes = JSON.parse(nodesEl.textContent || '[]');
514
+ const links = JSON.parse(linksEl.textContent || '[]');
515
+ if (nodes.length === 0) return;
516
+
517
+ const container = document.getElementById('dep-graph');
518
+ const width = container.clientWidth || 800;
519
+ const height = Math.max(400, nodes.length * 25);
520
+ container.style.height = height + 'px';
521
+
522
+ const layerColors = {
523
+ API: '#ec4899', Service: '#3b82f6', Data: '#10b981',
524
+ UI: '#f59e0b', Infrastructure: '#8b5cf6', Other: '#64748b',
525
+ };
526
+
527
+ const svg = d3.select('#dep-graph').append('svg')
528
+ .attr('width', width).attr('height', height)
529
+ .attr('viewBox', [0, 0, width, height]);
530
+
531
+ // Arrow marker
532
+ svg.append('defs').append('marker')
533
+ .attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10')
534
+ .attr('refX', 20).attr('refY', 0).attr('orient', 'auto')
535
+ .attr('markerWidth', 6).attr('markerHeight', 6)
536
+ .append('path').attr('d', 'M 0,-5 L 10,0 L 0,5')
537
+ .attr('fill', '#475569');
538
+
539
+ const simulation = d3.forceSimulation(nodes)
540
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80))
541
+ .force('charge', d3.forceManyBody().strength(-200))
542
+ .force('center', d3.forceCenter(width / 2, height / 2))
543
+ .force('collision', d3.forceCollide().radius(d => Math.max(d.connections * 3 + 12, 15)));
544
+
545
+ const link = svg.append('g')
546
+ .selectAll('line').data(links).join('line')
547
+ .attr('stroke', '#334155').attr('stroke-width', 1.5)
548
+ .attr('stroke-opacity', 0.6).attr('marker-end', 'url(#arrowhead)');
549
+
550
+ const node = svg.append('g')
551
+ .selectAll('g').data(nodes).join('g')
552
+ .call(d3.drag()
553
+ .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
554
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
555
+ .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
556
+ );
557
+
558
+ // Node circles — size based on connections
559
+ node.append('circle')
560
+ .attr('r', d => Math.max(d.connections * 3 + 6, 8))
561
+ .attr('fill', d => layerColors[d.layer] || '#64748b')
562
+ .attr('stroke', '#0f172a').attr('stroke-width', 2)
563
+ .attr('opacity', 0.85);
564
+
565
+ // Node labels
566
+ node.append('text')
567
+ .text(d => d.name.replace(/\\.[^.]+$/, ''))
568
+ .attr('x', 0).attr('y', d => -(Math.max(d.connections * 3 + 6, 8) + 6))
569
+ .attr('text-anchor', 'middle')
570
+ .attr('fill', '#94a3b8').attr('font-size', '10px').attr('font-weight', '500');
571
+
572
+ // Tooltip on hover
573
+ node.append('title')
574
+ .text(d => d.id + '\\nConnections: ' + d.connections + '\\nLayer: ' + d.layer);
575
+
576
+ simulation.on('tick', () => {
577
+ link
578
+ .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
579
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
580
+ node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
581
+ });
582
+ })();
583
+
584
+ // ── Bubble Chart ──
585
+ (function() {
586
+ const dataEl = document.getElementById('bubble-data');
587
+ if (!dataEl) return;
588
+
589
+ const bubbles = JSON.parse(dataEl.textContent || '[]');
590
+ if (bubbles.length === 0) return;
591
+
592
+ const container = document.getElementById('bubble-chart');
593
+ const width = container.clientWidth || 600;
594
+ const height = 300;
595
+
596
+ const svg = d3.select('#bubble-chart').append('svg')
597
+ .attr('width', width).attr('height', height)
598
+ .attr('viewBox', [0, 0, width, height]);
599
+
600
+ const simulation = d3.forceSimulation(bubbles)
601
+ .force('charge', d3.forceManyBody().strength(5))
602
+ .force('center', d3.forceCenter(width / 2, height / 2))
603
+ .force('collision', d3.forceCollide().radius(d => d.radius + 4))
604
+ .stop();
605
+
606
+ for (let i = 0; i < 120; i++) simulation.tick();
607
+
608
+ const g = svg.selectAll('g').data(bubbles).join('g')
609
+ .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
610
+
611
+ // Glow effect
612
+ g.append('circle')
613
+ .attr('r', d => d.radius)
614
+ .attr('fill', d => d.color + '20')
615
+ .attr('stroke', d => d.color).attr('stroke-width', 2)
616
+ .attr('opacity', 0)
617
+ .transition().duration(800).delay((d, i) => i * 200)
618
+ .attr('opacity', 1);
619
+
620
+ // Inner circle
621
+ g.append('circle')
622
+ .attr('r', d => d.radius * 0.7)
623
+ .attr('fill', d => d.color + '30')
624
+ .attr('opacity', 0)
625
+ .transition().duration(800).delay((d, i) => i * 200)
626
+ .attr('opacity', 1);
627
+
628
+ // Name
629
+ g.append('text')
630
+ .text(d => d.name)
631
+ .attr('text-anchor', 'middle').attr('dy', '-0.3em')
632
+ .attr('fill', '#e2e8f0').attr('font-size', d => Math.max(d.radius / 4, 10) + 'px')
633
+ .attr('font-weight', '700');
634
+
635
+ // Count badge
636
+ g.append('text')
637
+ .text(d => '×' + d.count)
638
+ .attr('text-anchor', 'middle').attr('dy', '1.2em')
639
+ .attr('fill', d => d.color).attr('font-size', d => Math.max(d.radius / 5, 9) + 'px')
640
+ .attr('font-weight', '600');
641
+
642
+ // Severity label
643
+ g.append('text')
644
+ .text(d => d.severity)
645
+ .attr('text-anchor', 'middle').attr('dy', '2.5em')
646
+ .attr('fill', '#64748b').attr('font-size', '9px').attr('text-transform', 'uppercase');
647
+ })();
648
+ <\/script>`;
649
+ }
650
+
312
651
  private getStyles(): string {
313
652
  return `<style>
314
653
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@@ -409,6 +748,20 @@ ${this.renderFooter()}
409
748
  }
410
749
  .success-card { border-color: #22c55e40; color: #22c55e; text-align: center; padding: 2rem; font-size: 1.1rem; }
411
750
 
751
+ /* ── Graph ── */
752
+ .graph-card { padding: 1rem; }
753
+ .graph-legend {
754
+ display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
755
+ justify-content: center;
756
+ }
757
+ .legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: #94a3b8; }
758
+ .legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
759
+ .graph-hint {
760
+ text-align: center; font-size: 0.75rem; color: #475569; margin-top: 0.5rem;
761
+ font-style: italic;
762
+ }
763
+ #dep-graph svg { background: rgba(0,0,0,0.2); border-radius: 12px; }
764
+
412
765
  /* ── Layers Grid ── */
413
766
  .layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
414
767
  .layer-card {
@@ -449,11 +802,6 @@ ${this.renderFooter()}
449
802
  .locations { font-size: 0.75rem; color: #64748b; }
450
803
  .locations code { background: #0f172a; padding: 1px 4px; border-radius: 3px; font-size: 0.7rem; }
451
804
 
452
- /* ── Mermaid ── */
453
- .mermaid-container {
454
- background: #f8fafc; border-radius: 12px; padding: 2rem; text-align: center; color: #0f172a;
455
- }
456
-
457
805
  /* ── Footer ── */
458
806
  .footer {
459
807
  text-align: center; padding: 2rem; color: #475569; font-size: 0.85rem;
@@ -478,7 +826,6 @@ ${this.renderFooter()}
478
826
  .card, .stat-card, .score-hero, .layer-card, .score-item {
479
827
  background: white; border-color: #e2e8f0;
480
828
  }
481
- .mermaid-container { border: 1px solid #e2e8f0; }
482
829
  }
483
830
  </style>`;
484
831
  }
package/src/index.ts CHANGED
@@ -59,7 +59,7 @@ class Architect implements ArchitectCommand {
59
59
  const diagramGenerator = new DiagramGenerator();
60
60
  const layerDiagram = diagramGenerator.generateLayerDiagram(layers);
61
61
 
62
- const suggestions = this.generateSuggestions(antiPatterns, score);
62
+ const suggestions = this.generateSuggestions(antiPatterns, score, edges);
63
63
 
64
64
  const report: AnalysisReport = {
65
65
  timestamp: new Date().toISOString(),
@@ -160,35 +160,96 @@ class Architect implements ArchitectCommand {
160
160
  }
161
161
 
162
162
  private generateSuggestions(
163
- antiPatterns: Array<{ name: string; severity: string; description: string; suggestion: string }>,
164
- score: { overall: number; breakdown: Record<string, number> }
163
+ antiPatterns: Array<{ name: string; severity: string; description: string; suggestion: string; location?: string; affectedFiles?: string[] }>,
164
+ score: { overall: number; breakdown: Record<string, number> },
165
+ edges?: { from: string; to: string }[]
165
166
  ) {
166
167
  const suggestions: Array<{ priority: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; title: string; description: string; impact: string }> = [];
167
168
 
169
+ // 1. Anti-pattern specific suggestions with file context
168
170
  for (const pattern of antiPatterns) {
169
171
  const priority = pattern.severity === 'CRITICAL' ? 'CRITICAL' as const : 'HIGH' as const;
172
+ const location = pattern.location ? ` in \`${pattern.location}\`` : '';
173
+ const affected = pattern.affectedFiles?.length
174
+ ? ` Affected files: ${pattern.affectedFiles.slice(0, 3).map(f => `\`${f}\``).join(', ')}${pattern.affectedFiles.length > 3 ? ` (+${pattern.affectedFiles.length - 3} more)` : ''}.`
175
+ : '';
176
+
170
177
  suggestions.push({
171
178
  priority,
172
179
  title: pattern.name,
173
- description: pattern.suggestion,
180
+ description: `${pattern.suggestion}${location}.${affected}`,
174
181
  impact: `Addressing this ${pattern.name} will improve overall architecture score`,
175
182
  });
176
183
  }
177
184
 
178
- if (score.breakdown.coupling < 70) {
185
+ // 2. Hub Detection — find files with many connections
186
+ if (edges && edges.length > 0) {
187
+ const connectionCount: Record<string, number> = {};
188
+ for (const edge of edges) {
189
+ connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
190
+ connectionCount[edge.to] = (connectionCount[edge.to] || 0) + 1;
191
+ }
192
+
193
+ const hubThreshold = 5;
194
+ const hubs = Object.entries(connectionCount)
195
+ .filter(([_, count]) => count >= hubThreshold)
196
+ .sort((a, b) => b[1] - a[1]);
197
+
198
+ for (const [file, count] of hubs.slice(0, 3)) {
199
+ const fileName = file.split('/').pop() || file;
200
+ const isBarrel = ['__init__.py', 'index.ts', 'index.js'].includes(fileName);
201
+
202
+ if (!isBarrel) {
203
+ suggestions.push({
204
+ priority: 'HIGH',
205
+ title: `Hub File: ${fileName}`,
206
+ description: `\`${file}\` has ${count} connections. Consider extracting a facade or splitting responsibilities to reduce coupling.`,
207
+ impact: `Reducing connections in \`${fileName}\` can improve coupling score by 10-15 points`,
208
+ });
209
+ }
210
+ }
211
+
212
+ // 3. Cross-boundary imports — files importing from many different directories
213
+ const crossBoundary: Record<string, Set<string>> = {};
214
+ for (const edge of edges) {
215
+ const fromDir = edge.from.split('/').slice(0, -1).join('/');
216
+ const toDir = edge.to.split('/').slice(0, -1).join('/');
217
+ if (fromDir !== toDir) {
218
+ if (!crossBoundary[edge.from]) crossBoundary[edge.from] = new Set();
219
+ crossBoundary[edge.from].add(toDir);
220
+ }
221
+ }
222
+
223
+ const crossViolators = Object.entries(crossBoundary)
224
+ .filter(([_, dirs]) => dirs.size >= 3)
225
+ .sort((a, b) => b[1].size - a[1].size);
226
+
227
+ for (const [file, dirs] of crossViolators.slice(0, 2)) {
228
+ const fileName = file.split('/').pop() || file;
229
+ suggestions.push({
230
+ priority: 'MEDIUM',
231
+ title: `Cross-boundary: ${fileName}`,
232
+ description: `\`${file}\` imports from ${dirs.size} different modules (${Array.from(dirs).slice(0, 3).join(', ')}). Consider dependency injection or a mediator pattern.`,
233
+ impact: `Reducing cross-boundary imports can improve cohesion by 5-10 points`,
234
+ });
235
+ }
236
+ }
237
+
238
+ // 4. Score-based suggestions (only if no specific suggestions cover it)
239
+ if (score.breakdown.coupling < 70 && !suggestions.some(s => s.title.startsWith('Hub File'))) {
179
240
  suggestions.push({
180
241
  priority: 'HIGH',
181
242
  title: 'Reduce Coupling',
182
- description: 'Use dependency injection and invert control to reduce module interdependencies',
243
+ description: 'Use dependency injection and invert control to reduce module interdependencies. Consider the Strategy or Observer pattern for loose coupling.',
183
244
  impact: 'Can improve coupling score by 15-20 points',
184
245
  });
185
246
  }
186
247
 
187
- if (score.breakdown.cohesion < 70) {
248
+ if (score.breakdown.cohesion < 70 && !suggestions.some(s => s.title.startsWith('Cross-boundary'))) {
188
249
  suggestions.push({
189
250
  priority: 'MEDIUM',
190
251
  title: 'Improve Cohesion',
191
- description: 'Group related functionality closer together; consider extracting utility modules',
252
+ description: 'Group related functionality closer together. Files that use each other frequently should be in the same module/package.',
192
253
  impact: 'Can improve cohesion score by 10-15 points',
193
254
  });
194
255
  }