@girardelli/architect 2.1.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.
@@ -17,17 +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)}
29
- ${plan ? this.renderRefactoringPlan(plan) : ''}
30
- ${agentSuggestion ? this.renderAgentSuggestions(agentSuggestion) : ''}
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>
31
76
  </div>
32
77
  ${this.renderFooter()}
33
78
  ${this.getScripts(report)}
@@ -208,44 +253,70 @@ ${this.getScripts(report)}
208
253
  renderDependencyGraph(report) {
209
254
  if (report.dependencyGraph.edges.length === 0)
210
255
  return '';
211
- // 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
212
259
  const connectionCount = {};
213
260
  for (const edge of report.dependencyGraph.edges) {
214
- connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
215
- 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
+ }
216
267
  }
268
+ // Build layer map from report layers
217
269
  const layerMap = {};
218
270
  for (const layer of report.layers) {
219
271
  for (const file of layer.files) {
220
272
  layerMap[file] = layer.name;
221
273
  }
222
274
  }
223
- const nodes = report.dependencyGraph.nodes.map(n => ({
275
+ // Create nodes only from real files
276
+ const allNodes = [...realFiles].map(n => ({
224
277
  id: n,
225
278
  name: n.split('/').pop() || n,
226
279
  connections: connectionCount[n] || 0,
227
280
  layer: layerMap[n] || 'Other',
228
281
  }));
229
- const links = report.dependencyGraph.edges.map(e => ({
230
- source: e.from,
231
- target: e.to,
232
- }));
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))];
233
295
  return `
234
296
  <h2 class="section-title">🔗 Dependency Graph</h2>
235
297
  <div class="card graph-card">
236
- <div class="graph-legend">
237
- <span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
238
- <span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
239
- <span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
240
- <span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
241
- <span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
242
- <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>` : ''}
243
314
  </div>
244
- <div id="dep-graph" style="width:100%; min-height:400px;"></div>
245
- <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>
246
317
  </div>
247
- <script type="application/json" id="graph-nodes">${JSON.stringify(nodes)}<\/script>
248
- <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>`;
249
320
  }
250
321
  /**
251
322
  * Bubble chart for anti-patterns — bigger = more severe
@@ -476,10 +547,12 @@ ${this.getScripts(report)}
476
547
  </details>
477
548
  </div>
478
549
  </div>
479
- <div class="rstep-ops">
480
- <h4>📋 Operations (${step.operations.length})</h4>
481
- ${operationsHtml}
482
- </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>
483
556
  <div class="rstep-impact">
484
557
  <h4>📈 Score Impact</h4>
485
558
  <div class="rimpact-tags">${impactHtml}</div>
@@ -507,6 +580,23 @@ document.addEventListener('DOMContentLoaded', () => {
507
580
  }, { threshold: 0.5 });
508
581
 
509
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
+ });
510
600
  });
511
601
 
512
602
  function animateCounter(el, target) {
@@ -609,7 +699,7 @@ function animateCounter(el, target) {
609
699
 
610
700
  const container = document.getElementById('dep-graph');
611
701
  const width = container.clientWidth || 800;
612
- const height = 450;
702
+ const height = 500;
613
703
  container.style.height = height + 'px';
614
704
 
615
705
  const layerColors = {
@@ -621,28 +711,43 @@ function animateCounter(el, target) {
621
711
  .attr('width', width).attr('height', height)
622
712
  .attr('viewBox', [0, 0, width, height]);
623
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
+
624
728
  // Arrow marker
625
- svg.append('defs').append('marker')
729
+ g.append('defs').append('marker')
626
730
  .attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10')
627
731
  .attr('refX', 20).attr('refY', 0).attr('orient', 'auto')
628
732
  .attr('markerWidth', 6).attr('markerHeight', 6)
629
733
  .append('path').attr('d', 'M 0,-5 L 10,0 L 0,5')
630
734
  .attr('fill', '#475569');
631
735
 
736
+ // Tuned simulation for better spread
632
737
  const simulation = d3.forceSimulation(nodes)
633
- .force('link', d3.forceLink(links).id(d => d.id).distance(60))
634
- .force('charge', d3.forceManyBody().strength(-150))
738
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80))
739
+ .force('charge', d3.forceManyBody().strength(-250))
635
740
  .force('center', d3.forceCenter(width / 2, height / 2))
636
- .force('x', d3.forceX(width / 2).strength(0.1))
637
- .force('y', d3.forceY(height / 2).strength(0.1))
638
- .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)));
639
744
 
640
- const link = svg.append('g')
745
+ const link = g.append('g')
641
746
  .selectAll('line').data(links).join('line')
642
- .attr('stroke', '#334155').attr('stroke-width', 1.5)
643
- .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)');
644
749
 
645
- const node = svg.append('g')
750
+ const node = g.append('g')
646
751
  .selectAll('g').data(nodes).join('g')
647
752
  .call(d3.drag()
648
753
  .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
@@ -650,37 +755,55 @@ function animateCounter(el, target) {
650
755
  .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
651
756
  );
652
757
 
653
- // Node circles — size based on connections
758
+ // Node circles — color by layer
654
759
  node.append('circle')
655
- .attr('r', d => Math.max(d.connections * 3 + 6, 8))
760
+ .attr('r', d => Math.max(d.connections * 2.5 + 5, 6))
656
761
  .attr('fill', d => layerColors[d.layer] || '#64748b')
657
- .attr('stroke', '#0f172a').attr('stroke-width', 2)
658
- .attr('opacity', 0.85);
762
+ .attr('stroke', '#0f172a').attr('stroke-width', 1.5)
763
+ .attr('opacity', 0.9);
659
764
 
660
- // Node labels
661
- node.append('text')
765
+ // Node labels — only show for nodes with enough connections
766
+ node.filter(d => d.connections >= 2).append('text')
662
767
  .text(d => d.name.replace(/\\.[^.]+$/, ''))
663
- .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))
664
769
  .attr('text-anchor', 'middle')
665
- .attr('fill', '#94a3b8').attr('font-size', '10px').attr('font-weight', '500');
770
+ .attr('fill', '#e2e8f0').attr('font-size', '9px').attr('font-weight', '500');
666
771
 
667
- // Tooltip on hover
772
+ // Tooltip
668
773
  node.append('title')
669
774
  .text(d => d.id + '\\nConnections: ' + d.connections + '\\nLayer: ' + d.layer);
670
775
 
671
776
  simulation.on('tick', () => {
672
- // Clamp nodes to stay within SVG bounds
673
- nodes.forEach(d => {
674
- const r = Math.max(d.connections * 3 + 6, 8) + 10;
675
- d.x = Math.max(r, Math.min(width - r, d.x));
676
- d.y = Math.max(r, Math.min(height - r, d.y));
677
- });
678
-
679
777
  link
680
778
  .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
681
779
  .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
682
780
  node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
683
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
+ };
684
807
  })();
685
808
 
686
809
  // ── Bubble Chart ──
@@ -785,49 +908,60 @@ function animateCounter(el, target) {
785
908
  return '#fbbf24';
786
909
  return '#60a5fa';
787
910
  };
788
- const agentCards = s.suggestedAgents.map(a => `<label class="agent-toggle-card" data-category="agents" data-name="${a}">
789
- <input type="checkbox" class="agent-check" checked data-type="agents" data-item="${a}">
790
- <div class="agent-toggle-inner">
791
- <div class="agent-toggle-icon">${roleIcon(a)}</div>
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>
792
932
  <div class="agent-toggle-info">
793
- <span class="agent-toggle-name">${a}</span>
794
- <span class="agent-toggle-role" style="color:${roleColor(a)}">${roleLabel(a)}</span>
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>` : ''}
795
936
  </div>
937
+ ${statusBadge(a.status)}
796
938
  <div class="agent-toggle-check">\u2713</div>
797
939
  </div>
798
940
  </label>`).join('\n');
799
- const ruleCards = s.suggestedRules.map(r => `<label class="agent-toggle-card mini" data-category="rules">
800
- <input type="checkbox" class="agent-check" checked data-type="rules" data-item="${r}">
801
- <div class="agent-toggle-inner">
802
- <span class="agent-toggle-icon">\u{1F4CF}</span>
803
- <span class="agent-toggle-name">${r}.md</span>
804
- <div class="agent-toggle-check">\u2713</div>
805
- </div>
806
- </label>`).join('\n');
807
- const guardCards = s.suggestedGuards.map(g => `<label class="agent-toggle-card mini" data-category="guards">
808
- <input type="checkbox" class="agent-check" checked data-type="guards" data-item="${g}">
809
- <div class="agent-toggle-inner">
810
- <span class="agent-toggle-icon">\u{1F6E1}\uFE0F</span>
811
- <span class="agent-toggle-name">${g}.md</span>
812
- <div class="agent-toggle-check">\u2713</div>
813
- </div>
814
- </label>`).join('\n');
815
- const workflowCards = s.suggestedWorkflows.map(w => `<label class="agent-toggle-card mini" data-category="workflows">
816
- <input type="checkbox" class="agent-check" checked data-type="workflows" data-item="${w}">
817
- <div class="agent-toggle-inner">
818
- <span class="agent-toggle-icon">\u26A1</span>
819
- <span class="agent-toggle-name">${w}.md</span>
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)}
820
950
  <div class="agent-toggle-check">\u2713</div>
821
951
  </div>
822
- </label>`).join('\n');
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');
823
956
  const skillCards = s.suggestedSkills.map(sk => `<label class="agent-toggle-card" data-category="skills">
824
957
  <input type="checkbox" class="agent-check" checked data-type="skills" data-item="${sk.source}">
825
- <div class="agent-toggle-inner">
958
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(sk.status)}">
826
959
  <span class="agent-toggle-icon">\u{1F9E0}</span>
827
960
  <div class="agent-toggle-info">
828
961
  <span class="agent-toggle-name">${sk.name}</span>
829
962
  <span class="agent-toggle-role" style="color:#34d399">${sk.description}</span>
830
963
  </div>
964
+ ${statusBadge(sk.status)}
831
965
  <div class="agent-toggle-check">\u2713</div>
832
966
  </div>
833
967
  </label>`).join('\n');
@@ -852,17 +986,31 @@ function animateCounter(el, target) {
852
986
  `\u{1F527} ${s.stack.primary}`,
853
987
  `\u{1F4E6} ${s.stack.frameworks.length > 0 ? s.stack.frameworks.join(', ') : 'No framework'}`,
854
988
  s.hasExistingAgents ? '\u{1F4C1} Existing .agent/' : '\u{1F4C1} New .agent/',
855
- [s.stack.hasBackend ? '\u{1F519} Backend' : '', s.stack.hasFrontend ? '\u{1F5A5}\uFE0F Frontend' : '', s.stack.hasMobile ? '\u{1F4F1} Mobile' : '', s.stack.hasDatabase ? '\u{1F5C4}\uFE0F DB' : ''].filter(Boolean).join('\n ')
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'] : []),
856
993
  ];
857
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;
858
1000
  return `
859
- <h2 class="section-title">\u{1F916} Agent System (Suggested)</h2>
1001
+ <h2 class="section-title">\u{1F916} Agent System</h2>
860
1002
 
861
1003
  <div class="card agent-system-card">
862
1004
  <div class="agent-stack-banner">
863
1005
  ${stackPills.map(p => `<div class="stack-pill">${p}</div>`).join('\n ')}
864
1006
  </div>
865
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
+
866
1014
  <div class="agent-controls">
867
1015
  <button class="agent-ctrl-btn" onclick="toggleAll(true)">\u2705 Select All</button>
868
1016
  <button class="agent-ctrl-btn" onclick="toggleAll(false)">\u2B1C Select None</button>
@@ -910,18 +1058,22 @@ function animateCounter(el, target) {
910
1058
  <style>
911
1059
  .agent-system-card { padding: 1.5rem; }
912
1060
  .agent-stack-banner { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
913
- .stack-pill { background: #1e293b; border: 1px solid #334155; border-radius: 99px; padding: 0.4rem 1rem; font-size: 0.8rem; color: #94a3b8; white-space: pre-line; }
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; }
914
1066
  .agent-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
915
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; }
916
1068
  .agent-ctrl-btn:hover { background: #334155; }
917
1069
  .agent-count-label { color: #94a3b8; font-size: 0.85rem; margin-left: auto; }
918
1070
  #agentSelectedCount { color: #c084fc; font-weight: 700; }
919
1071
  .agent-section-subtitle { color: #e2e8f0; font-size: 1.05rem; font-weight: 700; margin: 1.25rem 0 0.75rem; }
920
- .agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; }
1072
+ .agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; }
921
1073
  .agent-toggle-card { cursor: pointer; transition: all 0.3s; }
922
1074
  .agent-toggle-card input { display: none; }
923
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; }
924
- .agent-toggle-card input:checked + .agent-toggle-inner { border-color: #818cf8; background: #1e1b4b; }
1076
+ .agent-toggle-card input:checked + .agent-toggle-inner { background: #1e1b4b; }
925
1077
  .agent-toggle-icon { font-size: 1.3rem; flex-shrink: 0; }
926
1078
  .agent-toggle-info { flex: 1; min-width: 0; }
927
1079
  .agent-toggle-name { display: block; color: #e2e8f0; font-weight: 600; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -992,7 +1144,50 @@ function animateCounter(el, target) {
992
1144
  min-height: 100vh;
993
1145
  }
994
1146
 
995
- .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; }
996
1191
 
997
1192
  /* ── Header ── */
998
1193
  .header {
@@ -1071,6 +1266,44 @@ function animateCounter(el, target) {
1071
1266
  display: flex; align-items: center; gap: 0.5rem;
1072
1267
  }
1073
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
+
1074
1307
  /* ── Cards ── */
1075
1308
  .card {
1076
1309
  background: #1e293b; border-radius: 16px; border: 1px solid #334155;
@@ -1080,17 +1313,42 @@ function animateCounter(el, target) {
1080
1313
 
1081
1314
  /* ── Graph ── */
1082
1315
  .graph-card { padding: 1rem; }
1316
+ .graph-controls { margin-bottom: 0.75rem; }
1083
1317
  .graph-legend {
1084
1318
  display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
1085
1319
  justify-content: center;
1086
1320
  }
1087
1321
  .legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: #94a3b8; }
1088
- .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
+ }
1089
1346
  .graph-hint {
1090
1347
  text-align: center; font-size: 0.75rem; color: #475569; margin-top: 0.5rem;
1091
1348
  font-style: italic;
1092
1349
  }
1093
- #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; }
1094
1352
 
1095
1353
  /* ── Layers Grid ── */
1096
1354
  .layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }