@girardelli/architect 1.3.0 → 2.2.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 (58) hide show
  1. package/README.md +111 -112
  2. package/dist/agent-generator.d.ts +106 -0
  3. package/dist/agent-generator.d.ts.map +1 -0
  4. package/dist/agent-generator.js +1398 -0
  5. package/dist/agent-generator.js.map +1 -0
  6. package/dist/cli.js +132 -15
  7. package/dist/cli.js.map +1 -1
  8. package/dist/html-reporter.d.ts +8 -2
  9. package/dist/html-reporter.d.ts.map +1 -1
  10. package/dist/html-reporter.js +773 -50
  11. package/dist/html-reporter.js.map +1 -1
  12. package/dist/index.d.ts +26 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +25 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/refactor-engine.d.ts +18 -0
  17. package/dist/refactor-engine.d.ts.map +1 -0
  18. package/dist/refactor-engine.js +86 -0
  19. package/dist/refactor-engine.js.map +1 -0
  20. package/dist/refactor-reporter.d.ts +20 -0
  21. package/dist/refactor-reporter.d.ts.map +1 -0
  22. package/dist/refactor-reporter.js +389 -0
  23. package/dist/refactor-reporter.js.map +1 -0
  24. package/dist/rules/barrel-optimizer.d.ts +13 -0
  25. package/dist/rules/barrel-optimizer.d.ts.map +1 -0
  26. package/dist/rules/barrel-optimizer.js +77 -0
  27. package/dist/rules/barrel-optimizer.js.map +1 -0
  28. package/dist/rules/dead-code-detector.d.ts +21 -0
  29. package/dist/rules/dead-code-detector.d.ts.map +1 -0
  30. package/dist/rules/dead-code-detector.js +117 -0
  31. package/dist/rules/dead-code-detector.js.map +1 -0
  32. package/dist/rules/hub-splitter.d.ts +13 -0
  33. package/dist/rules/hub-splitter.d.ts.map +1 -0
  34. package/dist/rules/hub-splitter.js +110 -0
  35. package/dist/rules/hub-splitter.js.map +1 -0
  36. package/dist/rules/import-organizer.d.ts +13 -0
  37. package/dist/rules/import-organizer.d.ts.map +1 -0
  38. package/dist/rules/import-organizer.js +85 -0
  39. package/dist/rules/import-organizer.js.map +1 -0
  40. package/dist/rules/module-grouper.d.ts +13 -0
  41. package/dist/rules/module-grouper.d.ts.map +1 -0
  42. package/dist/rules/module-grouper.js +110 -0
  43. package/dist/rules/module-grouper.js.map +1 -0
  44. package/dist/types.d.ts +51 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/agent-generator.ts +1526 -0
  48. package/src/cli.ts +150 -15
  49. package/src/html-reporter.ts +799 -51
  50. package/src/index.ts +39 -1
  51. package/src/refactor-engine.ts +117 -0
  52. package/src/refactor-reporter.ts +408 -0
  53. package/src/rules/barrel-optimizer.ts +97 -0
  54. package/src/rules/dead-code-detector.ts +132 -0
  55. package/src/rules/hub-splitter.ts +123 -0
  56. package/src/rules/import-organizer.ts +98 -0
  57. package/src/rules/module-grouper.ts +124 -0
  58. package/src/types.ts +52 -0
@@ -1,11 +1,12 @@
1
- import { AnalysisReport, AntiPattern } from './types.js';
1
+ import { AnalysisReport, AntiPattern, RefactoringPlan, RefactorStep } from './types.js';
2
+ import { AgentSuggestion } from './agent-generator.js';
2
3
 
3
4
  /**
4
5
  * Generates premium visual HTML reports from AnalysisReport.
5
6
  * Features: D3.js force graph, bubble charts, radar chart, animated counters.
6
7
  */
7
8
  export class HtmlReportGenerator {
8
- generateHtml(report: AnalysisReport): string {
9
+ generateHtml(report: AnalysisReport, plan?: RefactoringPlan, agentSuggestion?: AgentSuggestion): string {
9
10
  const grouped = this.groupAntiPatterns(report.antiPatterns);
10
11
  const sugGrouped = this.groupSuggestions(report.suggestions);
11
12
 
@@ -20,15 +21,62 @@ ${this.getStyles()}
20
21
  </head>
21
22
  <body>
22
23
  ${this.renderHeader(report)}
23
- <div class="container">
24
- ${this.renderScoreHero(report)}
25
- ${this.renderRadarChart(report)}
26
- ${this.renderStats(report)}
27
- ${this.renderLayers(report)}
28
- ${this.renderDependencyGraph(report)}
29
- ${this.renderAntiPatternBubbles(report, grouped)}
30
- ${this.renderAntiPatterns(report, grouped)}
31
- ${this.renderSuggestions(sugGrouped)}
24
+ <div class="report-layout">
25
+ <nav class="sidebar" id="reportSidebar">
26
+ <div class="sidebar-title">Navigation</div>
27
+ <a href="#score" class="sidebar-link active" data-section="score">📊 Score</a>
28
+ <a href="#layers" class="sidebar-link" data-section="layers">📐 Layers & Graph</a>
29
+ <a href="#anti-patterns" class="sidebar-link" data-section="anti-patterns">⚠️ Anti-Patterns (${report.antiPatterns.length})</a>
30
+ <a href="#suggestions" class="sidebar-link" data-section="suggestions">💡 Suggestions (${report.suggestions.length})</a>
31
+ ${plan ? `<a href="#refactoring" class="sidebar-link" data-section="refactoring">🔧 Refactoring (${plan.steps.length})</a>` : ''}
32
+ ${agentSuggestion ? `<a href="#agents" class="sidebar-link" data-section="agents">🤖 Agents</a>` : ''}
33
+ </nav>
34
+ <button class="sidebar-toggle" onclick="document.getElementById('reportSidebar').classList.toggle('sidebar-open')">☰</button>
35
+
36
+ <div class="container">
37
+ <div id="score">
38
+ ${this.renderScoreHero(report)}
39
+ ${this.renderRadarChart(report)}
40
+ ${this.renderStats(report)}
41
+ </div>
42
+
43
+ <details class="section-accordion" id="layers" open>
44
+ <summary class="section-accordion-header">📐 Layer Analysis & Dependencies</summary>
45
+ <div class="section-accordion-body">
46
+ ${this.renderLayers(report)}
47
+ ${this.renderDependencyGraph(report)}
48
+ </div>
49
+ </details>
50
+
51
+ <details class="section-accordion" id="anti-patterns" open>
52
+ <summary class="section-accordion-header">⚠️ Anti-Patterns (${report.antiPatterns.length})</summary>
53
+ <div class="section-accordion-body">
54
+ ${this.renderAntiPatternBubbles(report, grouped)}
55
+ ${this.renderAntiPatterns(report, grouped)}
56
+ </div>
57
+ </details>
58
+
59
+ <details class="section-accordion" id="suggestions">
60
+ <summary class="section-accordion-header">💡 Suggestions (${report.suggestions.length})</summary>
61
+ <div class="section-accordion-body">
62
+ ${this.renderSuggestions(sugGrouped)}
63
+ </div>
64
+ </details>
65
+
66
+ ${plan ? `<details class="section-accordion" id="refactoring" open>
67
+ <summary class="section-accordion-header">🔧 Refactoring Plan (${plan.steps.length} steps, ${plan.totalOperations} operations)</summary>
68
+ <div class="section-accordion-body">
69
+ ${this.renderRefactoringPlan(plan)}
70
+ </div>
71
+ </details>` : ''}
72
+
73
+ ${agentSuggestion ? `<details class="section-accordion" id="agents" open>
74
+ <summary class="section-accordion-header">🤖 Agent System</summary>
75
+ <div class="section-accordion-body">
76
+ ${this.renderAgentSuggestions(agentSuggestion)}
77
+ </div>
78
+ </details>` : ''}
79
+ </div>
32
80
  </div>
33
81
  ${this.renderFooter()}
34
82
  ${this.getScripts(report)}
@@ -223,13 +271,21 @@ ${this.getScripts(report)}
223
271
  private renderDependencyGraph(report: AnalysisReport): string {
224
272
  if (report.dependencyGraph.edges.length === 0) return '';
225
273
 
226
- // Build node data with connection counts
274
+ // Build real file set only files that appear as SOURCE in edges (these are real scanned files)
275
+ const realFiles = new Set(report.dependencyGraph.edges.map(e => e.from));
276
+
277
+ // Count connections only for real files
227
278
  const connectionCount: Record<string, number> = {};
228
279
  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;
280
+ if (realFiles.has(edge.from)) {
281
+ connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
282
+ }
283
+ if (realFiles.has(edge.to)) {
284
+ connectionCount[edge.to] = (connectionCount[edge.to] || 0) + 1;
285
+ }
231
286
  }
232
287
 
288
+ // Build layer map from report layers
233
289
  const layerMap: Record<string, string> = {};
234
290
  for (const layer of report.layers) {
235
291
  for (const file of layer.files) {
@@ -237,34 +293,55 @@ ${this.getScripts(report)}
237
293
  }
238
294
  }
239
295
 
240
- const nodes = report.dependencyGraph.nodes.map(n => ({
296
+ // Create nodes only from real files
297
+ const allNodes = [...realFiles].map(n => ({
241
298
  id: n,
242
299
  name: n.split('/').pop() || n,
243
300
  connections: connectionCount[n] || 0,
244
301
  layer: layerMap[n] || 'Other',
245
302
  }));
246
303
 
247
- const links = report.dependencyGraph.edges.map(e => ({
248
- source: e.from,
249
- target: e.to,
250
- }));
304
+ // Build links only between real files
305
+ const allLinks = report.dependencyGraph.edges
306
+ .filter(e => realFiles.has(e.from) && realFiles.has(e.to))
307
+ .map(e => ({ source: e.from, target: e.to }));
308
+
309
+ // Limit to top N most-connected nodes for large projects
310
+ const maxNodes = 60;
311
+ const sortedNodes = [...allNodes].sort((a, b) => b.connections - a.connections);
312
+ const limitedNodes = sortedNodes.slice(0, maxNodes);
313
+ const limitedNodeIds = new Set(limitedNodes.map(n => n.id));
314
+ const limitedLinks = allLinks.filter(l => limitedNodeIds.has(l.source) && limitedNodeIds.has(l.target));
315
+ const isLimited = allNodes.length > maxNodes;
316
+
317
+ // Collect unique layers from limited nodes
318
+ const uniqueLayers = [...new Set(limitedNodes.map(n => n.layer))];
251
319
 
252
320
  return `
253
321
  <h2 class="section-title">🔗 Dependency Graph</h2>
254
322
  <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>
323
+ <div class="graph-controls">
324
+ <div class="graph-legend">
325
+ <span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
326
+ <span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
327
+ <span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
328
+ <span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
329
+ <span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
330
+ <span class="legend-item"><span class="legend-dot" style="background: #64748b"></span> Other</span>
331
+ </div>
332
+ <div class="graph-filters">
333
+ <input type="text" id="graphSearch" class="graph-search" placeholder="🔍 Search node..." oninput="filterGraphNodes(this.value)">
334
+ <div class="graph-layer-filters">
335
+ ${uniqueLayers.map(l => `<label class="graph-filter-check"><input type="checkbox" checked data-layer="${l}" onchange="toggleGraphLayer('${l}', this.checked)"><span class="legend-dot" style="background: ${({'API': '#ec4899', 'Service': '#3b82f6', 'Data': '#10b981', 'UI': '#f59e0b', 'Infrastructure': '#8b5cf6'} as Record<string, string>)[l] || '#64748b'}"></span> ${l}</label>`).join('')}
336
+ </div>
337
+ </div>
338
+ ${isLimited ? `<div class="graph-limit-notice">Showing top ${maxNodes} of ${allNodes.length} source files (most connected) · ${limitedLinks.length} links</div>` : ''}
262
339
  </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>
340
+ <div id="dep-graph" style="width:100%; min-height:500px;"></div>
341
+ <div class="graph-hint">🖱️ Drag nodes • Scroll to zoomDouble-click to reset • Node size = connections</div>
265
342
  </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>`;
343
+ <script type="application/json" id="graph-nodes">${JSON.stringify(limitedNodes)}<\\/script>
344
+ <script type="application/json" id="graph-links">${JSON.stringify(limitedLinks)}<\\/script>`;
268
345
  }
269
346
 
270
347
  /**
@@ -388,11 +465,162 @@ ${this.getScripts(report)}
388
465
  private renderFooter(): string {
389
466
  return `
390
467
  <div class="footer">
391
- <p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect</a> — AI-powered architecture analysis</p>
468
+ <p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect v2.0</a> — AI-powered architecture analysis + refactoring engine</p>
392
469
  <p>By <strong>Camilo Girardelli</strong> · <a href="https://www.girardellitecnologia.com">Girardelli Tecnologia</a></p>
393
470
  </div>`;
394
471
  }
395
472
 
473
+ // ── Refactoring Plan Section ──
474
+
475
+ private opColor(type: string): string {
476
+ switch (type) {
477
+ case 'CREATE': return '#22c55e';
478
+ case 'MOVE': return '#3b82f6';
479
+ case 'MODIFY': return '#f59e0b';
480
+ case 'DELETE': return '#ef4444';
481
+ default: return '#64748b';
482
+ }
483
+ }
484
+
485
+ private opIcon(type: string): string {
486
+ switch (type) {
487
+ case 'CREATE': return '➕';
488
+ case 'MOVE': return '📦';
489
+ case 'MODIFY': return '✏️';
490
+ case 'DELETE': return '🗑️';
491
+ default: return '📄';
492
+ }
493
+ }
494
+
495
+ private renderRefactoringPlan(plan: RefactoringPlan): string {
496
+ if (plan.steps.length === 0) {
497
+ return `
498
+ <h2 class="section-title">✅ Refactoring Plan</h2>
499
+ <div class="card success-card">
500
+ <p>No refactoring needed! Your architecture is already in great shape.</p>
501
+ </div>`;
502
+ }
503
+
504
+ const improvement = plan.estimatedScoreAfter.overall - plan.currentScore.overall;
505
+
506
+ const metrics = Object.keys(plan.currentScore.breakdown) as Array<keyof typeof plan.currentScore.breakdown>;
507
+ const bars = metrics.map(metric => {
508
+ const before = plan.currentScore.breakdown[metric];
509
+ const after = plan.estimatedScoreAfter.breakdown[metric] ?? before;
510
+ const diff = after - before;
511
+ return `
512
+ <div class="comparison-row">
513
+ <div class="refactor-metric-name">${metric}</div>
514
+ <div class="refactor-metric-bars">
515
+ <div class="rbar-before" style="width: ${before}%; background: ${this.scoreColor(before)}40"><span>${before}</span></div>
516
+ <div class="rbar-after" style="width: ${after}%; background: ${this.scoreColor(after)}"><span>${after}</span></div>
517
+ </div>
518
+ <div class="refactor-metric-diff" style="color: ${diff > 0 ? '#22c55e' : '#64748b'}">
519
+ ${diff > 0 ? `+${diff}` : diff === 0 ? '—' : String(diff)}
520
+ </div>
521
+ </div>`;
522
+ }).join('');
523
+
524
+ const stepsHtml = plan.steps.map(step => this.renderRefactorStep(step)).join('');
525
+
526
+ const criticalCount = plan.steps.filter(s => s.priority === 'CRITICAL').length;
527
+ const highCount = plan.steps.filter(s => s.priority === 'HIGH').length;
528
+ const mediumCount = plan.steps.filter(s => s.priority === 'MEDIUM').length;
529
+ const lowCount = plan.steps.filter(s => s.priority === 'LOW').length;
530
+
531
+ return `
532
+ <h2 class="section-title">🔧 Refactoring Plan</h2>
533
+
534
+ <div class="card refactor-score">
535
+ <div class="refactor-score-pair">
536
+ <div class="rscore-box">
537
+ <div class="rscore-num" style="color: ${this.scoreColor(plan.currentScore.overall)}">${plan.currentScore.overall}</div>
538
+ <div class="rscore-label">Current</div>
539
+ </div>
540
+ <div class="rscore-arrow">
541
+ <svg width="60" height="30" viewBox="0 0 60 30">
542
+ <path d="M5 15 L45 15 M40 8 L48 15 L40 22" stroke="#818cf8" stroke-width="2.5" fill="none"/>
543
+ </svg>
544
+ </div>
545
+ <div class="rscore-box">
546
+ <div class="rscore-num" style="color: ${this.scoreColor(plan.estimatedScoreAfter.overall)}">${plan.estimatedScoreAfter.overall}</div>
547
+ <div class="rscore-label">Estimated</div>
548
+ </div>
549
+ <div class="rscore-improvement" style="color: #22c55e">+${improvement} pts</div>
550
+ </div>
551
+ <div class="refactor-bars-section">
552
+ <div class="refactor-legend">
553
+ <span class="rlegend-tag rbefore">Before</span>
554
+ <span class="rlegend-tag rafter">After</span>
555
+ </div>
556
+ ${bars}
557
+ </div>
558
+ </div>
559
+
560
+ <div class="refactor-stats-row">
561
+ <div class="rstat">${plan.steps.length} steps</div>
562
+ <div class="rstat">${plan.totalOperations} operations</div>
563
+ <div class="rstat">Tier 1: ${plan.tier1Steps}</div>
564
+ <div class="rstat">Tier 2: ${plan.tier2Steps}</div>
565
+ </div>
566
+
567
+ <div class="priority-bar">
568
+ ${criticalCount ? `<div class="prio-seg prio-critical" style="flex: ${criticalCount}">🔴 ${criticalCount}</div>` : ''}
569
+ ${highCount ? `<div class="prio-seg prio-high" style="flex: ${highCount}">🟠 ${highCount}</div>` : ''}
570
+ ${mediumCount ? `<div class="prio-seg prio-medium" style="flex: ${mediumCount}">🔵 ${mediumCount}</div>` : ''}
571
+ ${lowCount ? `<div class="prio-seg prio-low" style="flex: ${lowCount}">🟢 ${lowCount}</div>` : ''}
572
+ </div>
573
+
574
+ <div class="refactor-roadmap">
575
+ ${stepsHtml}
576
+ </div>`;
577
+ }
578
+
579
+ private renderRefactorStep(step: RefactorStep): string {
580
+ const operationsHtml = step.operations.map(op => `
581
+ <div class="rop">
582
+ <span class="rop-icon">${this.opIcon(op.type)}</span>
583
+ <span class="rop-badge" style="background: ${this.opColor(op.type)}20; color: ${this.opColor(op.type)}; border: 1px solid ${this.opColor(op.type)}40">${op.type}</span>
584
+ <code class="rop-path">${this.escapeHtml(op.path)}</code>
585
+ ${op.newPath ? `<span class="rop-arrow">→</span> <code class="rop-path">${this.escapeHtml(op.newPath)}</code>` : ''}
586
+ <div class="rop-desc">${this.escapeHtml(op.description)}</div>
587
+ </div>
588
+ `).join('');
589
+
590
+ const impactHtml = step.scoreImpact.map(i =>
591
+ `<span class="rimpact-tag">${i.metric}: ${i.before}→${i.after} <strong>+${i.after - i.before}</strong></span>`
592
+ ).join('');
593
+
594
+ return `
595
+ <div class="rstep-card">
596
+ <div class="rstep-header">
597
+ <div class="rstep-number">${step.id}</div>
598
+ <div class="rstep-info">
599
+ <div class="rstep-title-row">
600
+ <h3>${this.escapeHtml(step.title)}</h3>
601
+ <span class="severity-badge severity-${step.priority}">${step.priority}</span>
602
+ <span class="tier-badge">Tier ${step.tier}</span>
603
+ </div>
604
+ <p class="rstep-desc">${this.escapeHtml(step.description)}</p>
605
+ <details class="rstep-details">
606
+ <summary>📖 Why?</summary>
607
+ <p class="rstep-rationale">${this.escapeHtml(step.rationale)}</p>
608
+ </details>
609
+ </div>
610
+ </div>
611
+ <details class="rstep-ops-accordion">
612
+ <summary class="rstep-ops-toggle">📋 Operations (${step.operations.length})</summary>
613
+ <div class="rstep-ops">
614
+ ${operationsHtml}
615
+ </div>
616
+ </details>
617
+ <div class="rstep-impact">
618
+ <h4>📈 Score Impact</h4>
619
+ <div class="rimpact-tags">${impactHtml}</div>
620
+ </div>
621
+ </div>`;
622
+ }
623
+
396
624
  /**
397
625
  * All JavaScript for D3.js visualizations, animated counters, and radar chart
398
626
  */
@@ -414,6 +642,23 @@ document.addEventListener('DOMContentLoaded', () => {
414
642
  }, { threshold: 0.5 });
415
643
 
416
644
  counters.forEach(c => observer.observe(c));
645
+
646
+ // ── Sidebar Active Section Tracking ──
647
+ const sectionIds = ['score', 'layers', 'anti-patterns', 'suggestions', 'refactoring', 'agents'];
648
+ const sectionObserver = new IntersectionObserver((entries) => {
649
+ entries.forEach(entry => {
650
+ if (entry.isIntersecting) {
651
+ document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
652
+ const link = document.querySelector('.sidebar-link[data-section="' + entry.target.id + '"]');
653
+ if (link) link.classList.add('active');
654
+ }
655
+ });
656
+ }, { threshold: 0.15, rootMargin: '-80px 0px -60% 0px' });
657
+
658
+ sectionIds.forEach(id => {
659
+ const el = document.getElementById(id);
660
+ if (el) sectionObserver.observe(el);
661
+ });
417
662
  });
418
663
 
419
664
  function animateCounter(el, target) {
@@ -516,7 +761,7 @@ function animateCounter(el, target) {
516
761
 
517
762
  const container = document.getElementById('dep-graph');
518
763
  const width = container.clientWidth || 800;
519
- const height = Math.max(400, nodes.length * 25);
764
+ const height = 500;
520
765
  container.style.height = height + 'px';
521
766
 
522
767
  const layerColors = {
@@ -528,26 +773,43 @@ function animateCounter(el, target) {
528
773
  .attr('width', width).attr('height', height)
529
774
  .attr('viewBox', [0, 0, width, height]);
530
775
 
776
+ // Zoom container
777
+ const g = svg.append('g');
778
+
779
+ // Zoom behavior
780
+ const zoom = d3.zoom()
781
+ .scaleExtent([0.2, 5])
782
+ .on('zoom', (event) => { g.attr('transform', event.transform); });
783
+ svg.call(zoom);
784
+
785
+ // Double-click to reset zoom
786
+ svg.on('dblclick.zoom', () => {
787
+ svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
788
+ });
789
+
531
790
  // Arrow marker
532
- svg.append('defs').append('marker')
791
+ g.append('defs').append('marker')
533
792
  .attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10')
534
793
  .attr('refX', 20).attr('refY', 0).attr('orient', 'auto')
535
794
  .attr('markerWidth', 6).attr('markerHeight', 6)
536
795
  .append('path').attr('d', 'M 0,-5 L 10,0 L 0,5')
537
796
  .attr('fill', '#475569');
538
797
 
798
+ // Tuned simulation for better spread
539
799
  const simulation = d3.forceSimulation(nodes)
540
800
  .force('link', d3.forceLink(links).id(d => d.id).distance(80))
541
- .force('charge', d3.forceManyBody().strength(-200))
801
+ .force('charge', d3.forceManyBody().strength(-250))
542
802
  .force('center', d3.forceCenter(width / 2, height / 2))
543
- .force('collision', d3.forceCollide().radius(d => Math.max(d.connections * 3 + 12, 15)));
803
+ .force('x', d3.forceX(width / 2).strength(0.05))
804
+ .force('y', d3.forceY(height / 2).strength(0.05))
805
+ .force('collision', d3.forceCollide().radius(d => Math.max(d.connections * 2 + 16, 20)));
544
806
 
545
- const link = svg.append('g')
807
+ const link = g.append('g')
546
808
  .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)');
809
+ .attr('stroke', '#334155').attr('stroke-width', 1)
810
+ .attr('stroke-opacity', 0.4).attr('marker-end', 'url(#arrowhead)');
549
811
 
550
- const node = svg.append('g')
812
+ const node = g.append('g')
551
813
  .selectAll('g').data(nodes).join('g')
552
814
  .call(d3.drag()
553
815
  .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
@@ -555,21 +817,21 @@ function animateCounter(el, target) {
555
817
  .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
556
818
  );
557
819
 
558
- // Node circles — size based on connections
820
+ // Node circles — color by layer
559
821
  node.append('circle')
560
- .attr('r', d => Math.max(d.connections * 3 + 6, 8))
822
+ .attr('r', d => Math.max(d.connections * 2.5 + 5, 6))
561
823
  .attr('fill', d => layerColors[d.layer] || '#64748b')
562
- .attr('stroke', '#0f172a').attr('stroke-width', 2)
563
- .attr('opacity', 0.85);
824
+ .attr('stroke', '#0f172a').attr('stroke-width', 1.5)
825
+ .attr('opacity', 0.9);
564
826
 
565
- // Node labels
566
- node.append('text')
827
+ // Node labels — only show for nodes with enough connections
828
+ node.filter(d => d.connections >= 2).append('text')
567
829
  .text(d => d.name.replace(/\\.[^.]+$/, ''))
568
- .attr('x', 0).attr('y', d => -(Math.max(d.connections * 3 + 6, 8) + 6))
830
+ .attr('x', 0).attr('y', d => -(Math.max(d.connections * 2.5 + 5, 6) + 4))
569
831
  .attr('text-anchor', 'middle')
570
- .attr('fill', '#94a3b8').attr('font-size', '10px').attr('font-weight', '500');
832
+ .attr('fill', '#e2e8f0').attr('font-size', '9px').attr('font-weight', '500');
571
833
 
572
- // Tooltip on hover
834
+ // Tooltip
573
835
  node.append('title')
574
836
  .text(d => d.id + '\\nConnections: ' + d.connections + '\\nLayer: ' + d.layer);
575
837
 
@@ -579,6 +841,31 @@ function animateCounter(el, target) {
579
841
  .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
580
842
  node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
581
843
  });
844
+
845
+ // Expose search and filter functions
846
+ window.filterGraphNodes = function(query) {
847
+ if (!query) {
848
+ node.attr('opacity', 1);
849
+ link.attr('opacity', 0.4);
850
+ return;
851
+ }
852
+ query = query.toLowerCase();
853
+ node.attr('opacity', d => d.id.toLowerCase().includes(query) || d.name.toLowerCase().includes(query) ? 1 : 0.1);
854
+ link.attr('opacity', d => {
855
+ const srcMatch = d.source.id.toLowerCase().includes(query);
856
+ const tgtMatch = d.target.id.toLowerCase().includes(query);
857
+ return (srcMatch || tgtMatch) ? 0.6 : 0.05;
858
+ });
859
+ };
860
+
861
+ window.toggleGraphLayer = function(layer, visible) {
862
+ node.filter(d => d.layer === layer)
863
+ .transition().duration(300)
864
+ .attr('opacity', visible ? 1 : 0.05);
865
+ link.filter(d => d.source.layer === layer || d.target.layer === layer)
866
+ .transition().duration(300)
867
+ .attr('opacity', visible ? 0.4 : 0.02);
868
+ };
582
869
  })();
583
870
 
584
871
  // ── Bubble Chart ──
@@ -648,6 +935,271 @@ function animateCounter(el, target) {
648
935
  <\/script>`;
649
936
  }
650
937
 
938
+
939
+ private renderAgentSuggestions(s: AgentSuggestion): string {
940
+ const roleIcon = (name: string): string => {
941
+ if (name.includes('ORCHESTRATOR')) return '\u{1F3AD}';
942
+ if (name.includes('BACKEND') || name.includes('FRONTEND') || name.includes('DATABASE') || name.includes('FLUTTER')) return '\u{1F4BB}';
943
+ if (name.includes('SECURITY')) return '\u{1F6E1}\uFE0F';
944
+ if (name.includes('QA')) return '\u{1F9EA}';
945
+ if (name.includes('TECH-DEBT')) return '\u{1F4CA}';
946
+ return '\u{1F916}';
947
+ };
948
+
949
+ const roleLabel = (name: string): string => {
950
+ if (name.includes('ORCHESTRATOR')) return 'coordination';
951
+ if (name.includes('SECURITY')) return 'protection';
952
+ if (name.includes('QA')) return 'quality';
953
+ if (name.includes('TECH-DEBT')) return 'governance';
954
+ return 'development';
955
+ };
956
+
957
+ const roleColor = (name: string): string => {
958
+ if (name.includes('ORCHESTRATOR')) return '#c084fc';
959
+ if (name.includes('SECURITY')) return '#f87171';
960
+ if (name.includes('QA')) return '#34d399';
961
+ if (name.includes('TECH-DEBT')) return '#fbbf24';
962
+ return '#60a5fa';
963
+ };
964
+
965
+ // Status helpers
966
+ const statusBadge = (status: string): string => {
967
+ const map: Record<string, { icon: string; label: string; color: string }> = {
968
+ 'KEEP': { icon: '✅', label: 'KEEP', color: '#22c55e' },
969
+ 'MODIFY': { icon: '🔵', label: 'MODIFY', color: '#3b82f6' },
970
+ 'CREATE': { icon: '🟡', label: 'NEW', color: '#f59e0b' },
971
+ 'DELETE': { icon: '🔴', label: 'REMOVE', color: '#ef4444' },
972
+ };
973
+ const s = map[status] || map['CREATE'];
974
+ return `<span class="agent-status-badge" style="background:${s.color}20;color:${s.color};border:1px solid ${s.color}40">${s.icon} ${s.label}</span>`;
975
+ };
976
+
977
+ const statusBorder = (status: string): string => {
978
+ const map: Record<string, string> = {
979
+ 'KEEP': '#22c55e', 'MODIFY': '#3b82f6', 'CREATE': '#f59e0b', 'DELETE': '#ef4444',
980
+ };
981
+ return map[status] || '#334155';
982
+ };
983
+
984
+ const agentCards = s.suggestedAgents.map(a =>
985
+ `<label class="agent-toggle-card" data-category="agents" data-name="${a.name}">
986
+ <input type="checkbox" class="agent-check" ${a.status !== 'DELETE' ? 'checked' : ''} data-type="agents" data-item="${a.name}">
987
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(a.status)}">
988
+ <div class="agent-toggle-icon">${roleIcon(a.name)}</div>
989
+ <div class="agent-toggle-info">
990
+ <span class="agent-toggle-name">${a.name}</span>
991
+ <span class="agent-toggle-role" style="color:${roleColor(a.name)}">${roleLabel(a.name)}</span>
992
+ ${a.description ? `<span class="agent-toggle-desc">${a.description}</span>` : ''}
993
+ </div>
994
+ ${statusBadge(a.status)}
995
+ <div class="agent-toggle-check">\u2713</div>
996
+ </div>
997
+ </label>`
998
+ ).join('\n');
999
+
1000
+ const miniCard = (item: { name: string; status: string; description?: string }, icon: string, type: string): string =>
1001
+ `<label class="agent-toggle-card mini" data-category="${type}">
1002
+ <input type="checkbox" class="agent-check" ${item.status !== 'DELETE' ? 'checked' : ''} data-type="${type}" data-item="${item.name}">
1003
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(item.status)}">
1004
+ <span class="agent-toggle-icon">${icon}</span>
1005
+ <div class="agent-toggle-info">
1006
+ <span class="agent-toggle-name">${item.name}.md</span>
1007
+ ${item.description ? `<span class="agent-toggle-desc">${item.description}</span>` : ''}
1008
+ </div>
1009
+ ${statusBadge(item.status)}
1010
+ <div class="agent-toggle-check">\u2713</div>
1011
+ </div>
1012
+ </label>`;
1013
+
1014
+ const ruleCards = s.suggestedRules.map(r => miniCard(r, '\u{1F4CF}', 'rules')).join('\n');
1015
+ const guardCards = s.suggestedGuards.map(g => miniCard(g, '\u{1F6E1}\uFE0F', 'guards')).join('\n');
1016
+ const workflowCards = s.suggestedWorkflows.map(w => miniCard(w, '\u26A1', 'workflows')).join('\n');
1017
+
1018
+ const skillCards = s.suggestedSkills.map(sk =>
1019
+ `<label class="agent-toggle-card" data-category="skills">
1020
+ <input type="checkbox" class="agent-check" checked data-type="skills" data-item="${sk.source}">
1021
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(sk.status)}">
1022
+ <span class="agent-toggle-icon">\u{1F9E0}</span>
1023
+ <div class="agent-toggle-info">
1024
+ <span class="agent-toggle-name">${sk.name}</span>
1025
+ <span class="agent-toggle-role" style="color:#34d399">${sk.description}</span>
1026
+ </div>
1027
+ ${statusBadge(sk.status)}
1028
+ <div class="agent-toggle-check">\u2713</div>
1029
+ </div>
1030
+ </label>`
1031
+ ).join('\n');
1032
+
1033
+ const auditSection = s.audit.filter(f => f.type !== 'OK').length > 0 ? `
1034
+ <div class="agent-audit-section">
1035
+ <h3 class="agent-section-subtitle">\u{1F50D} Audit Findings</h3>
1036
+ <div class="agent-audit-grid">
1037
+ ${s.audit.filter(f => f.type !== 'OK').map(f => {
1038
+ const icon = f.type === 'MISSING' ? '\u274C' : f.type === 'IMPROVEMENT' ? '\u{1F4A1}' : '\u26A0\uFE0F';
1039
+ const cls = f.type === 'MISSING' ? 'audit-missing' : 'audit-improvement';
1040
+ return `<div class="agent-audit-item ${cls}">
1041
+ <span class="audit-icon">${icon}</span>
1042
+ <div class="audit-content">
1043
+ <span class="audit-desc">${f.description}</span>
1044
+ ${f.suggestion ? `<span class="audit-suggestion">\u2192 ${f.suggestion}</span>` : ''}
1045
+ </div>
1046
+ </div>`;
1047
+ }).join('\n')}
1048
+ </div>
1049
+ </div>` : '';
1050
+
1051
+ const stackPills = [
1052
+ `\u{1F527} ${s.stack.primary}`,
1053
+ `\u{1F4E6} ${s.stack.frameworks.length > 0 ? s.stack.frameworks.join(', ') : 'No framework'}`,
1054
+ s.hasExistingAgents ? '\u{1F4C1} Existing .agent/' : '\u{1F4C1} New .agent/',
1055
+ ...(s.stack.hasBackend ? ['\u{1F519} Backend'] : []),
1056
+ ...(s.stack.hasFrontend ? ['\u{1F5A5}\uFE0F Frontend'] : []),
1057
+ ...(s.stack.hasMobile ? ['\u{1F4F1} Mobile'] : []),
1058
+ ...(s.stack.hasDatabase ? ['\u{1F5C4}\uFE0F Database'] : []),
1059
+ ];
1060
+
1061
+ const totalItems = s.suggestedAgents.length + s.suggestedRules.length + s.suggestedGuards.length + s.suggestedWorkflows.length + s.suggestedSkills.length;
1062
+
1063
+ // Status summary counts
1064
+ const allItems = [...s.suggestedAgents, ...s.suggestedRules, ...s.suggestedGuards, ...s.suggestedWorkflows];
1065
+ const keepCount = allItems.filter(i => i.status === 'KEEP').length;
1066
+ const modifyCount = allItems.filter(i => i.status === 'MODIFY').length;
1067
+ const createCount = allItems.filter(i => i.status === 'CREATE').length;
1068
+
1069
+ return `
1070
+ <h2 class="section-title">\u{1F916} Agent System</h2>
1071
+
1072
+ <div class="card agent-system-card">
1073
+ <div class="agent-stack-banner">
1074
+ ${stackPills.map(p => `<div class="stack-pill">${p}</div>`).join('\n ')}
1075
+ </div>
1076
+
1077
+ <div class="agent-status-legend">
1078
+ <span class="status-legend-item"><span class="legend-dot" style="background:#22c55e"></span> KEEP (${keepCount})</span>
1079
+ <span class="status-legend-item"><span class="legend-dot" style="background:#3b82f6"></span> MODIFY (${modifyCount})</span>
1080
+ <span class="status-legend-item"><span class="legend-dot" style="background:#f59e0b"></span> NEW (${createCount})</span>
1081
+ </div>
1082
+
1083
+ <div class="agent-controls">
1084
+ <button class="agent-ctrl-btn" onclick="toggleAll(true)">\u2705 Select All</button>
1085
+ <button class="agent-ctrl-btn" onclick="toggleAll(false)">\u2B1C Select None</button>
1086
+ <span class="agent-count-label"><span id="agentSelectedCount">${totalItems}</span> selected</span>
1087
+ </div>
1088
+
1089
+ <h3 class="agent-section-subtitle">\u{1F916} Agents</h3>
1090
+ <div class="agent-toggle-grid">
1091
+ ${agentCards}
1092
+ </div>
1093
+
1094
+ <div class="agent-extras-grid">
1095
+ <div>
1096
+ <h3 class="agent-section-subtitle">\u{1F4CF} Rules</h3>
1097
+ <div class="agent-toggle-list">${ruleCards}</div>
1098
+ </div>
1099
+ <div>
1100
+ <h3 class="agent-section-subtitle">\u{1F6E1}\uFE0F Guards</h3>
1101
+ <div class="agent-toggle-list">${guardCards}</div>
1102
+ </div>
1103
+ <div>
1104
+ <h3 class="agent-section-subtitle">\u26A1 Workflows</h3>
1105
+ <div class="agent-toggle-list">${workflowCards}</div>
1106
+ </div>
1107
+ </div>
1108
+
1109
+ <h3 class="agent-section-subtitle">\u{1F9E0} Skills <span style="font-size:0.7rem;color:#94a3b8;font-weight:400">from skills.sh</span></h3>
1110
+ <div class="agent-toggle-grid">
1111
+ ${skillCards}
1112
+ </div>
1113
+
1114
+ ${auditSection}
1115
+
1116
+ <div class="agent-command-box">
1117
+ <div class="agent-command-header">
1118
+ <span>\u{1F4A1} Command to generate selected items:</span>
1119
+ <button class="agent-copy-btn" onclick="copyAgentCommand()">
1120
+ <span id="copyIcon">\u{1F4CB}</span> Copy
1121
+ </button>
1122
+ </div>
1123
+ <code id="agentCommandOutput" class="agent-command-code">${s.command}</code>
1124
+ </div>
1125
+ </div>
1126
+
1127
+ <style>
1128
+ .agent-system-card { padding: 1.5rem; }
1129
+ .agent-stack-banner { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
1130
+ .stack-pill { background: #1e293b; border: 1px solid #334155; border-radius: 99px; padding: 0.4rem 1rem; font-size: 0.8rem; color: #94a3b8; white-space: nowrap; }
1131
+ .agent-status-legend { display: flex; gap: 1.5rem; margin-bottom: 1rem; padding: 0.5rem 0; border-bottom: 1px solid #1e293b; }
1132
+ .status-legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: #94a3b8; }
1133
+ .agent-status-badge { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.15rem 0.5rem; border-radius: 99px; font-size: 0.65rem; font-weight: 700; flex-shrink: 0; letter-spacing: 0.03em; }
1134
+ .agent-toggle-desc { display: block; font-size: 0.65rem; color: #64748b; margin-top: 0.15rem; line-height: 1.3; }
1135
+ .agent-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
1136
+ .agent-ctrl-btn { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.4rem 1rem; border-radius: 8px; font-size: 0.8rem; cursor: pointer; transition: all 0.2s; }
1137
+ .agent-ctrl-btn:hover { background: #334155; }
1138
+ .agent-count-label { color: #94a3b8; font-size: 0.85rem; margin-left: auto; }
1139
+ #agentSelectedCount { color: #c084fc; font-weight: 700; }
1140
+ .agent-section-subtitle { color: #e2e8f0; font-size: 1.05rem; font-weight: 700; margin: 1.25rem 0 0.75rem; }
1141
+ .agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; }
1142
+ .agent-toggle-card { cursor: pointer; transition: all 0.3s; }
1143
+ .agent-toggle-card input { display: none; }
1144
+ .agent-toggle-inner { display: flex; align-items: center; gap: 0.75rem; background: #1e293b; border: 2px solid #334155; border-radius: 12px; padding: 0.75rem 1rem; transition: all 0.3s; }
1145
+ .agent-toggle-card input:checked + .agent-toggle-inner { background: #1e1b4b; }
1146
+ .agent-toggle-icon { font-size: 1.3rem; flex-shrink: 0; }
1147
+ .agent-toggle-info { flex: 1; min-width: 0; }
1148
+ .agent-toggle-name { display: block; color: #e2e8f0; font-weight: 600; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1149
+ .agent-toggle-role { display: block; font-size: 0.7rem; margin-top: 0.15rem; }
1150
+ .agent-toggle-check { color: #334155; font-size: 1rem; flex-shrink: 0; transition: color 0.3s; }
1151
+ .agent-toggle-card input:checked + .agent-toggle-inner .agent-toggle-check { color: #818cf8; }
1152
+ .agent-toggle-card.mini .agent-toggle-inner { padding: 0.5rem 0.75rem; border-radius: 8px; }
1153
+ .agent-extras-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 0.5rem; }
1154
+ @media (max-width: 768px) { .agent-extras-grid { grid-template-columns: 1fr; } }
1155
+ .agent-toggle-list { display: flex; flex-direction: column; gap: 0.5rem; }
1156
+ .agent-audit-section { margin-top: 1.5rem; }
1157
+ .agent-audit-grid { display: flex; flex-direction: column; gap: 0.5rem; }
1158
+ .agent-audit-item { display: flex; gap: 0.75rem; align-items: flex-start; background: #1e293b; padding: 0.75rem 1rem; border-radius: 8px; }
1159
+ .agent-audit-item.audit-missing { border-left: 3px solid #ef4444; }
1160
+ .agent-audit-item.audit-improvement { border-left: 3px solid #fbbf24; }
1161
+ .audit-icon { font-size: 1rem; flex-shrink: 0; margin-top: 2px; }
1162
+ .audit-content { display: flex; flex-direction: column; gap: 0.25rem; }
1163
+ .audit-desc { color: #e2e8f0; font-size: 0.85rem; }
1164
+ .audit-suggestion { color: #94a3b8; font-size: 0.8rem; font-style: italic; }
1165
+ .agent-command-box { margin-top: 1.5rem; background: #0f172a; border-radius: 12px; border: 1px solid #334155; overflow: hidden; }
1166
+ .agent-command-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: #1e293b; font-size: 0.8rem; color: #94a3b8; }
1167
+ .agent-copy-btn { background: #c084fc; color: #0f172a; border: none; border-radius: 6px; padding: 0.4rem 0.8rem; cursor: pointer; font-size: 0.75rem; font-weight: 600; transition: all 0.2s; }
1168
+ .agent-copy-btn:hover { background: #a855f7; transform: scale(1.05); }
1169
+ .agent-command-code { display: block; padding: 1rem; color: #c084fc; font-size: 0.85rem; word-break: break-all; font-family: 'Fira Code', monospace; }
1170
+ </style>
1171
+
1172
+ <script>
1173
+ (function() {
1174
+ var basePath = ${JSON.stringify(s.command.replace('architect agents ', ''))};
1175
+ var totalItems = ${totalItems};
1176
+ function updateCommand() {
1177
+ var checks = document.querySelectorAll('.agent-check');
1178
+ var selected = { agents: [], rules: [], guards: [], workflows: [], skills: [] };
1179
+ var count = 0;
1180
+ checks.forEach(function(cb) { if (cb.checked) { selected[cb.dataset.type].push(cb.dataset.item); count++; } });
1181
+ document.getElementById('agentSelectedCount').textContent = count;
1182
+ var cmd;
1183
+ if (count === totalItems) { cmd = 'architect agents ' + basePath; }
1184
+ else if (count === 0) { cmd = '# No items selected'; }
1185
+ else {
1186
+ var parts = ['architect agents ' + basePath];
1187
+ if (selected.agents.length > 0) parts.push('--agents ' + selected.agents.join(','));
1188
+ if (selected.rules.length > 0) parts.push('--rules ' + selected.rules.join(','));
1189
+ if (selected.guards.length > 0) parts.push('--guards ' + selected.guards.join(','));
1190
+ if (selected.workflows.length > 0) parts.push('--workflows ' + selected.workflows.join(','));
1191
+ if (selected.skills.length > 0) parts.push('&& ' + selected.skills.map(function(sk){ return 'npx skills add ' + sk; }).join(' && '));
1192
+ cmd = parts.join(' ');
1193
+ }
1194
+ document.getElementById('agentCommandOutput').textContent = cmd;
1195
+ }
1196
+ document.querySelectorAll('.agent-check').forEach(function(cb) { cb.addEventListener('change', updateCommand); });
1197
+ window.toggleAll = function(state) { document.querySelectorAll('.agent-check').forEach(function(cb) { cb.checked = state; }); updateCommand(); };
1198
+ window.copyAgentCommand = function() { var cmd = document.getElementById('agentCommandOutput').textContent; navigator.clipboard.writeText(cmd).then(function() { var btn = document.getElementById('copyIcon'); btn.textContent = '\u2705'; setTimeout(function() { btn.textContent = '\ud83d\udccb'; }, 2000); }); };
1199
+ })();
1200
+ <\/script>`;
1201
+ }
1202
+
651
1203
  private getStyles(): string {
652
1204
  return `<style>
653
1205
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@@ -662,7 +1214,50 @@ function animateCounter(el, target) {
662
1214
  min-height: 100vh;
663
1215
  }
664
1216
 
665
- .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
1217
+ html { scroll-behavior: smooth; }
1218
+
1219
+ /* ── Layout ── */
1220
+ .report-layout { display: flex; min-height: 100vh; }
1221
+
1222
+ .sidebar {
1223
+ position: sticky; top: 0; height: 100vh; width: 220px; min-width: 220px;
1224
+ background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
1225
+ border-right: 1px solid #334155; padding: 1.5rem 0;
1226
+ display: flex; flex-direction: column; gap: 0.25rem;
1227
+ overflow-y: auto; z-index: 100;
1228
+ }
1229
+ .sidebar-title {
1230
+ font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
1231
+ letter-spacing: 0.15em; color: #475569; padding: 0 1.25rem; margin-bottom: 0.75rem;
1232
+ }
1233
+ .sidebar-link {
1234
+ display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1.25rem;
1235
+ color: #94a3b8; text-decoration: none; font-size: 0.8rem; font-weight: 500;
1236
+ border-left: 3px solid transparent; transition: all 0.2s;
1237
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1238
+ }
1239
+ .sidebar-link:hover { color: #e2e8f0; background: #1e293b; border-left-color: #475569; }
1240
+ .sidebar-link.active { color: #c084fc; background: #c084fc10; border-left-color: #c084fc; font-weight: 700; }
1241
+
1242
+ .sidebar-toggle {
1243
+ display: none; position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 200;
1244
+ width: 48px; height: 48px; border-radius: 50%; border: none;
1245
+ background: #c084fc; color: #0f172a; font-size: 1.2rem; cursor: pointer;
1246
+ box-shadow: 0 4px 16px rgba(192,132,252,0.4); transition: all 0.2s;
1247
+ }
1248
+ .sidebar-toggle:hover { transform: scale(1.1); }
1249
+
1250
+ @media (max-width: 1024px) {
1251
+ .sidebar {
1252
+ position: fixed; left: -240px; top: 0; width: 240px; min-width: 240px;
1253
+ transition: left 0.3s ease; box-shadow: none;
1254
+ }
1255
+ .sidebar.sidebar-open { left: 0; box-shadow: 4px 0 24px rgba(0,0,0,0.5); }
1256
+ .sidebar-toggle { display: flex; align-items: center; justify-content: center; }
1257
+ .report-layout { flex-direction: column; }
1258
+ }
1259
+
1260
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; flex: 1; min-width: 0; }
666
1261
 
667
1262
  /* ── Header ── */
668
1263
  .header {
@@ -741,6 +1336,44 @@ function animateCounter(el, target) {
741
1336
  display: flex; align-items: center; gap: 0.5rem;
742
1337
  }
743
1338
 
1339
+ /* ── Section Accordion ── */
1340
+ .section-accordion {
1341
+ margin: 1.5rem 0; border: 1px solid #334155; border-radius: 16px;
1342
+ background: transparent; overflow: hidden;
1343
+ }
1344
+ .section-accordion-header {
1345
+ cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.75rem;
1346
+ font-size: 1.3rem; font-weight: 700; color: #e2e8f0;
1347
+ padding: 1.25rem 1.5rem; background: linear-gradient(135deg, #1e293b, #0f172a);
1348
+ border-bottom: 1px solid transparent; transition: all 0.3s; user-select: none;
1349
+ }
1350
+ .section-accordion-header:hover { background: linear-gradient(135deg, #334155, #1e293b); }
1351
+ .section-accordion[open] > .section-accordion-header { border-bottom-color: #334155; }
1352
+ .section-accordion-header::after {
1353
+ content: '\\25B6'; margin-left: auto; font-size: 0.8rem; color: #818cf8;
1354
+ transition: transform 0.3s;
1355
+ }
1356
+ .section-accordion[open] > .section-accordion-header::after { transform: rotate(90deg); }
1357
+ .section-accordion-header::-webkit-details-marker { display: none; }
1358
+ .section-accordion-body { padding: 0.5rem 0; }
1359
+
1360
+ /* ── Operations Accordion (inside refactoring steps) ── */
1361
+ .rstep-ops-accordion {
1362
+ margin: 0.75rem 0; border: 1px solid #1e293b; border-radius: 10px; overflow: hidden;
1363
+ }
1364
+ .rstep-ops-toggle {
1365
+ cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.5rem;
1366
+ font-size: 0.9rem; font-weight: 600; color: #94a3b8;
1367
+ padding: 0.75rem 1rem; background: #0f172a; transition: all 0.2s;
1368
+ }
1369
+ .rstep-ops-toggle:hover { background: #1e293b; color: #e2e8f0; }
1370
+ .rstep-ops-toggle::after {
1371
+ content: '\\25B6'; margin-left: auto; font-size: 0.65rem; color: #818cf8;
1372
+ transition: transform 0.3s;
1373
+ }
1374
+ .rstep-ops-accordion[open] > .rstep-ops-toggle::after { transform: rotate(90deg); }
1375
+ .rstep-ops-toggle::-webkit-details-marker { display: none; }
1376
+
744
1377
  /* ── Cards ── */
745
1378
  .card {
746
1379
  background: #1e293b; border-radius: 16px; border: 1px solid #334155;
@@ -750,17 +1383,42 @@ function animateCounter(el, target) {
750
1383
 
751
1384
  /* ── Graph ── */
752
1385
  .graph-card { padding: 1rem; }
1386
+ .graph-controls { margin-bottom: 0.75rem; }
753
1387
  .graph-legend {
754
1388
  display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
755
1389
  justify-content: center;
756
1390
  }
757
1391
  .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; }
1392
+ .legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
1393
+ .graph-filters {
1394
+ display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;
1395
+ justify-content: center; margin-top: 0.5rem;
1396
+ }
1397
+ .graph-search {
1398
+ background: #0f172a; border: 1px solid #334155; border-radius: 8px;
1399
+ padding: 0.4rem 0.75rem; color: #e2e8f0; font-size: 0.8rem;
1400
+ outline: none; width: 180px; transition: border-color 0.2s;
1401
+ }
1402
+ .graph-search:focus { border-color: #818cf8; }
1403
+ .graph-layer-filters {
1404
+ display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;
1405
+ }
1406
+ .graph-filter-check {
1407
+ display: flex; align-items: center; gap: 4px;
1408
+ font-size: 0.75rem; color: #94a3b8; cursor: pointer;
1409
+ }
1410
+ .graph-filter-check input { width: 14px; height: 14px; accent-color: #818cf8; }
1411
+ .graph-limit-notice {
1412
+ text-align: center; font-size: 0.75rem; color: #f59e0b;
1413
+ background: #f59e0b15; padding: 0.3rem 0.75rem; border-radius: 6px;
1414
+ margin-top: 0.5rem;
1415
+ }
759
1416
  .graph-hint {
760
1417
  text-align: center; font-size: 0.75rem; color: #475569; margin-top: 0.5rem;
761
1418
  font-style: italic;
762
1419
  }
763
- #dep-graph svg { background: rgba(0,0,0,0.2); border-radius: 12px; }
1420
+ #dep-graph svg { background: rgba(0,0,0,0.2); border-radius: 12px; cursor: grab; }
1421
+ #dep-graph svg:active { cursor: grabbing; }
764
1422
 
765
1423
  /* ── Layers Grid ── */
766
1424
  .layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
@@ -810,12 +1468,102 @@ function animateCounter(el, target) {
810
1468
  .footer a { color: #818cf8; text-decoration: none; }
811
1469
  .footer a:hover { text-decoration: underline; }
812
1470
 
1471
+ /* ── Refactoring Plan ── */
1472
+ .refactor-score { padding: 2rem; }
1473
+ .refactor-score-pair {
1474
+ display: flex; align-items: center; justify-content: center; gap: 1.5rem;
1475
+ margin-bottom: 2rem; flex-wrap: wrap;
1476
+ }
1477
+ .rscore-box { text-align: center; }
1478
+ .rscore-num { font-size: 3rem; font-weight: 900; line-height: 1; }
1479
+ .rscore-label { font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
1480
+ .rscore-improvement { font-size: 1.3rem; font-weight: 700; }
1481
+
1482
+ .refactor-legend { display: flex; gap: 1rem; margin-bottom: 0.5rem; }
1483
+ .rlegend-tag { font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 6px; }
1484
+ .rlegend-tag.rbefore { background: rgba(255,255,255,0.05); color: #94a3b8; }
1485
+ .rlegend-tag.rafter { background: rgba(129,140,248,0.2); color: #818cf8; }
1486
+
1487
+ .refactor-metric-name { width: 100px; font-size: 0.8rem; text-transform: uppercase; color: #94a3b8; font-weight: 600; }
1488
+ .refactor-metric-bars { flex: 1; position: relative; height: 30px; }
1489
+ .rbar-before, .rbar-after {
1490
+ position: absolute; left: 0; height: 14px; border-radius: 4px;
1491
+ display: flex; align-items: center; padding-left: 6px;
1492
+ font-size: 0.7rem; font-weight: 600;
1493
+ }
1494
+ .rbar-before { top: 0; }
1495
+ .rbar-after { top: 15px; }
1496
+ .refactor-metric-diff { width: 50px; text-align: right; font-weight: 700; font-size: 0.85rem; }
1497
+
1498
+ .refactor-stats-row {
1499
+ display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;
1500
+ }
1501
+ .rstat {
1502
+ background: #1e293b; border: 1px solid #334155; border-radius: 99px;
1503
+ padding: 0.4rem 1rem; font-size: 0.85rem; color: #94a3b8; font-weight: 500;
1504
+ }
1505
+
1506
+ .priority-bar {
1507
+ display: flex; border-radius: 12px; overflow: hidden; height: 32px; margin-bottom: 2rem;
1508
+ }
1509
+ .prio-seg {
1510
+ display: flex; align-items: center; justify-content: center;
1511
+ font-size: 0.75rem; font-weight: 600;
1512
+ }
1513
+ .prio-critical { background: #ef444430; color: #ef4444; }
1514
+ .prio-high { background: #f59e0b30; color: #f59e0b; }
1515
+ .prio-medium { background: #3b82f630; color: #60a5fa; }
1516
+ .prio-low { background: #22c55e30; color: #22c55e; }
1517
+
1518
+ .refactor-roadmap { display: flex; flex-direction: column; gap: 1rem; }
1519
+ .rstep-card {
1520
+ background: #1e293b; border-radius: 16px; border: 1px solid #334155;
1521
+ padding: 1.5rem; transition: border-color 0.2s;
1522
+ }
1523
+ .rstep-card:hover { border-color: #818cf8; }
1524
+ .rstep-header { display: flex; gap: 1rem; margin-bottom: 1rem; }
1525
+ .rstep-number {
1526
+ width: 40px; height: 40px; border-radius: 50%;
1527
+ background: linear-gradient(135deg, #818cf8, #c084fc);
1528
+ display: flex; align-items: center; justify-content: center;
1529
+ font-weight: 800; font-size: 1rem; color: white; flex-shrink: 0;
1530
+ }
1531
+ .rstep-info { flex: 1; }
1532
+ .rstep-title-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
1533
+ .rstep-title-row h3 { font-size: 1.1rem; font-weight: 700; }
1534
+ .rstep-desc { color: #94a3b8; font-size: 0.9rem; margin-top: 0.3rem; }
1535
+ .tier-badge {
1536
+ background: #818cf815; color: #818cf8; border: 1px solid #818cf830;
1537
+ padding: 0.15rem 0.5rem; border-radius: 99px; font-size: 0.65rem; font-weight: 600;
1538
+ }
1539
+ .rstep-details { margin-top: 0.5rem; }
1540
+ .rstep-details summary { cursor: pointer; color: #818cf8; font-size: 0.85rem; font-weight: 500; }
1541
+ .rstep-rationale { color: #64748b; font-size: 0.85rem; margin-top: 0.3rem; font-style: italic; }
1542
+
1543
+ .rstep-ops { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
1544
+ .rstep-ops h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
1545
+ .rop { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
1546
+ .rop-icon { font-size: 0.9rem; }
1547
+ .rop-badge { padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.65rem; font-weight: 700; }
1548
+ .rop-path { background: #0f172a; padding: 1px 6px; border-radius: 4px; font-size: 0.8rem; color: #c084fc; }
1549
+ .rop-arrow { color: #818cf8; font-weight: 700; }
1550
+ .rop-desc { width: 100%; color: #64748b; font-size: 0.8rem; padding-left: 1.8rem; }
1551
+
1552
+ .rstep-impact { margin-top: 0.5rem; }
1553
+ .rstep-impact h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.3rem; }
1554
+ .rimpact-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
1555
+ .rimpact-tag {
1556
+ background: #22c55e10; color: #22c55e; border: 1px solid #22c55e30;
1557
+ padding: 0.2rem 0.6rem; border-radius: 8px; font-size: 0.75rem; font-weight: 500;
1558
+ }
1559
+
813
1560
  /* ── Responsive ── */
814
1561
  @media (max-width: 768px) {
815
1562
  .score-hero { flex-direction: column; gap: 1.5rem; }
816
1563
  .score-breakdown { grid-template-columns: 1fr; }
817
1564
  .header h1 { font-size: 1.8rem; }
818
1565
  .container { padding: 1rem; }
1566
+ .refactor-score-pair { flex-direction: column; }
819
1567
  }
820
1568
 
821
1569
  /* ── Print ── */