@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
@@ -3,7 +3,7 @@
3
3
  * Features: D3.js force graph, bubble charts, radar chart, animated counters.
4
4
  */
5
5
  export class HtmlReportGenerator {
6
- generateHtml(report) {
6
+ generateHtml(report, plan, agentSuggestion) {
7
7
  const grouped = this.groupAntiPatterns(report.antiPatterns);
8
8
  const sugGrouped = this.groupSuggestions(report.suggestions);
9
9
  return `<!DOCTYPE html>
@@ -17,15 +17,62 @@ ${this.getStyles()}
17
17
  </head>
18
18
  <body>
19
19
  ${this.renderHeader(report)}
20
- <div class="container">
21
- ${this.renderScoreHero(report)}
22
- ${this.renderRadarChart(report)}
23
- ${this.renderStats(report)}
24
- ${this.renderLayers(report)}
25
- ${this.renderDependencyGraph(report)}
26
- ${this.renderAntiPatternBubbles(report, grouped)}
27
- ${this.renderAntiPatterns(report, grouped)}
28
- ${this.renderSuggestions(sugGrouped)}
20
+ <div class="report-layout">
21
+ <nav class="sidebar" id="reportSidebar">
22
+ <div class="sidebar-title">Navigation</div>
23
+ <a href="#score" class="sidebar-link active" data-section="score">📊 Score</a>
24
+ <a href="#layers" class="sidebar-link" data-section="layers">📐 Layers & Graph</a>
25
+ <a href="#anti-patterns" class="sidebar-link" data-section="anti-patterns">⚠️ Anti-Patterns (${report.antiPatterns.length})</a>
26
+ <a href="#suggestions" class="sidebar-link" data-section="suggestions">💡 Suggestions (${report.suggestions.length})</a>
27
+ ${plan ? `<a href="#refactoring" class="sidebar-link" data-section="refactoring">🔧 Refactoring (${plan.steps.length})</a>` : ''}
28
+ ${agentSuggestion ? `<a href="#agents" class="sidebar-link" data-section="agents">🤖 Agents</a>` : ''}
29
+ </nav>
30
+ <button class="sidebar-toggle" onclick="document.getElementById('reportSidebar').classList.toggle('sidebar-open')">☰</button>
31
+
32
+ <div class="container">
33
+ <div id="score">
34
+ ${this.renderScoreHero(report)}
35
+ ${this.renderRadarChart(report)}
36
+ ${this.renderStats(report)}
37
+ </div>
38
+
39
+ <details class="section-accordion" id="layers" open>
40
+ <summary class="section-accordion-header">📐 Layer Analysis & Dependencies</summary>
41
+ <div class="section-accordion-body">
42
+ ${this.renderLayers(report)}
43
+ ${this.renderDependencyGraph(report)}
44
+ </div>
45
+ </details>
46
+
47
+ <details class="section-accordion" id="anti-patterns" open>
48
+ <summary class="section-accordion-header">⚠️ Anti-Patterns (${report.antiPatterns.length})</summary>
49
+ <div class="section-accordion-body">
50
+ ${this.renderAntiPatternBubbles(report, grouped)}
51
+ ${this.renderAntiPatterns(report, grouped)}
52
+ </div>
53
+ </details>
54
+
55
+ <details class="section-accordion" id="suggestions">
56
+ <summary class="section-accordion-header">💡 Suggestions (${report.suggestions.length})</summary>
57
+ <div class="section-accordion-body">
58
+ ${this.renderSuggestions(sugGrouped)}
59
+ </div>
60
+ </details>
61
+
62
+ ${plan ? `<details class="section-accordion" id="refactoring" open>
63
+ <summary class="section-accordion-header">🔧 Refactoring Plan (${plan.steps.length} steps, ${plan.totalOperations} operations)</summary>
64
+ <div class="section-accordion-body">
65
+ ${this.renderRefactoringPlan(plan)}
66
+ </div>
67
+ </details>` : ''}
68
+
69
+ ${agentSuggestion ? `<details class="section-accordion" id="agents" open>
70
+ <summary class="section-accordion-header">🤖 Agent System</summary>
71
+ <div class="section-accordion-body">
72
+ ${this.renderAgentSuggestions(agentSuggestion)}
73
+ </div>
74
+ </details>` : ''}
75
+ </div>
29
76
  </div>
30
77
  ${this.renderFooter()}
31
78
  ${this.getScripts(report)}
@@ -206,44 +253,70 @@ ${this.getScripts(report)}
206
253
  renderDependencyGraph(report) {
207
254
  if (report.dependencyGraph.edges.length === 0)
208
255
  return '';
209
- // Build node data with connection counts
256
+ // Build real file set only files that appear as SOURCE in edges (these are real scanned files)
257
+ const realFiles = new Set(report.dependencyGraph.edges.map(e => e.from));
258
+ // Count connections only for real files
210
259
  const connectionCount = {};
211
260
  for (const edge of report.dependencyGraph.edges) {
212
- connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
213
- connectionCount[edge.to] = (connectionCount[edge.to] || 0) + 1;
261
+ if (realFiles.has(edge.from)) {
262
+ connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
263
+ }
264
+ if (realFiles.has(edge.to)) {
265
+ connectionCount[edge.to] = (connectionCount[edge.to] || 0) + 1;
266
+ }
214
267
  }
268
+ // Build layer map from report layers
215
269
  const layerMap = {};
216
270
  for (const layer of report.layers) {
217
271
  for (const file of layer.files) {
218
272
  layerMap[file] = layer.name;
219
273
  }
220
274
  }
221
- const nodes = report.dependencyGraph.nodes.map(n => ({
275
+ // Create nodes only from real files
276
+ const allNodes = [...realFiles].map(n => ({
222
277
  id: n,
223
278
  name: n.split('/').pop() || n,
224
279
  connections: connectionCount[n] || 0,
225
280
  layer: layerMap[n] || 'Other',
226
281
  }));
227
- const links = report.dependencyGraph.edges.map(e => ({
228
- source: e.from,
229
- target: e.to,
230
- }));
282
+ // Build links only between real files
283
+ const allLinks = report.dependencyGraph.edges
284
+ .filter(e => realFiles.has(e.from) && realFiles.has(e.to))
285
+ .map(e => ({ source: e.from, target: e.to }));
286
+ // Limit to top N most-connected nodes for large projects
287
+ const maxNodes = 60;
288
+ const sortedNodes = [...allNodes].sort((a, b) => b.connections - a.connections);
289
+ const limitedNodes = sortedNodes.slice(0, maxNodes);
290
+ const limitedNodeIds = new Set(limitedNodes.map(n => n.id));
291
+ const limitedLinks = allLinks.filter(l => limitedNodeIds.has(l.source) && limitedNodeIds.has(l.target));
292
+ const isLimited = allNodes.length > maxNodes;
293
+ // Collect unique layers from limited nodes
294
+ const uniqueLayers = [...new Set(limitedNodes.map(n => n.layer))];
231
295
  return `
232
296
  <h2 class="section-title">🔗 Dependency Graph</h2>
233
297
  <div class="card graph-card">
234
- <div class="graph-legend">
235
- <span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
236
- <span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
237
- <span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
238
- <span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
239
- <span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
240
- <span class="legend-item"><span class="legend-dot" style="background: #64748b"></span> Other</span>
298
+ <div class="graph-controls">
299
+ <div class="graph-legend">
300
+ <span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
301
+ <span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
302
+ <span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
303
+ <span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
304
+ <span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
305
+ <span class="legend-item"><span class="legend-dot" style="background: #64748b"></span> Other</span>
306
+ </div>
307
+ <div class="graph-filters">
308
+ <input type="text" id="graphSearch" class="graph-search" placeholder="🔍 Search node..." oninput="filterGraphNodes(this.value)">
309
+ <div class="graph-layer-filters">
310
+ ${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' }[l] || '#64748b'}"></span> ${l}</label>`).join('')}
311
+ </div>
312
+ </div>
313
+ ${isLimited ? `<div class="graph-limit-notice">Showing top ${maxNodes} of ${allNodes.length} source files (most connected) · ${limitedLinks.length} links</div>` : ''}
241
314
  </div>
242
- <div id="dep-graph" style="width:100%; min-height:400px;"></div>
243
- <div class="graph-hint">🖱️ Drag nodes to explore • Node size = number of connections</div>
315
+ <div id="dep-graph" style="width:100%; min-height:500px;"></div>
316
+ <div class="graph-hint">🖱️ Drag nodes • Scroll to zoomDouble-click to reset • Node size = connections</div>
244
317
  </div>
245
- <script type="application/json" id="graph-nodes">${JSON.stringify(nodes)}<\/script>
246
- <script type="application/json" id="graph-links">${JSON.stringify(links)}<\/script>`;
318
+ <script type="application/json" id="graph-nodes">${JSON.stringify(limitedNodes)}<\\/script>
319
+ <script type="application/json" id="graph-links">${JSON.stringify(limitedLinks)}<\\/script>`;
247
320
  }
248
321
  /**
249
322
  * Bubble chart for anti-patterns — bigger = more severe
@@ -345,8 +418,145 @@ ${this.getScripts(report)}
345
418
  renderFooter() {
346
419
  return `
347
420
  <div class="footer">
348
- <p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect</a> — AI-powered architecture analysis</p>
421
+ <p>Generated by <a href="https://github.com/camilooscargbaptista/architect">🏗️ Architect v2.0</a> — AI-powered architecture analysis + refactoring engine</p>
349
422
  <p>By <strong>Camilo Girardelli</strong> · <a href="https://www.girardellitecnologia.com">Girardelli Tecnologia</a></p>
423
+ </div>`;
424
+ }
425
+ // ── Refactoring Plan Section ──
426
+ opColor(type) {
427
+ switch (type) {
428
+ case 'CREATE': return '#22c55e';
429
+ case 'MOVE': return '#3b82f6';
430
+ case 'MODIFY': return '#f59e0b';
431
+ case 'DELETE': return '#ef4444';
432
+ default: return '#64748b';
433
+ }
434
+ }
435
+ opIcon(type) {
436
+ switch (type) {
437
+ case 'CREATE': return '➕';
438
+ case 'MOVE': return '📦';
439
+ case 'MODIFY': return '✏️';
440
+ case 'DELETE': return '🗑️';
441
+ default: return '📄';
442
+ }
443
+ }
444
+ renderRefactoringPlan(plan) {
445
+ if (plan.steps.length === 0) {
446
+ return `
447
+ <h2 class="section-title">✅ Refactoring Plan</h2>
448
+ <div class="card success-card">
449
+ <p>No refactoring needed! Your architecture is already in great shape.</p>
450
+ </div>`;
451
+ }
452
+ const improvement = plan.estimatedScoreAfter.overall - plan.currentScore.overall;
453
+ const metrics = Object.keys(plan.currentScore.breakdown);
454
+ const bars = metrics.map(metric => {
455
+ const before = plan.currentScore.breakdown[metric];
456
+ const after = plan.estimatedScoreAfter.breakdown[metric] ?? before;
457
+ const diff = after - before;
458
+ return `
459
+ <div class="comparison-row">
460
+ <div class="refactor-metric-name">${metric}</div>
461
+ <div class="refactor-metric-bars">
462
+ <div class="rbar-before" style="width: ${before}%; background: ${this.scoreColor(before)}40"><span>${before}</span></div>
463
+ <div class="rbar-after" style="width: ${after}%; background: ${this.scoreColor(after)}"><span>${after}</span></div>
464
+ </div>
465
+ <div class="refactor-metric-diff" style="color: ${diff > 0 ? '#22c55e' : '#64748b'}">
466
+ ${diff > 0 ? `+${diff}` : diff === 0 ? '—' : String(diff)}
467
+ </div>
468
+ </div>`;
469
+ }).join('');
470
+ const stepsHtml = plan.steps.map(step => this.renderRefactorStep(step)).join('');
471
+ const criticalCount = plan.steps.filter(s => s.priority === 'CRITICAL').length;
472
+ const highCount = plan.steps.filter(s => s.priority === 'HIGH').length;
473
+ const mediumCount = plan.steps.filter(s => s.priority === 'MEDIUM').length;
474
+ const lowCount = plan.steps.filter(s => s.priority === 'LOW').length;
475
+ return `
476
+ <h2 class="section-title">🔧 Refactoring Plan</h2>
477
+
478
+ <div class="card refactor-score">
479
+ <div class="refactor-score-pair">
480
+ <div class="rscore-box">
481
+ <div class="rscore-num" style="color: ${this.scoreColor(plan.currentScore.overall)}">${plan.currentScore.overall}</div>
482
+ <div class="rscore-label">Current</div>
483
+ </div>
484
+ <div class="rscore-arrow">
485
+ <svg width="60" height="30" viewBox="0 0 60 30">
486
+ <path d="M5 15 L45 15 M40 8 L48 15 L40 22" stroke="#818cf8" stroke-width="2.5" fill="none"/>
487
+ </svg>
488
+ </div>
489
+ <div class="rscore-box">
490
+ <div class="rscore-num" style="color: ${this.scoreColor(plan.estimatedScoreAfter.overall)}">${plan.estimatedScoreAfter.overall}</div>
491
+ <div class="rscore-label">Estimated</div>
492
+ </div>
493
+ <div class="rscore-improvement" style="color: #22c55e">+${improvement} pts</div>
494
+ </div>
495
+ <div class="refactor-bars-section">
496
+ <div class="refactor-legend">
497
+ <span class="rlegend-tag rbefore">Before</span>
498
+ <span class="rlegend-tag rafter">After</span>
499
+ </div>
500
+ ${bars}
501
+ </div>
502
+ </div>
503
+
504
+ <div class="refactor-stats-row">
505
+ <div class="rstat">${plan.steps.length} steps</div>
506
+ <div class="rstat">${plan.totalOperations} operations</div>
507
+ <div class="rstat">Tier 1: ${plan.tier1Steps}</div>
508
+ <div class="rstat">Tier 2: ${plan.tier2Steps}</div>
509
+ </div>
510
+
511
+ <div class="priority-bar">
512
+ ${criticalCount ? `<div class="prio-seg prio-critical" style="flex: ${criticalCount}">🔴 ${criticalCount}</div>` : ''}
513
+ ${highCount ? `<div class="prio-seg prio-high" style="flex: ${highCount}">🟠 ${highCount}</div>` : ''}
514
+ ${mediumCount ? `<div class="prio-seg prio-medium" style="flex: ${mediumCount}">🔵 ${mediumCount}</div>` : ''}
515
+ ${lowCount ? `<div class="prio-seg prio-low" style="flex: ${lowCount}">🟢 ${lowCount}</div>` : ''}
516
+ </div>
517
+
518
+ <div class="refactor-roadmap">
519
+ ${stepsHtml}
520
+ </div>`;
521
+ }
522
+ renderRefactorStep(step) {
523
+ const operationsHtml = step.operations.map(op => `
524
+ <div class="rop">
525
+ <span class="rop-icon">${this.opIcon(op.type)}</span>
526
+ <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>
527
+ <code class="rop-path">${this.escapeHtml(op.path)}</code>
528
+ ${op.newPath ? `<span class="rop-arrow">→</span> <code class="rop-path">${this.escapeHtml(op.newPath)}</code>` : ''}
529
+ <div class="rop-desc">${this.escapeHtml(op.description)}</div>
530
+ </div>
531
+ `).join('');
532
+ const impactHtml = step.scoreImpact.map(i => `<span class="rimpact-tag">${i.metric}: ${i.before}→${i.after} <strong>+${i.after - i.before}</strong></span>`).join('');
533
+ return `
534
+ <div class="rstep-card">
535
+ <div class="rstep-header">
536
+ <div class="rstep-number">${step.id}</div>
537
+ <div class="rstep-info">
538
+ <div class="rstep-title-row">
539
+ <h3>${this.escapeHtml(step.title)}</h3>
540
+ <span class="severity-badge severity-${step.priority}">${step.priority}</span>
541
+ <span class="tier-badge">Tier ${step.tier}</span>
542
+ </div>
543
+ <p class="rstep-desc">${this.escapeHtml(step.description)}</p>
544
+ <details class="rstep-details">
545
+ <summary>📖 Why?</summary>
546
+ <p class="rstep-rationale">${this.escapeHtml(step.rationale)}</p>
547
+ </details>
548
+ </div>
549
+ </div>
550
+ <details class="rstep-ops-accordion">
551
+ <summary class="rstep-ops-toggle">📋 Operations (${step.operations.length})</summary>
552
+ <div class="rstep-ops">
553
+ ${operationsHtml}
554
+ </div>
555
+ </details>
556
+ <div class="rstep-impact">
557
+ <h4>📈 Score Impact</h4>
558
+ <div class="rimpact-tags">${impactHtml}</div>
559
+ </div>
350
560
  </div>`;
351
561
  }
352
562
  /**
@@ -370,6 +580,23 @@ document.addEventListener('DOMContentLoaded', () => {
370
580
  }, { threshold: 0.5 });
371
581
 
372
582
  counters.forEach(c => observer.observe(c));
583
+
584
+ // ── Sidebar Active Section Tracking ──
585
+ const sectionIds = ['score', 'layers', 'anti-patterns', 'suggestions', 'refactoring', 'agents'];
586
+ const sectionObserver = new IntersectionObserver((entries) => {
587
+ entries.forEach(entry => {
588
+ if (entry.isIntersecting) {
589
+ document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
590
+ const link = document.querySelector('.sidebar-link[data-section="' + entry.target.id + '"]');
591
+ if (link) link.classList.add('active');
592
+ }
593
+ });
594
+ }, { threshold: 0.15, rootMargin: '-80px 0px -60% 0px' });
595
+
596
+ sectionIds.forEach(id => {
597
+ const el = document.getElementById(id);
598
+ if (el) sectionObserver.observe(el);
599
+ });
373
600
  });
374
601
 
375
602
  function animateCounter(el, target) {
@@ -472,7 +699,7 @@ function animateCounter(el, target) {
472
699
 
473
700
  const container = document.getElementById('dep-graph');
474
701
  const width = container.clientWidth || 800;
475
- const height = Math.max(400, nodes.length * 25);
702
+ const height = 500;
476
703
  container.style.height = height + 'px';
477
704
 
478
705
  const layerColors = {
@@ -484,26 +711,43 @@ function animateCounter(el, target) {
484
711
  .attr('width', width).attr('height', height)
485
712
  .attr('viewBox', [0, 0, width, height]);
486
713
 
714
+ // Zoom container
715
+ const g = svg.append('g');
716
+
717
+ // Zoom behavior
718
+ const zoom = d3.zoom()
719
+ .scaleExtent([0.2, 5])
720
+ .on('zoom', (event) => { g.attr('transform', event.transform); });
721
+ svg.call(zoom);
722
+
723
+ // Double-click to reset zoom
724
+ svg.on('dblclick.zoom', () => {
725
+ svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
726
+ });
727
+
487
728
  // Arrow marker
488
- svg.append('defs').append('marker')
729
+ g.append('defs').append('marker')
489
730
  .attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10')
490
731
  .attr('refX', 20).attr('refY', 0).attr('orient', 'auto')
491
732
  .attr('markerWidth', 6).attr('markerHeight', 6)
492
733
  .append('path').attr('d', 'M 0,-5 L 10,0 L 0,5')
493
734
  .attr('fill', '#475569');
494
735
 
736
+ // Tuned simulation for better spread
495
737
  const simulation = d3.forceSimulation(nodes)
496
738
  .force('link', d3.forceLink(links).id(d => d.id).distance(80))
497
- .force('charge', d3.forceManyBody().strength(-200))
739
+ .force('charge', d3.forceManyBody().strength(-250))
498
740
  .force('center', d3.forceCenter(width / 2, height / 2))
499
- .force('collision', d3.forceCollide().radius(d => Math.max(d.connections * 3 + 12, 15)));
741
+ .force('x', d3.forceX(width / 2).strength(0.05))
742
+ .force('y', d3.forceY(height / 2).strength(0.05))
743
+ .force('collision', d3.forceCollide().radius(d => Math.max(d.connections * 2 + 16, 20)));
500
744
 
501
- const link = svg.append('g')
745
+ const link = g.append('g')
502
746
  .selectAll('line').data(links).join('line')
503
- .attr('stroke', '#334155').attr('stroke-width', 1.5)
504
- .attr('stroke-opacity', 0.6).attr('marker-end', 'url(#arrowhead)');
747
+ .attr('stroke', '#334155').attr('stroke-width', 1)
748
+ .attr('stroke-opacity', 0.4).attr('marker-end', 'url(#arrowhead)');
505
749
 
506
- const node = svg.append('g')
750
+ const node = g.append('g')
507
751
  .selectAll('g').data(nodes).join('g')
508
752
  .call(d3.drag()
509
753
  .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
@@ -511,21 +755,21 @@ function animateCounter(el, target) {
511
755
  .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
512
756
  );
513
757
 
514
- // Node circles — size based on connections
758
+ // Node circles — color by layer
515
759
  node.append('circle')
516
- .attr('r', d => Math.max(d.connections * 3 + 6, 8))
760
+ .attr('r', d => Math.max(d.connections * 2.5 + 5, 6))
517
761
  .attr('fill', d => layerColors[d.layer] || '#64748b')
518
- .attr('stroke', '#0f172a').attr('stroke-width', 2)
519
- .attr('opacity', 0.85);
762
+ .attr('stroke', '#0f172a').attr('stroke-width', 1.5)
763
+ .attr('opacity', 0.9);
520
764
 
521
- // Node labels
522
- node.append('text')
765
+ // Node labels — only show for nodes with enough connections
766
+ node.filter(d => d.connections >= 2).append('text')
523
767
  .text(d => d.name.replace(/\\.[^.]+$/, ''))
524
- .attr('x', 0).attr('y', d => -(Math.max(d.connections * 3 + 6, 8) + 6))
768
+ .attr('x', 0).attr('y', d => -(Math.max(d.connections * 2.5 + 5, 6) + 4))
525
769
  .attr('text-anchor', 'middle')
526
- .attr('fill', '#94a3b8').attr('font-size', '10px').attr('font-weight', '500');
770
+ .attr('fill', '#e2e8f0').attr('font-size', '9px').attr('font-weight', '500');
527
771
 
528
- // Tooltip on hover
772
+ // Tooltip
529
773
  node.append('title')
530
774
  .text(d => d.id + '\\nConnections: ' + d.connections + '\\nLayer: ' + d.layer);
531
775
 
@@ -535,6 +779,31 @@ function animateCounter(el, target) {
535
779
  .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
536
780
  node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
537
781
  });
782
+
783
+ // Expose search and filter functions
784
+ window.filterGraphNodes = function(query) {
785
+ if (!query) {
786
+ node.attr('opacity', 1);
787
+ link.attr('opacity', 0.4);
788
+ return;
789
+ }
790
+ query = query.toLowerCase();
791
+ node.attr('opacity', d => d.id.toLowerCase().includes(query) || d.name.toLowerCase().includes(query) ? 1 : 0.1);
792
+ link.attr('opacity', d => {
793
+ const srcMatch = d.source.id.toLowerCase().includes(query);
794
+ const tgtMatch = d.target.id.toLowerCase().includes(query);
795
+ return (srcMatch || tgtMatch) ? 0.6 : 0.05;
796
+ });
797
+ };
798
+
799
+ window.toggleGraphLayer = function(layer, visible) {
800
+ node.filter(d => d.layer === layer)
801
+ .transition().duration(300)
802
+ .attr('opacity', visible ? 1 : 0.05);
803
+ link.filter(d => d.source.layer === layer || d.target.layer === layer)
804
+ .transition().duration(300)
805
+ .attr('opacity', visible ? 0.4 : 0.02);
806
+ };
538
807
  })();
539
808
 
540
809
  // ── Bubble Chart ──
@@ -601,6 +870,264 @@ function animateCounter(el, target) {
601
870
  .attr('text-anchor', 'middle').attr('dy', '2.5em')
602
871
  .attr('fill', '#64748b').attr('font-size', '9px').attr('text-transform', 'uppercase');
603
872
  })();
873
+ <\/script>`;
874
+ }
875
+ renderAgentSuggestions(s) {
876
+ const roleIcon = (name) => {
877
+ if (name.includes('ORCHESTRATOR'))
878
+ return '\u{1F3AD}';
879
+ if (name.includes('BACKEND') || name.includes('FRONTEND') || name.includes('DATABASE') || name.includes('FLUTTER'))
880
+ return '\u{1F4BB}';
881
+ if (name.includes('SECURITY'))
882
+ return '\u{1F6E1}\uFE0F';
883
+ if (name.includes('QA'))
884
+ return '\u{1F9EA}';
885
+ if (name.includes('TECH-DEBT'))
886
+ return '\u{1F4CA}';
887
+ return '\u{1F916}';
888
+ };
889
+ const roleLabel = (name) => {
890
+ if (name.includes('ORCHESTRATOR'))
891
+ return 'coordination';
892
+ if (name.includes('SECURITY'))
893
+ return 'protection';
894
+ if (name.includes('QA'))
895
+ return 'quality';
896
+ if (name.includes('TECH-DEBT'))
897
+ return 'governance';
898
+ return 'development';
899
+ };
900
+ const roleColor = (name) => {
901
+ if (name.includes('ORCHESTRATOR'))
902
+ return '#c084fc';
903
+ if (name.includes('SECURITY'))
904
+ return '#f87171';
905
+ if (name.includes('QA'))
906
+ return '#34d399';
907
+ if (name.includes('TECH-DEBT'))
908
+ return '#fbbf24';
909
+ return '#60a5fa';
910
+ };
911
+ // Status helpers
912
+ const statusBadge = (status) => {
913
+ const map = {
914
+ 'KEEP': { icon: '✅', label: 'KEEP', color: '#22c55e' },
915
+ 'MODIFY': { icon: '🔵', label: 'MODIFY', color: '#3b82f6' },
916
+ 'CREATE': { icon: '🟡', label: 'NEW', color: '#f59e0b' },
917
+ 'DELETE': { icon: '🔴', label: 'REMOVE', color: '#ef4444' },
918
+ };
919
+ const s = map[status] || map['CREATE'];
920
+ 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>`;
921
+ };
922
+ const statusBorder = (status) => {
923
+ const map = {
924
+ 'KEEP': '#22c55e', 'MODIFY': '#3b82f6', 'CREATE': '#f59e0b', 'DELETE': '#ef4444',
925
+ };
926
+ return map[status] || '#334155';
927
+ };
928
+ const agentCards = s.suggestedAgents.map(a => `<label class="agent-toggle-card" data-category="agents" data-name="${a.name}">
929
+ <input type="checkbox" class="agent-check" ${a.status !== 'DELETE' ? 'checked' : ''} data-type="agents" data-item="${a.name}">
930
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(a.status)}">
931
+ <div class="agent-toggle-icon">${roleIcon(a.name)}</div>
932
+ <div class="agent-toggle-info">
933
+ <span class="agent-toggle-name">${a.name}</span>
934
+ <span class="agent-toggle-role" style="color:${roleColor(a.name)}">${roleLabel(a.name)}</span>
935
+ ${a.description ? `<span class="agent-toggle-desc">${a.description}</span>` : ''}
936
+ </div>
937
+ ${statusBadge(a.status)}
938
+ <div class="agent-toggle-check">\u2713</div>
939
+ </div>
940
+ </label>`).join('\n');
941
+ const miniCard = (item, icon, type) => `<label class="agent-toggle-card mini" data-category="${type}">
942
+ <input type="checkbox" class="agent-check" ${item.status !== 'DELETE' ? 'checked' : ''} data-type="${type}" data-item="${item.name}">
943
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(item.status)}">
944
+ <span class="agent-toggle-icon">${icon}</span>
945
+ <div class="agent-toggle-info">
946
+ <span class="agent-toggle-name">${item.name}.md</span>
947
+ ${item.description ? `<span class="agent-toggle-desc">${item.description}</span>` : ''}
948
+ </div>
949
+ ${statusBadge(item.status)}
950
+ <div class="agent-toggle-check">\u2713</div>
951
+ </div>
952
+ </label>`;
953
+ const ruleCards = s.suggestedRules.map(r => miniCard(r, '\u{1F4CF}', 'rules')).join('\n');
954
+ const guardCards = s.suggestedGuards.map(g => miniCard(g, '\u{1F6E1}\uFE0F', 'guards')).join('\n');
955
+ const workflowCards = s.suggestedWorkflows.map(w => miniCard(w, '\u26A1', 'workflows')).join('\n');
956
+ const skillCards = s.suggestedSkills.map(sk => `<label class="agent-toggle-card" data-category="skills">
957
+ <input type="checkbox" class="agent-check" checked data-type="skills" data-item="${sk.source}">
958
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(sk.status)}">
959
+ <span class="agent-toggle-icon">\u{1F9E0}</span>
960
+ <div class="agent-toggle-info">
961
+ <span class="agent-toggle-name">${sk.name}</span>
962
+ <span class="agent-toggle-role" style="color:#34d399">${sk.description}</span>
963
+ </div>
964
+ ${statusBadge(sk.status)}
965
+ <div class="agent-toggle-check">\u2713</div>
966
+ </div>
967
+ </label>`).join('\n');
968
+ const auditSection = s.audit.filter(f => f.type !== 'OK').length > 0 ? `
969
+ <div class="agent-audit-section">
970
+ <h3 class="agent-section-subtitle">\u{1F50D} Audit Findings</h3>
971
+ <div class="agent-audit-grid">
972
+ ${s.audit.filter(f => f.type !== 'OK').map(f => {
973
+ const icon = f.type === 'MISSING' ? '\u274C' : f.type === 'IMPROVEMENT' ? '\u{1F4A1}' : '\u26A0\uFE0F';
974
+ const cls = f.type === 'MISSING' ? 'audit-missing' : 'audit-improvement';
975
+ return `<div class="agent-audit-item ${cls}">
976
+ <span class="audit-icon">${icon}</span>
977
+ <div class="audit-content">
978
+ <span class="audit-desc">${f.description}</span>
979
+ ${f.suggestion ? `<span class="audit-suggestion">\u2192 ${f.suggestion}</span>` : ''}
980
+ </div>
981
+ </div>`;
982
+ }).join('\n')}
983
+ </div>
984
+ </div>` : '';
985
+ const stackPills = [
986
+ `\u{1F527} ${s.stack.primary}`,
987
+ `\u{1F4E6} ${s.stack.frameworks.length > 0 ? s.stack.frameworks.join(', ') : 'No framework'}`,
988
+ s.hasExistingAgents ? '\u{1F4C1} Existing .agent/' : '\u{1F4C1} New .agent/',
989
+ ...(s.stack.hasBackend ? ['\u{1F519} Backend'] : []),
990
+ ...(s.stack.hasFrontend ? ['\u{1F5A5}\uFE0F Frontend'] : []),
991
+ ...(s.stack.hasMobile ? ['\u{1F4F1} Mobile'] : []),
992
+ ...(s.stack.hasDatabase ? ['\u{1F5C4}\uFE0F Database'] : []),
993
+ ];
994
+ const totalItems = s.suggestedAgents.length + s.suggestedRules.length + s.suggestedGuards.length + s.suggestedWorkflows.length + s.suggestedSkills.length;
995
+ // Status summary counts
996
+ const allItems = [...s.suggestedAgents, ...s.suggestedRules, ...s.suggestedGuards, ...s.suggestedWorkflows];
997
+ const keepCount = allItems.filter(i => i.status === 'KEEP').length;
998
+ const modifyCount = allItems.filter(i => i.status === 'MODIFY').length;
999
+ const createCount = allItems.filter(i => i.status === 'CREATE').length;
1000
+ return `
1001
+ <h2 class="section-title">\u{1F916} Agent System</h2>
1002
+
1003
+ <div class="card agent-system-card">
1004
+ <div class="agent-stack-banner">
1005
+ ${stackPills.map(p => `<div class="stack-pill">${p}</div>`).join('\n ')}
1006
+ </div>
1007
+
1008
+ <div class="agent-status-legend">
1009
+ <span class="status-legend-item"><span class="legend-dot" style="background:#22c55e"></span> KEEP (${keepCount})</span>
1010
+ <span class="status-legend-item"><span class="legend-dot" style="background:#3b82f6"></span> MODIFY (${modifyCount})</span>
1011
+ <span class="status-legend-item"><span class="legend-dot" style="background:#f59e0b"></span> NEW (${createCount})</span>
1012
+ </div>
1013
+
1014
+ <div class="agent-controls">
1015
+ <button class="agent-ctrl-btn" onclick="toggleAll(true)">\u2705 Select All</button>
1016
+ <button class="agent-ctrl-btn" onclick="toggleAll(false)">\u2B1C Select None</button>
1017
+ <span class="agent-count-label"><span id="agentSelectedCount">${totalItems}</span> selected</span>
1018
+ </div>
1019
+
1020
+ <h3 class="agent-section-subtitle">\u{1F916} Agents</h3>
1021
+ <div class="agent-toggle-grid">
1022
+ ${agentCards}
1023
+ </div>
1024
+
1025
+ <div class="agent-extras-grid">
1026
+ <div>
1027
+ <h3 class="agent-section-subtitle">\u{1F4CF} Rules</h3>
1028
+ <div class="agent-toggle-list">${ruleCards}</div>
1029
+ </div>
1030
+ <div>
1031
+ <h3 class="agent-section-subtitle">\u{1F6E1}\uFE0F Guards</h3>
1032
+ <div class="agent-toggle-list">${guardCards}</div>
1033
+ </div>
1034
+ <div>
1035
+ <h3 class="agent-section-subtitle">\u26A1 Workflows</h3>
1036
+ <div class="agent-toggle-list">${workflowCards}</div>
1037
+ </div>
1038
+ </div>
1039
+
1040
+ <h3 class="agent-section-subtitle">\u{1F9E0} Skills <span style="font-size:0.7rem;color:#94a3b8;font-weight:400">from skills.sh</span></h3>
1041
+ <div class="agent-toggle-grid">
1042
+ ${skillCards}
1043
+ </div>
1044
+
1045
+ ${auditSection}
1046
+
1047
+ <div class="agent-command-box">
1048
+ <div class="agent-command-header">
1049
+ <span>\u{1F4A1} Command to generate selected items:</span>
1050
+ <button class="agent-copy-btn" onclick="copyAgentCommand()">
1051
+ <span id="copyIcon">\u{1F4CB}</span> Copy
1052
+ </button>
1053
+ </div>
1054
+ <code id="agentCommandOutput" class="agent-command-code">${s.command}</code>
1055
+ </div>
1056
+ </div>
1057
+
1058
+ <style>
1059
+ .agent-system-card { padding: 1.5rem; }
1060
+ .agent-stack-banner { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
1061
+ .stack-pill { background: #1e293b; border: 1px solid #334155; border-radius: 99px; padding: 0.4rem 1rem; font-size: 0.8rem; color: #94a3b8; white-space: nowrap; }
1062
+ .agent-status-legend { display: flex; gap: 1.5rem; margin-bottom: 1rem; padding: 0.5rem 0; border-bottom: 1px solid #1e293b; }
1063
+ .status-legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.8rem; color: #94a3b8; }
1064
+ .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; }
1065
+ .agent-toggle-desc { display: block; font-size: 0.65rem; color: #64748b; margin-top: 0.15rem; line-height: 1.3; }
1066
+ .agent-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
1067
+ .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; }
1068
+ .agent-ctrl-btn:hover { background: #334155; }
1069
+ .agent-count-label { color: #94a3b8; font-size: 0.85rem; margin-left: auto; }
1070
+ #agentSelectedCount { color: #c084fc; font-weight: 700; }
1071
+ .agent-section-subtitle { color: #e2e8f0; font-size: 1.05rem; font-weight: 700; margin: 1.25rem 0 0.75rem; }
1072
+ .agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; }
1073
+ .agent-toggle-card { cursor: pointer; transition: all 0.3s; }
1074
+ .agent-toggle-card input { display: none; }
1075
+ .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; }
1076
+ .agent-toggle-card input:checked + .agent-toggle-inner { background: #1e1b4b; }
1077
+ .agent-toggle-icon { font-size: 1.3rem; flex-shrink: 0; }
1078
+ .agent-toggle-info { flex: 1; min-width: 0; }
1079
+ .agent-toggle-name { display: block; color: #e2e8f0; font-weight: 600; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1080
+ .agent-toggle-role { display: block; font-size: 0.7rem; margin-top: 0.15rem; }
1081
+ .agent-toggle-check { color: #334155; font-size: 1rem; flex-shrink: 0; transition: color 0.3s; }
1082
+ .agent-toggle-card input:checked + .agent-toggle-inner .agent-toggle-check { color: #818cf8; }
1083
+ .agent-toggle-card.mini .agent-toggle-inner { padding: 0.5rem 0.75rem; border-radius: 8px; }
1084
+ .agent-extras-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 0.5rem; }
1085
+ @media (max-width: 768px) { .agent-extras-grid { grid-template-columns: 1fr; } }
1086
+ .agent-toggle-list { display: flex; flex-direction: column; gap: 0.5rem; }
1087
+ .agent-audit-section { margin-top: 1.5rem; }
1088
+ .agent-audit-grid { display: flex; flex-direction: column; gap: 0.5rem; }
1089
+ .agent-audit-item { display: flex; gap: 0.75rem; align-items: flex-start; background: #1e293b; padding: 0.75rem 1rem; border-radius: 8px; }
1090
+ .agent-audit-item.audit-missing { border-left: 3px solid #ef4444; }
1091
+ .agent-audit-item.audit-improvement { border-left: 3px solid #fbbf24; }
1092
+ .audit-icon { font-size: 1rem; flex-shrink: 0; margin-top: 2px; }
1093
+ .audit-content { display: flex; flex-direction: column; gap: 0.25rem; }
1094
+ .audit-desc { color: #e2e8f0; font-size: 0.85rem; }
1095
+ .audit-suggestion { color: #94a3b8; font-size: 0.8rem; font-style: italic; }
1096
+ .agent-command-box { margin-top: 1.5rem; background: #0f172a; border-radius: 12px; border: 1px solid #334155; overflow: hidden; }
1097
+ .agent-command-header { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: #1e293b; font-size: 0.8rem; color: #94a3b8; }
1098
+ .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; }
1099
+ .agent-copy-btn:hover { background: #a855f7; transform: scale(1.05); }
1100
+ .agent-command-code { display: block; padding: 1rem; color: #c084fc; font-size: 0.85rem; word-break: break-all; font-family: 'Fira Code', monospace; }
1101
+ </style>
1102
+
1103
+ <script>
1104
+ (function() {
1105
+ var basePath = ${JSON.stringify(s.command.replace('architect agents ', ''))};
1106
+ var totalItems = ${totalItems};
1107
+ function updateCommand() {
1108
+ var checks = document.querySelectorAll('.agent-check');
1109
+ var selected = { agents: [], rules: [], guards: [], workflows: [], skills: [] };
1110
+ var count = 0;
1111
+ checks.forEach(function(cb) { if (cb.checked) { selected[cb.dataset.type].push(cb.dataset.item); count++; } });
1112
+ document.getElementById('agentSelectedCount').textContent = count;
1113
+ var cmd;
1114
+ if (count === totalItems) { cmd = 'architect agents ' + basePath; }
1115
+ else if (count === 0) { cmd = '# No items selected'; }
1116
+ else {
1117
+ var parts = ['architect agents ' + basePath];
1118
+ if (selected.agents.length > 0) parts.push('--agents ' + selected.agents.join(','));
1119
+ if (selected.rules.length > 0) parts.push('--rules ' + selected.rules.join(','));
1120
+ if (selected.guards.length > 0) parts.push('--guards ' + selected.guards.join(','));
1121
+ if (selected.workflows.length > 0) parts.push('--workflows ' + selected.workflows.join(','));
1122
+ if (selected.skills.length > 0) parts.push('&& ' + selected.skills.map(function(sk){ return 'npx skills add ' + sk; }).join(' && '));
1123
+ cmd = parts.join(' ');
1124
+ }
1125
+ document.getElementById('agentCommandOutput').textContent = cmd;
1126
+ }
1127
+ document.querySelectorAll('.agent-check').forEach(function(cb) { cb.addEventListener('change', updateCommand); });
1128
+ window.toggleAll = function(state) { document.querySelectorAll('.agent-check').forEach(function(cb) { cb.checked = state; }); updateCommand(); };
1129
+ 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); }); };
1130
+ })();
604
1131
  <\/script>`;
605
1132
  }
606
1133
  getStyles() {
@@ -617,7 +1144,50 @@ function animateCounter(el, target) {
617
1144
  min-height: 100vh;
618
1145
  }
619
1146
 
620
- .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
1147
+ html { scroll-behavior: smooth; }
1148
+
1149
+ /* ── Layout ── */
1150
+ .report-layout { display: flex; min-height: 100vh; }
1151
+
1152
+ .sidebar {
1153
+ position: sticky; top: 0; height: 100vh; width: 220px; min-width: 220px;
1154
+ background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
1155
+ border-right: 1px solid #334155; padding: 1.5rem 0;
1156
+ display: flex; flex-direction: column; gap: 0.25rem;
1157
+ overflow-y: auto; z-index: 100;
1158
+ }
1159
+ .sidebar-title {
1160
+ font-size: 0.7rem; font-weight: 700; text-transform: uppercase;
1161
+ letter-spacing: 0.15em; color: #475569; padding: 0 1.25rem; margin-bottom: 0.75rem;
1162
+ }
1163
+ .sidebar-link {
1164
+ display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1.25rem;
1165
+ color: #94a3b8; text-decoration: none; font-size: 0.8rem; font-weight: 500;
1166
+ border-left: 3px solid transparent; transition: all 0.2s;
1167
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1168
+ }
1169
+ .sidebar-link:hover { color: #e2e8f0; background: #1e293b; border-left-color: #475569; }
1170
+ .sidebar-link.active { color: #c084fc; background: #c084fc10; border-left-color: #c084fc; font-weight: 700; }
1171
+
1172
+ .sidebar-toggle {
1173
+ display: none; position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 200;
1174
+ width: 48px; height: 48px; border-radius: 50%; border: none;
1175
+ background: #c084fc; color: #0f172a; font-size: 1.2rem; cursor: pointer;
1176
+ box-shadow: 0 4px 16px rgba(192,132,252,0.4); transition: all 0.2s;
1177
+ }
1178
+ .sidebar-toggle:hover { transform: scale(1.1); }
1179
+
1180
+ @media (max-width: 1024px) {
1181
+ .sidebar {
1182
+ position: fixed; left: -240px; top: 0; width: 240px; min-width: 240px;
1183
+ transition: left 0.3s ease; box-shadow: none;
1184
+ }
1185
+ .sidebar.sidebar-open { left: 0; box-shadow: 4px 0 24px rgba(0,0,0,0.5); }
1186
+ .sidebar-toggle { display: flex; align-items: center; justify-content: center; }
1187
+ .report-layout { flex-direction: column; }
1188
+ }
1189
+
1190
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; flex: 1; min-width: 0; }
621
1191
 
622
1192
  /* ── Header ── */
623
1193
  .header {
@@ -696,6 +1266,44 @@ function animateCounter(el, target) {
696
1266
  display: flex; align-items: center; gap: 0.5rem;
697
1267
  }
698
1268
 
1269
+ /* ── Section Accordion ── */
1270
+ .section-accordion {
1271
+ margin: 1.5rem 0; border: 1px solid #334155; border-radius: 16px;
1272
+ background: transparent; overflow: hidden;
1273
+ }
1274
+ .section-accordion-header {
1275
+ cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.75rem;
1276
+ font-size: 1.3rem; font-weight: 700; color: #e2e8f0;
1277
+ padding: 1.25rem 1.5rem; background: linear-gradient(135deg, #1e293b, #0f172a);
1278
+ border-bottom: 1px solid transparent; transition: all 0.3s; user-select: none;
1279
+ }
1280
+ .section-accordion-header:hover { background: linear-gradient(135deg, #334155, #1e293b); }
1281
+ .section-accordion[open] > .section-accordion-header { border-bottom-color: #334155; }
1282
+ .section-accordion-header::after {
1283
+ content: '\\25B6'; margin-left: auto; font-size: 0.8rem; color: #818cf8;
1284
+ transition: transform 0.3s;
1285
+ }
1286
+ .section-accordion[open] > .section-accordion-header::after { transform: rotate(90deg); }
1287
+ .section-accordion-header::-webkit-details-marker { display: none; }
1288
+ .section-accordion-body { padding: 0.5rem 0; }
1289
+
1290
+ /* ── Operations Accordion (inside refactoring steps) ── */
1291
+ .rstep-ops-accordion {
1292
+ margin: 0.75rem 0; border: 1px solid #1e293b; border-radius: 10px; overflow: hidden;
1293
+ }
1294
+ .rstep-ops-toggle {
1295
+ cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.5rem;
1296
+ font-size: 0.9rem; font-weight: 600; color: #94a3b8;
1297
+ padding: 0.75rem 1rem; background: #0f172a; transition: all 0.2s;
1298
+ }
1299
+ .rstep-ops-toggle:hover { background: #1e293b; color: #e2e8f0; }
1300
+ .rstep-ops-toggle::after {
1301
+ content: '\\25B6'; margin-left: auto; font-size: 0.65rem; color: #818cf8;
1302
+ transition: transform 0.3s;
1303
+ }
1304
+ .rstep-ops-accordion[open] > .rstep-ops-toggle::after { transform: rotate(90deg); }
1305
+ .rstep-ops-toggle::-webkit-details-marker { display: none; }
1306
+
699
1307
  /* ── Cards ── */
700
1308
  .card {
701
1309
  background: #1e293b; border-radius: 16px; border: 1px solid #334155;
@@ -705,17 +1313,42 @@ function animateCounter(el, target) {
705
1313
 
706
1314
  /* ── Graph ── */
707
1315
  .graph-card { padding: 1rem; }
1316
+ .graph-controls { margin-bottom: 0.75rem; }
708
1317
  .graph-legend {
709
1318
  display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
710
1319
  justify-content: center;
711
1320
  }
712
1321
  .legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: #94a3b8; }
713
- .legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
1322
+ .legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
1323
+ .graph-filters {
1324
+ display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;
1325
+ justify-content: center; margin-top: 0.5rem;
1326
+ }
1327
+ .graph-search {
1328
+ background: #0f172a; border: 1px solid #334155; border-radius: 8px;
1329
+ padding: 0.4rem 0.75rem; color: #e2e8f0; font-size: 0.8rem;
1330
+ outline: none; width: 180px; transition: border-color 0.2s;
1331
+ }
1332
+ .graph-search:focus { border-color: #818cf8; }
1333
+ .graph-layer-filters {
1334
+ display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;
1335
+ }
1336
+ .graph-filter-check {
1337
+ display: flex; align-items: center; gap: 4px;
1338
+ font-size: 0.75rem; color: #94a3b8; cursor: pointer;
1339
+ }
1340
+ .graph-filter-check input { width: 14px; height: 14px; accent-color: #818cf8; }
1341
+ .graph-limit-notice {
1342
+ text-align: center; font-size: 0.75rem; color: #f59e0b;
1343
+ background: #f59e0b15; padding: 0.3rem 0.75rem; border-radius: 6px;
1344
+ margin-top: 0.5rem;
1345
+ }
714
1346
  .graph-hint {
715
1347
  text-align: center; font-size: 0.75rem; color: #475569; margin-top: 0.5rem;
716
1348
  font-style: italic;
717
1349
  }
718
- #dep-graph svg { background: rgba(0,0,0,0.2); border-radius: 12px; }
1350
+ #dep-graph svg { background: rgba(0,0,0,0.2); border-radius: 12px; cursor: grab; }
1351
+ #dep-graph svg:active { cursor: grabbing; }
719
1352
 
720
1353
  /* ── Layers Grid ── */
721
1354
  .layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }
@@ -765,12 +1398,102 @@ function animateCounter(el, target) {
765
1398
  .footer a { color: #818cf8; text-decoration: none; }
766
1399
  .footer a:hover { text-decoration: underline; }
767
1400
 
1401
+ /* ── Refactoring Plan ── */
1402
+ .refactor-score { padding: 2rem; }
1403
+ .refactor-score-pair {
1404
+ display: flex; align-items: center; justify-content: center; gap: 1.5rem;
1405
+ margin-bottom: 2rem; flex-wrap: wrap;
1406
+ }
1407
+ .rscore-box { text-align: center; }
1408
+ .rscore-num { font-size: 3rem; font-weight: 900; line-height: 1; }
1409
+ .rscore-label { font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }
1410
+ .rscore-improvement { font-size: 1.3rem; font-weight: 700; }
1411
+
1412
+ .refactor-legend { display: flex; gap: 1rem; margin-bottom: 0.5rem; }
1413
+ .rlegend-tag { font-size: 0.75rem; padding: 0.2rem 0.6rem; border-radius: 6px; }
1414
+ .rlegend-tag.rbefore { background: rgba(255,255,255,0.05); color: #94a3b8; }
1415
+ .rlegend-tag.rafter { background: rgba(129,140,248,0.2); color: #818cf8; }
1416
+
1417
+ .refactor-metric-name { width: 100px; font-size: 0.8rem; text-transform: uppercase; color: #94a3b8; font-weight: 600; }
1418
+ .refactor-metric-bars { flex: 1; position: relative; height: 30px; }
1419
+ .rbar-before, .rbar-after {
1420
+ position: absolute; left: 0; height: 14px; border-radius: 4px;
1421
+ display: flex; align-items: center; padding-left: 6px;
1422
+ font-size: 0.7rem; font-weight: 600;
1423
+ }
1424
+ .rbar-before { top: 0; }
1425
+ .rbar-after { top: 15px; }
1426
+ .refactor-metric-diff { width: 50px; text-align: right; font-weight: 700; font-size: 0.85rem; }
1427
+
1428
+ .refactor-stats-row {
1429
+ display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;
1430
+ }
1431
+ .rstat {
1432
+ background: #1e293b; border: 1px solid #334155; border-radius: 99px;
1433
+ padding: 0.4rem 1rem; font-size: 0.85rem; color: #94a3b8; font-weight: 500;
1434
+ }
1435
+
1436
+ .priority-bar {
1437
+ display: flex; border-radius: 12px; overflow: hidden; height: 32px; margin-bottom: 2rem;
1438
+ }
1439
+ .prio-seg {
1440
+ display: flex; align-items: center; justify-content: center;
1441
+ font-size: 0.75rem; font-weight: 600;
1442
+ }
1443
+ .prio-critical { background: #ef444430; color: #ef4444; }
1444
+ .prio-high { background: #f59e0b30; color: #f59e0b; }
1445
+ .prio-medium { background: #3b82f630; color: #60a5fa; }
1446
+ .prio-low { background: #22c55e30; color: #22c55e; }
1447
+
1448
+ .refactor-roadmap { display: flex; flex-direction: column; gap: 1rem; }
1449
+ .rstep-card {
1450
+ background: #1e293b; border-radius: 16px; border: 1px solid #334155;
1451
+ padding: 1.5rem; transition: border-color 0.2s;
1452
+ }
1453
+ .rstep-card:hover { border-color: #818cf8; }
1454
+ .rstep-header { display: flex; gap: 1rem; margin-bottom: 1rem; }
1455
+ .rstep-number {
1456
+ width: 40px; height: 40px; border-radius: 50%;
1457
+ background: linear-gradient(135deg, #818cf8, #c084fc);
1458
+ display: flex; align-items: center; justify-content: center;
1459
+ font-weight: 800; font-size: 1rem; color: white; flex-shrink: 0;
1460
+ }
1461
+ .rstep-info { flex: 1; }
1462
+ .rstep-title-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
1463
+ .rstep-title-row h3 { font-size: 1.1rem; font-weight: 700; }
1464
+ .rstep-desc { color: #94a3b8; font-size: 0.9rem; margin-top: 0.3rem; }
1465
+ .tier-badge {
1466
+ background: #818cf815; color: #818cf8; border: 1px solid #818cf830;
1467
+ padding: 0.15rem 0.5rem; border-radius: 99px; font-size: 0.65rem; font-weight: 600;
1468
+ }
1469
+ .rstep-details { margin-top: 0.5rem; }
1470
+ .rstep-details summary { cursor: pointer; color: #818cf8; font-size: 0.85rem; font-weight: 500; }
1471
+ .rstep-rationale { color: #64748b; font-size: 0.85rem; margin-top: 0.3rem; font-style: italic; }
1472
+
1473
+ .rstep-ops { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
1474
+ .rstep-ops h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
1475
+ .rop { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
1476
+ .rop-icon { font-size: 0.9rem; }
1477
+ .rop-badge { padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.65rem; font-weight: 700; }
1478
+ .rop-path { background: #0f172a; padding: 1px 6px; border-radius: 4px; font-size: 0.8rem; color: #c084fc; }
1479
+ .rop-arrow { color: #818cf8; font-weight: 700; }
1480
+ .rop-desc { width: 100%; color: #64748b; font-size: 0.8rem; padding-left: 1.8rem; }
1481
+
1482
+ .rstep-impact { margin-top: 0.5rem; }
1483
+ .rstep-impact h4 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.3rem; }
1484
+ .rimpact-tags { display: flex; gap: 0.5rem; flex-wrap: wrap; }
1485
+ .rimpact-tag {
1486
+ background: #22c55e10; color: #22c55e; border: 1px solid #22c55e30;
1487
+ padding: 0.2rem 0.6rem; border-radius: 8px; font-size: 0.75rem; font-weight: 500;
1488
+ }
1489
+
768
1490
  /* ── Responsive ── */
769
1491
  @media (max-width: 768px) {
770
1492
  .score-hero { flex-direction: column; gap: 1.5rem; }
771
1493
  .score-breakdown { grid-template-columns: 1fr; }
772
1494
  .header h1 { font-size: 1.8rem; }
773
1495
  .container { padding: 1rem; }
1496
+ .refactor-score-pair { flex-direction: column; }
774
1497
  }
775
1498
 
776
1499
  /* ── Print ── */