@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.
@@ -21,17 +21,62 @@ ${this.getStyles()}
21
21
  </head>
22
22
  <body>
23
23
  ${this.renderHeader(report)}
24
- <div class="container">
25
- ${this.renderScoreHero(report)}
26
- ${this.renderRadarChart(report)}
27
- ${this.renderStats(report)}
28
- ${this.renderLayers(report)}
29
- ${this.renderDependencyGraph(report)}
30
- ${this.renderAntiPatternBubbles(report, grouped)}
31
- ${this.renderAntiPatterns(report, grouped)}
32
- ${this.renderSuggestions(sugGrouped)}
33
- ${plan ? this.renderRefactoringPlan(plan) : ''}
34
- ${agentSuggestion ? this.renderAgentSuggestions(agentSuggestion) : ''}
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>
35
80
  </div>
36
81
  ${this.renderFooter()}
37
82
  ${this.getScripts(report)}
@@ -226,13 +271,21 @@ ${this.getScripts(report)}
226
271
  private renderDependencyGraph(report: AnalysisReport): string {
227
272
  if (report.dependencyGraph.edges.length === 0) return '';
228
273
 
229
- // 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
230
278
  const connectionCount: Record<string, number> = {};
231
279
  for (const edge of report.dependencyGraph.edges) {
232
- connectionCount[edge.from] = (connectionCount[edge.from] || 0) + 1;
233
- 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
+ }
234
286
  }
235
287
 
288
+ // Build layer map from report layers
236
289
  const layerMap: Record<string, string> = {};
237
290
  for (const layer of report.layers) {
238
291
  for (const file of layer.files) {
@@ -240,34 +293,55 @@ ${this.getScripts(report)}
240
293
  }
241
294
  }
242
295
 
243
- const nodes = report.dependencyGraph.nodes.map(n => ({
296
+ // Create nodes only from real files
297
+ const allNodes = [...realFiles].map(n => ({
244
298
  id: n,
245
299
  name: n.split('/').pop() || n,
246
300
  connections: connectionCount[n] || 0,
247
301
  layer: layerMap[n] || 'Other',
248
302
  }));
249
303
 
250
- const links = report.dependencyGraph.edges.map(e => ({
251
- source: e.from,
252
- target: e.to,
253
- }));
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))];
254
319
 
255
320
  return `
256
321
  <h2 class="section-title">🔗 Dependency Graph</h2>
257
322
  <div class="card graph-card">
258
- <div class="graph-legend">
259
- <span class="legend-item"><span class="legend-dot" style="background: #ec4899"></span> API</span>
260
- <span class="legend-item"><span class="legend-dot" style="background: #3b82f6"></span> Service</span>
261
- <span class="legend-item"><span class="legend-dot" style="background: #10b981"></span> Data</span>
262
- <span class="legend-item"><span class="legend-dot" style="background: #f59e0b"></span> UI</span>
263
- <span class="legend-item"><span class="legend-dot" style="background: #8b5cf6"></span> Infra</span>
264
- <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>` : ''}
265
339
  </div>
266
- <div id="dep-graph" style="width:100%; min-height:400px;"></div>
267
- <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>
268
342
  </div>
269
- <script type="application/json" id="graph-nodes">${JSON.stringify(nodes)}<\/script>
270
- <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>`;
271
345
  }
272
346
 
273
347
  /**
@@ -534,10 +608,12 @@ ${this.getScripts(report)}
534
608
  </details>
535
609
  </div>
536
610
  </div>
537
- <div class="rstep-ops">
538
- <h4>📋 Operations (${step.operations.length})</h4>
539
- ${operationsHtml}
540
- </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>
541
617
  <div class="rstep-impact">
542
618
  <h4>📈 Score Impact</h4>
543
619
  <div class="rimpact-tags">${impactHtml}</div>
@@ -566,6 +642,23 @@ document.addEventListener('DOMContentLoaded', () => {
566
642
  }, { threshold: 0.5 });
567
643
 
568
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
+ });
569
662
  });
570
663
 
571
664
  function animateCounter(el, target) {
@@ -668,7 +761,7 @@ function animateCounter(el, target) {
668
761
 
669
762
  const container = document.getElementById('dep-graph');
670
763
  const width = container.clientWidth || 800;
671
- const height = 450;
764
+ const height = 500;
672
765
  container.style.height = height + 'px';
673
766
 
674
767
  const layerColors = {
@@ -680,28 +773,43 @@ function animateCounter(el, target) {
680
773
  .attr('width', width).attr('height', height)
681
774
  .attr('viewBox', [0, 0, width, height]);
682
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
+
683
790
  // Arrow marker
684
- svg.append('defs').append('marker')
791
+ g.append('defs').append('marker')
685
792
  .attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10')
686
793
  .attr('refX', 20).attr('refY', 0).attr('orient', 'auto')
687
794
  .attr('markerWidth', 6).attr('markerHeight', 6)
688
795
  .append('path').attr('d', 'M 0,-5 L 10,0 L 0,5')
689
796
  .attr('fill', '#475569');
690
797
 
798
+ // Tuned simulation for better spread
691
799
  const simulation = d3.forceSimulation(nodes)
692
- .force('link', d3.forceLink(links).id(d => d.id).distance(60))
693
- .force('charge', d3.forceManyBody().strength(-150))
800
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80))
801
+ .force('charge', d3.forceManyBody().strength(-250))
694
802
  .force('center', d3.forceCenter(width / 2, height / 2))
695
- .force('x', d3.forceX(width / 2).strength(0.1))
696
- .force('y', d3.forceY(height / 2).strength(0.1))
697
- .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)));
698
806
 
699
- const link = svg.append('g')
807
+ const link = g.append('g')
700
808
  .selectAll('line').data(links).join('line')
701
- .attr('stroke', '#334155').attr('stroke-width', 1.5)
702
- .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)');
703
811
 
704
- const node = svg.append('g')
812
+ const node = g.append('g')
705
813
  .selectAll('g').data(nodes).join('g')
706
814
  .call(d3.drag()
707
815
  .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
@@ -709,37 +817,55 @@ function animateCounter(el, target) {
709
817
  .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
710
818
  );
711
819
 
712
- // Node circles — size based on connections
820
+ // Node circles — color by layer
713
821
  node.append('circle')
714
- .attr('r', d => Math.max(d.connections * 3 + 6, 8))
822
+ .attr('r', d => Math.max(d.connections * 2.5 + 5, 6))
715
823
  .attr('fill', d => layerColors[d.layer] || '#64748b')
716
- .attr('stroke', '#0f172a').attr('stroke-width', 2)
717
- .attr('opacity', 0.85);
824
+ .attr('stroke', '#0f172a').attr('stroke-width', 1.5)
825
+ .attr('opacity', 0.9);
718
826
 
719
- // Node labels
720
- node.append('text')
827
+ // Node labels — only show for nodes with enough connections
828
+ node.filter(d => d.connections >= 2).append('text')
721
829
  .text(d => d.name.replace(/\\.[^.]+$/, ''))
722
- .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))
723
831
  .attr('text-anchor', 'middle')
724
- .attr('fill', '#94a3b8').attr('font-size', '10px').attr('font-weight', '500');
832
+ .attr('fill', '#e2e8f0').attr('font-size', '9px').attr('font-weight', '500');
725
833
 
726
- // Tooltip on hover
834
+ // Tooltip
727
835
  node.append('title')
728
836
  .text(d => d.id + '\\nConnections: ' + d.connections + '\\nLayer: ' + d.layer);
729
837
 
730
838
  simulation.on('tick', () => {
731
- // Clamp nodes to stay within SVG bounds
732
- nodes.forEach(d => {
733
- const r = Math.max(d.connections * 3 + 6, 8) + 10;
734
- d.x = Math.max(r, Math.min(width - r, d.x));
735
- d.y = Math.max(r, Math.min(height - r, d.y));
736
- });
737
-
738
839
  link
739
840
  .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
740
841
  .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
741
842
  node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
742
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
+ };
743
869
  })();
744
870
 
745
871
  // ── Bubble Chart ──
@@ -836,62 +962,69 @@ function animateCounter(el, target) {
836
962
  return '#60a5fa';
837
963
  };
838
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
+
839
984
  const agentCards = s.suggestedAgents.map(a =>
840
- `<label class="agent-toggle-card" data-category="agents" data-name="${a}">
841
- <input type="checkbox" class="agent-check" checked data-type="agents" data-item="${a}">
842
- <div class="agent-toggle-inner">
843
- <div class="agent-toggle-icon">${roleIcon(a)}</div>
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>
844
989
  <div class="agent-toggle-info">
845
- <span class="agent-toggle-name">${a}</span>
846
- <span class="agent-toggle-role" style="color:${roleColor(a)}">${roleLabel(a)}</span>
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>` : ''}
847
993
  </div>
994
+ ${statusBadge(a.status)}
848
995
  <div class="agent-toggle-check">\u2713</div>
849
996
  </div>
850
997
  </label>`
851
998
  ).join('\n');
852
999
 
853
- const ruleCards = s.suggestedRules.map(r =>
854
- `<label class="agent-toggle-card mini" data-category="rules">
855
- <input type="checkbox" class="agent-check" checked data-type="rules" data-item="${r}">
856
- <div class="agent-toggle-inner">
857
- <span class="agent-toggle-icon">\u{1F4CF}</span>
858
- <span class="agent-toggle-name">${r}.md</span>
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)}
859
1010
  <div class="agent-toggle-check">\u2713</div>
860
1011
  </div>
861
- </label>`
862
- ).join('\n');
1012
+ </label>`;
863
1013
 
864
- const guardCards = s.suggestedGuards.map(g =>
865
- `<label class="agent-toggle-card mini" data-category="guards">
866
- <input type="checkbox" class="agent-check" checked data-type="guards" data-item="${g}">
867
- <div class="agent-toggle-inner">
868
- <span class="agent-toggle-icon">\u{1F6E1}\uFE0F</span>
869
- <span class="agent-toggle-name">${g}.md</span>
870
- <div class="agent-toggle-check">\u2713</div>
871
- </div>
872
- </label>`
873
- ).join('\n');
874
-
875
- const workflowCards = s.suggestedWorkflows.map(w =>
876
- `<label class="agent-toggle-card mini" data-category="workflows">
877
- <input type="checkbox" class="agent-check" checked data-type="workflows" data-item="${w}">
878
- <div class="agent-toggle-inner">
879
- <span class="agent-toggle-icon">\u26A1</span>
880
- <span class="agent-toggle-name">${w}.md</span>
881
- <div class="agent-toggle-check">\u2713</div>
882
- </div>
883
- </label>`
884
- ).join('\n');
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');
885
1017
 
886
1018
  const skillCards = s.suggestedSkills.map(sk =>
887
1019
  `<label class="agent-toggle-card" data-category="skills">
888
1020
  <input type="checkbox" class="agent-check" checked data-type="skills" data-item="${sk.source}">
889
- <div class="agent-toggle-inner">
1021
+ <div class="agent-toggle-inner" style="border-color:${statusBorder(sk.status)}">
890
1022
  <span class="agent-toggle-icon">\u{1F9E0}</span>
891
1023
  <div class="agent-toggle-info">
892
1024
  <span class="agent-toggle-name">${sk.name}</span>
893
1025
  <span class="agent-toggle-role" style="color:#34d399">${sk.description}</span>
894
1026
  </div>
1027
+ ${statusBadge(sk.status)}
895
1028
  <div class="agent-toggle-check">\u2713</div>
896
1029
  </div>
897
1030
  </label>`
@@ -919,19 +1052,34 @@ function animateCounter(el, target) {
919
1052
  `\u{1F527} ${s.stack.primary}`,
920
1053
  `\u{1F4E6} ${s.stack.frameworks.length > 0 ? s.stack.frameworks.join(', ') : 'No framework'}`,
921
1054
  s.hasExistingAgents ? '\u{1F4C1} Existing .agent/' : '\u{1F4C1} New .agent/',
922
- [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 ')
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'] : []),
923
1059
  ];
924
1060
 
925
1061
  const totalItems = s.suggestedAgents.length + s.suggestedRules.length + s.suggestedGuards.length + s.suggestedWorkflows.length + s.suggestedSkills.length;
926
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
+
927
1069
  return `
928
- <h2 class="section-title">\u{1F916} Agent System (Suggested)</h2>
1070
+ <h2 class="section-title">\u{1F916} Agent System</h2>
929
1071
 
930
1072
  <div class="card agent-system-card">
931
1073
  <div class="agent-stack-banner">
932
1074
  ${stackPills.map(p => `<div class="stack-pill">${p}</div>`).join('\n ')}
933
1075
  </div>
934
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
+
935
1083
  <div class="agent-controls">
936
1084
  <button class="agent-ctrl-btn" onclick="toggleAll(true)">\u2705 Select All</button>
937
1085
  <button class="agent-ctrl-btn" onclick="toggleAll(false)">\u2B1C Select None</button>
@@ -979,18 +1127,22 @@ function animateCounter(el, target) {
979
1127
  <style>
980
1128
  .agent-system-card { padding: 1.5rem; }
981
1129
  .agent-stack-banner { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
982
- .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; }
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; }
983
1135
  .agent-controls { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
984
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; }
985
1137
  .agent-ctrl-btn:hover { background: #334155; }
986
1138
  .agent-count-label { color: #94a3b8; font-size: 0.85rem; margin-left: auto; }
987
1139
  #agentSelectedCount { color: #c084fc; font-weight: 700; }
988
1140
  .agent-section-subtitle { color: #e2e8f0; font-size: 1.05rem; font-weight: 700; margin: 1.25rem 0 0.75rem; }
989
- .agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.75rem; }
1141
+ .agent-toggle-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; }
990
1142
  .agent-toggle-card { cursor: pointer; transition: all 0.3s; }
991
1143
  .agent-toggle-card input { display: none; }
992
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; }
993
- .agent-toggle-card input:checked + .agent-toggle-inner { border-color: #818cf8; background: #1e1b4b; }
1145
+ .agent-toggle-card input:checked + .agent-toggle-inner { background: #1e1b4b; }
994
1146
  .agent-toggle-icon { font-size: 1.3rem; flex-shrink: 0; }
995
1147
  .agent-toggle-info { flex: 1; min-width: 0; }
996
1148
  .agent-toggle-name { display: block; color: #e2e8f0; font-weight: 600; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -1062,7 +1214,50 @@ function animateCounter(el, target) {
1062
1214
  min-height: 100vh;
1063
1215
  }
1064
1216
 
1065
- .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; }
1066
1261
 
1067
1262
  /* ── Header ── */
1068
1263
  .header {
@@ -1141,6 +1336,44 @@ function animateCounter(el, target) {
1141
1336
  display: flex; align-items: center; gap: 0.5rem;
1142
1337
  }
1143
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
+
1144
1377
  /* ── Cards ── */
1145
1378
  .card {
1146
1379
  background: #1e293b; border-radius: 16px; border: 1px solid #334155;
@@ -1150,17 +1383,42 @@ function animateCounter(el, target) {
1150
1383
 
1151
1384
  /* ── Graph ── */
1152
1385
  .graph-card { padding: 1rem; }
1386
+ .graph-controls { margin-bottom: 0.75rem; }
1153
1387
  .graph-legend {
1154
1388
  display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem;
1155
1389
  justify-content: center;
1156
1390
  }
1157
1391
  .legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.75rem; color: #94a3b8; }
1158
- .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
+ }
1159
1416
  .graph-hint {
1160
1417
  text-align: center; font-size: 0.75rem; color: #475569; margin-top: 0.5rem;
1161
1418
  font-style: italic;
1162
1419
  }
1163
- #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; }
1164
1422
 
1165
1423
  /* ── Layers Grid ── */
1166
1424
  .layers-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; }