mcp-vector-search 1.0.3__py3-none-any.whl → 1.1.22__py3-none-any.whl

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 (63) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +48 -1
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +35 -0
  7. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  8. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  9. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  10. mcp_vector_search/analysis/collectors/smells.py +325 -0
  11. mcp_vector_search/analysis/debt.py +516 -0
  12. mcp_vector_search/analysis/interpretation.py +685 -0
  13. mcp_vector_search/analysis/metrics.py +74 -1
  14. mcp_vector_search/analysis/reporters/__init__.py +3 -1
  15. mcp_vector_search/analysis/reporters/console.py +424 -0
  16. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  17. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  18. mcp_vector_search/analysis/storage/__init__.py +93 -0
  19. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  20. mcp_vector_search/analysis/storage/schema.py +245 -0
  21. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  22. mcp_vector_search/analysis/trends.py +308 -0
  23. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  24. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  25. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  26. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  27. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  28. mcp_vector_search/cli/commands/analyze.py +665 -11
  29. mcp_vector_search/cli/commands/chat.py +193 -0
  30. mcp_vector_search/cli/commands/index.py +600 -2
  31. mcp_vector_search/cli/commands/index_background.py +467 -0
  32. mcp_vector_search/cli/commands/search.py +194 -1
  33. mcp_vector_search/cli/commands/setup.py +64 -13
  34. mcp_vector_search/cli/commands/status.py +302 -3
  35. mcp_vector_search/cli/commands/visualize/cli.py +26 -10
  36. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
  37. mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
  38. mcp_vector_search/cli/commands/visualize/server.py +304 -15
  39. mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
  40. mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
  41. mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
  42. mcp_vector_search/cli/didyoumean.py +5 -0
  43. mcp_vector_search/cli/main.py +16 -5
  44. mcp_vector_search/cli/output.py +134 -5
  45. mcp_vector_search/config/thresholds.py +89 -1
  46. mcp_vector_search/core/__init__.py +16 -0
  47. mcp_vector_search/core/database.py +39 -2
  48. mcp_vector_search/core/embeddings.py +24 -0
  49. mcp_vector_search/core/git.py +380 -0
  50. mcp_vector_search/core/indexer.py +445 -84
  51. mcp_vector_search/core/llm_client.py +9 -4
  52. mcp_vector_search/core/models.py +88 -1
  53. mcp_vector_search/core/relationships.py +473 -0
  54. mcp_vector_search/core/search.py +1 -1
  55. mcp_vector_search/mcp/server.py +795 -4
  56. mcp_vector_search/parsers/python.py +285 -5
  57. mcp_vector_search/utils/gitignore.py +0 -3
  58. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
  59. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
  60. mcp_vector_search/cli/commands/visualize.py.original +0 -2536
  61. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
  62. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
  63. {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
@@ -50,6 +50,9 @@ let navigationIndex = -1;
50
50
  // Call lines visibility
51
51
  let showCallLines = true;
52
52
 
53
+ // File filter: 'all', 'code', 'docs'
54
+ let currentFileFilter = 'all';
55
+
53
56
  // Chunk types for code nodes (function, class, method, text, imports, module)
54
57
  const chunkTypes = ['function', 'class', 'method', 'text', 'imports', 'module'];
55
58
 
@@ -57,8 +60,8 @@ const chunkTypes = ['function', 'class', 'method', 'text', 'imports', 'module'];
57
60
  const sizeConfig = {
58
61
  minRadius: 12, // Minimum node radius (50% larger for readability)
59
62
  maxRadius: 24, // Maximum node radius
60
- chunkMinRadius: 9, // Minimum for chunks (50% larger for readability)
61
- chunkMaxRadius: 16 // Maximum for chunks
63
+ chunkMinRadius: 5, // Minimum for small chunks (more visible size contrast)
64
+ chunkMaxRadius: 28 // Maximum for large chunks (more visible size contrast)
62
65
  };
63
66
 
64
67
  // Dynamic dimensions that update when viewer opens/closes
@@ -76,13 +79,133 @@ const margin = {top: 40, right: 120, bottom: 20, left: 120};
76
79
  // DATA LOADING
77
80
  // ============================================================================
78
81
 
82
+ let graphStatusCheckInterval = null;
83
+
84
+ async function checkGraphStatus() {
85
+ try {
86
+ const response = await fetch('/api/graph-status');
87
+ const status = await response.json();
88
+ return status;
89
+ } catch (error) {
90
+ console.error('Failed to check graph status:', error);
91
+ return { ready: false, size: 0 };
92
+ }
93
+ }
94
+
95
+ function showLoadingIndicator(message) {
96
+ const loadingDiv = document.getElementById('graph-loading-indicator') || createLoadingDiv();
97
+ loadingDiv.querySelector('.loading-message').textContent = message;
98
+ loadingDiv.style.display = 'flex';
99
+ }
100
+
101
+ function hideLoadingIndicator() {
102
+ const loadingDiv = document.getElementById('graph-loading-indicator');
103
+ if (loadingDiv) {
104
+ loadingDiv.style.display = 'none';
105
+ }
106
+ }
107
+
108
+ function createLoadingDiv() {
109
+ const div = document.createElement('div');
110
+ div.id = 'graph-loading-indicator';
111
+ div.style.cssText = `
112
+ position: fixed;
113
+ top: 50%;
114
+ left: 50%;
115
+ transform: translate(-50%, -50%);
116
+ background: rgba(255, 255, 255, 0.95);
117
+ padding: 30px 50px;
118
+ border-radius: 8px;
119
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: center;
123
+ gap: 15px;
124
+ z-index: 10000;
125
+ `;
126
+
127
+ const spinner = document.createElement('div');
128
+ spinner.style.cssText = `
129
+ border: 4px solid #f3f3f3;
130
+ border-top: 4px solid #3498db;
131
+ border-radius: 50%;
132
+ width: 40px;
133
+ height: 40px;
134
+ animation: spin 1s linear infinite;
135
+ `;
136
+
137
+ const message = document.createElement('div');
138
+ message.className = 'loading-message';
139
+ message.style.cssText = 'color: #333; font-size: 16px; font-family: Arial, sans-serif;';
140
+ message.textContent = 'Loading graph data...';
141
+
142
+ div.appendChild(spinner);
143
+ div.appendChild(message);
144
+ document.body.appendChild(div);
145
+
146
+ // Add spinner animation
147
+ const style = document.createElement('style');
148
+ style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
149
+ document.head.appendChild(style);
150
+
151
+ return div;
152
+ }
153
+
79
154
  async function loadGraphData() {
155
+ try {
156
+ // Check if graph is ready
157
+ const status = await checkGraphStatus();
158
+
159
+ if (!status.ready) {
160
+ console.log('Graph data not ready yet, will poll every 5 seconds...');
161
+ showLoadingIndicator('Generating graph data... This may take a few minutes.');
162
+
163
+ // Start polling for graph readiness
164
+ graphStatusCheckInterval = setInterval(async () => {
165
+ const checkStatus = await checkGraphStatus();
166
+ if (checkStatus.ready) {
167
+ clearInterval(graphStatusCheckInterval);
168
+ console.log('Graph data is now ready, loading...');
169
+ showLoadingIndicator('Graph data ready! Loading visualization...');
170
+ await loadGraphDataActual();
171
+ hideLoadingIndicator();
172
+ }
173
+ }, 5000); // Poll every 5 seconds
174
+
175
+ return;
176
+ }
177
+
178
+ // Graph is already ready, load it
179
+ await loadGraphDataActual();
180
+ } catch (error) {
181
+ console.error('Failed to load graph data:', error);
182
+ hideLoadingIndicator();
183
+ document.body.innerHTML =
184
+ '<div style="color: red; padding: 20px; font-family: Arial;">Error loading visualization data. Check console for details.</div>';
185
+ }
186
+ }
187
+
188
+ async function loadGraphDataActual() {
80
189
  try {
81
190
  const response = await fetch('/api/graph');
82
191
  const data = await response.json();
192
+
193
+ // Check if we got an error response
194
+ if (data.error) {
195
+ console.warn('Graph data not available yet:', data.error);
196
+ showLoadingIndicator('Waiting for graph data...');
197
+ return;
198
+ }
199
+
83
200
  allNodes = data.nodes || [];
84
201
  allLinks = data.links || [];
85
202
 
203
+ // Store trend data globally for visualization
204
+ window.graphTrendData = data.trends || null;
205
+ if (window.graphTrendData) {
206
+ console.log(`Loaded trend data: ${window.graphTrendData.entries_count} entries`);
207
+ }
208
+
86
209
  console.log(`Loaded ${allNodes.length} nodes and ${allLinks.length} links`);
87
210
 
88
211
  // DEBUG: Log first few nodes to see actual structure
@@ -180,14 +303,14 @@ function buildTreeStructure() {
180
303
  dir_hierarchy: 0,
181
304
  dir_containment: 0,
182
305
  file_containment: 0,
183
- chunk_containment: 0 // undefined links = chunk-to-chunk (class -> method)
306
+ chunk_hierarchy: 0 // chunk_hierarchy links = class -> method
184
307
  };
185
308
 
186
309
  let relationshipsMatched = {
187
310
  dir_hierarchy: 0,
188
311
  dir_containment: 0,
189
312
  file_containment: 0,
190
- chunk_containment: 0
313
+ chunk_hierarchy: 0
191
314
  };
192
315
 
193
316
  // Process all relationship links
@@ -202,11 +325,12 @@ function buildTreeStructure() {
202
325
  category = 'dir_containment';
203
326
  } else if (linkType === 'file_containment') {
204
327
  category = 'file_containment';
205
- } else if (linkType === undefined || linkType === 'undefined') {
206
- // Undefined links are chunk-to-chunk (e.g., class -> method)
207
- category = 'chunk_containment';
328
+ } else if (linkType === 'chunk_hierarchy') {
329
+ // chunk_hierarchy links are chunk-to-chunk (e.g., class -> method)
330
+ category = 'chunk_hierarchy';
208
331
  } else {
209
- // Skip semantic, caller, and other non-hierarchical links
332
+ // Skip semantic, caller, undefined, and other non-hierarchical links
333
+ // This includes links without a 'type' field (e.g., subproject links)
210
334
  return;
211
335
  }
212
336
 
@@ -238,7 +362,7 @@ function buildTreeStructure() {
238
362
  console.log(` dir_hierarchy: ${relationshipsMatched.dir_hierarchy}/${relationshipsProcessed.dir_hierarchy} matched`);
239
363
  console.log(` dir_containment: ${relationshipsMatched.dir_containment}/${relationshipsProcessed.dir_containment} matched`);
240
364
  console.log(` file_containment: ${relationshipsMatched.file_containment}/${relationshipsProcessed.file_containment} matched`);
241
- console.log(` chunk_containment: ${relationshipsMatched.chunk_containment}/${relationshipsProcessed.chunk_containment} matched`);
365
+ console.log(` chunk_hierarchy (class→method): ${relationshipsMatched.chunk_hierarchy}/${relationshipsProcessed.chunk_hierarchy} matched`);
242
366
  console.log(` Total parent-child links: ${parentMap.size}`);
243
367
  console.log('=== END TREE RELATIONSHIPS ===');
244
368
 
@@ -330,11 +454,18 @@ function buildTreeStructure() {
330
454
  console.log(`Promoting ${chunkChildren.length} children from ${onlyChild.type} to file ${node.name}`);
331
455
  // Replace the single chunk with its children
332
456
  node.children = chunkChildren;
333
- // Store info about the collapsed chunk
457
+ // Store info about the collapsed chunk (include ALL relevant properties)
334
458
  node.collapsed_chunk = {
335
459
  type: onlyChild.type,
336
460
  name: onlyChild.name,
337
- id: onlyChild.id
461
+ id: onlyChild.id,
462
+ content: onlyChild.content,
463
+ docstring: onlyChild.docstring,
464
+ start_line: onlyChild.start_line,
465
+ end_line: onlyChild.end_line,
466
+ file_path: onlyChild.file_path,
467
+ language: onlyChild.language,
468
+ complexity: onlyChild.complexity
338
469
  };
339
470
  } else {
340
471
  // Collapse file+chunk into combined name (like directory chains)
@@ -379,18 +510,10 @@ function buildTreeStructure() {
379
510
  }
380
511
  }
381
512
 
382
- // Collapse all child nodes of the root (but keep root's direct children visible initially)
383
- // This way, only the root level (first level) is visible, all deeper levels are collapsed
513
+ // Collapse ALL nodes except the root itself
514
+ // This ensures only the root node is visible initially, all children are collapsed
384
515
  if (treeData.children) {
385
- treeData.children.forEach(child => {
386
- // Collapse all descendants of each root child, but keep the root children themselves visible
387
- if (child.children && child.children.length > 0) {
388
- child.children.forEach(grandchild => collapseAll(grandchild));
389
- // Move children to _children to collapse
390
- child._children = child.children;
391
- child.children = null;
392
- }
393
- });
516
+ treeData.children.forEach(child => collapseAll(child));
394
517
  }
395
518
 
396
519
  console.log('Tree structure built with all directories and files collapsed');
@@ -438,11 +561,18 @@ let allLineCounts = []; // Collect all line counts for percentile calculation
438
561
  function calculateNodeSizes(node) {
439
562
  if (!node) return 0;
440
563
 
441
- // For chunks: use line count directly
564
+ // For chunks: use actual line count (primary metric)
565
+ // Falls back to content-based estimate only if line numbers unavailable
442
566
  if (chunkTypes.includes(node.type)) {
567
+ // Primary: use actual line span from start_line/end_line
568
+ // This ensures visual correlation with displayed line ranges
569
+ const contentLength = node.content ? node.content.length : 0;
443
570
  const lineCount = (node.start_line && node.end_line)
444
571
  ? node.end_line - node.start_line + 1
445
- : 1;
572
+ : Math.max(1, Math.floor(contentLength / 40)); // Fallback: estimate ~40 chars per line
573
+
574
+ // Use actual line count for sizing (NOT content length)
575
+ // This prevents inversion where sparse 101-line files appear smaller than dense 3-line files
446
576
  node._lineCount = lineCount;
447
577
  allLineCounts.push(lineCount);
448
578
 
@@ -461,6 +591,16 @@ function calculateNodeSizes(node) {
461
591
  totalLines += calculateNodeSizes(child);
462
592
  });
463
593
 
594
+ // Handle collapsed file+chunk: use the collapsed chunk's line count
595
+ if (node.type === 'file' && node.collapsed_chunk) {
596
+ const cc = node.collapsed_chunk;
597
+ if (cc.start_line && cc.end_line) {
598
+ totalLines = cc.end_line - cc.start_line + 1;
599
+ } else if (cc.content) {
600
+ totalLines = Math.max(1, Math.floor(cc.content.length / 40));
601
+ }
602
+ }
603
+
464
604
  node._lineCount = totalLines || 1; // Minimum 1 for empty dirs/files
465
605
 
466
606
  // DON'T add files/directories to allLineCounts - they skew percentiles
@@ -905,43 +1045,145 @@ function calculateAverageComplexity(node) {
905
1045
  return count > 0 ? totalComplexity / count : 0;
906
1046
  }
907
1047
 
908
- function getNodeRadius(d) {
1048
+ // Get stroke color based on complexity - red outline for high complexity
1049
+ function getNodeStrokeColor(d) {
909
1050
  const nodeData = d.data;
910
- const lineCount = nodeData._lineCount || 1;
911
1051
 
912
- // Determine min/max based on node type
913
- let minR, maxR;
1052
+ // Only chunks have direct complexity
914
1053
  if (chunkTypes.includes(nodeData.type)) {
915
- minR = sizeConfig.chunkMinRadius;
916
- maxR = sizeConfig.chunkMaxRadius;
917
- } else {
918
- minR = sizeConfig.minRadius;
919
- maxR = sizeConfig.maxRadius;
1054
+ const complexity = nodeData.complexity || 0;
1055
+ // Complexity thresholds:
1056
+ // 0-5: white (simple)
1057
+ // 5-10: orange (moderate)
1058
+ // 10+: red (complex)
1059
+ if (complexity >= 10) {
1060
+ return '#e74c3c'; // Red for high complexity
1061
+ } else if (complexity >= 5) {
1062
+ return '#f39c12'; // Orange for moderate
1063
+ }
1064
+ return '#fff'; // White for low complexity
1065
+ }
1066
+
1067
+ // Files and directories: check average complexity of children
1068
+ if (nodeData.type === 'file' || nodeData.type === 'directory') {
1069
+ const avgComplexity = calculateAverageComplexity(nodeData);
1070
+ if (avgComplexity >= 10) {
1071
+ return '#e74c3c'; // Red
1072
+ } else if (avgComplexity >= 5) {
1073
+ return '#f39c12'; // Orange
1074
+ }
1075
+ }
1076
+
1077
+ return '#fff'; // Default white
1078
+ }
1079
+
1080
+ // Get stroke width based on complexity - thicker for high complexity
1081
+ function getNodeStrokeWidth(d) {
1082
+ const nodeData = d.data;
1083
+ const complexity = chunkTypes.includes(nodeData.type)
1084
+ ? (nodeData.complexity || 0)
1085
+ : calculateAverageComplexity(nodeData);
1086
+
1087
+ if (complexity >= 10) return 3; // Thick red outline
1088
+ if (complexity >= 5) return 2.5; // Medium orange outline
1089
+ return 2; // Default
1090
+ }
1091
+
1092
+ function getNodeRadius(d) {
1093
+ const nodeData = d.data;
1094
+
1095
+ // Size configuration based on node type
1096
+ const dirMinRadius = 8; // Min for directories
1097
+ const dirMaxRadius = 40; // Max for directories
1098
+ const fileMinRadius = 6; // Min for files
1099
+ const fileMaxRadius = 30; // Max for files
1100
+ const chunkMinRadius = sizeConfig.chunkMinRadius; // From config
1101
+ const chunkMaxRadius = sizeConfig.chunkMaxRadius; // From config
1102
+
1103
+ // Directory nodes: size by file_count (logarithmic scale)
1104
+ if (nodeData.type === 'directory') {
1105
+ const fileCount = nodeData.file_count || 0;
1106
+ if (fileCount === 0) return dirMinRadius;
1107
+
1108
+ // Logarithmic scale: log2(fileCount + 1) for smooth scaling
1109
+ // +1 to avoid log(0), gives range [1, log2(max_files+1)]
1110
+ const logCount = Math.log2(fileCount + 1);
1111
+ const maxLogCount = Math.log2(100 + 1); // Assume max ~100 files per dir
1112
+ const normalized = Math.min(logCount / maxLogCount, 1.0);
1113
+
1114
+ return dirMinRadius + (normalized * (dirMaxRadius - dirMinRadius));
920
1115
  }
921
1116
 
922
- // Percentile-based relative sizing:
923
- // - Below 20th percentile → minimum size
924
- // - Above 80th percentile → maximum size
925
- // - Between 20th-80th → linear interpolation
1117
+ // File nodes: size by total lines of code (sum of all chunks)
1118
+ if (nodeData.type === 'file') {
1119
+ const lineCount = nodeData._lineCount || 1;
1120
+
1121
+ // Collapsed file+chunk: use chunk sizing (since it's really a chunk)
1122
+ if (nodeData.collapsed_chunk) {
1123
+ const minLines = 5;
1124
+ const maxLines = 150;
1125
+
1126
+ let normalized;
1127
+ if (lineCount <= minLines) {
1128
+ normalized = 0;
1129
+ } else if (lineCount >= maxLines) {
1130
+ normalized = 1;
1131
+ } else {
1132
+ const logMin = Math.log(minLines);
1133
+ const logMax = Math.log(maxLines);
1134
+ const logCount = Math.log(lineCount);
1135
+ normalized = (logCount - logMin) / (logMax - logMin);
1136
+ }
1137
+ return chunkMinRadius + (normalized * (chunkMaxRadius - chunkMinRadius));
1138
+ }
1139
+
1140
+ // Regular files: linear scaling based on total lines
1141
+ const minFileLines = 5;
1142
+ const maxFileLines = 300;
1143
+
1144
+ let normalized;
1145
+ if (lineCount <= minFileLines) {
1146
+ normalized = 0;
1147
+ } else if (lineCount >= maxFileLines) {
1148
+ normalized = 1;
1149
+ } else {
1150
+ normalized = (lineCount - minFileLines) / (maxFileLines - minFileLines);
1151
+ }
926
1152
 
927
- if (percentile80 <= percentile20) {
928
- return (minR + maxR) / 2; // Default if no range
1153
+ return fileMinRadius + (normalized * (fileMaxRadius - fileMinRadius));
929
1154
  }
930
1155
 
931
- let normalized;
932
- if (lineCount <= percentile20) {
933
- // Below 20th percentile - use minimum size
934
- normalized = 0;
935
- } else if (lineCount >= percentile80) {
936
- // Above 80th percentile - use maximum size
937
- normalized = 1;
938
- } else {
939
- // Linear interpolation between 20th and 80th percentile
940
- normalized = (lineCount - percentile20) / (percentile80 - percentile20);
1156
+ // Chunk nodes: size by lines of code (absolute thresholds, not percentiles)
1157
+ // This ensures 330-line functions are ALWAYS big, regardless of codebase distribution
1158
+ if (chunkTypes.includes(nodeData.type)) {
1159
+ const lineCount = nodeData._lineCount || 1;
1160
+
1161
+ // Absolute thresholds for intuitive sizing:
1162
+ // - 1-10 lines: small (imports, constants, simple functions)
1163
+ // - 10-50 lines: medium (typical functions)
1164
+ // - 50-150 lines: large (complex functions)
1165
+ // - 150+ lines: maximum (very large functions/classes)
1166
+ const minLines = 5;
1167
+ const maxLines = 150; // Anything over 150 lines gets max size
1168
+
1169
+ let normalized;
1170
+ if (lineCount <= minLines) {
1171
+ normalized = 0;
1172
+ } else if (lineCount >= maxLines) {
1173
+ normalized = 1;
1174
+ } else {
1175
+ // Logarithmic scaling for better visual distribution
1176
+ const logMin = Math.log(minLines);
1177
+ const logMax = Math.log(maxLines);
1178
+ const logCount = Math.log(lineCount);
1179
+ normalized = (logCount - logMin) / (logMax - logMin);
1180
+ }
1181
+
1182
+ return chunkMinRadius + (normalized * (chunkMaxRadius - chunkMinRadius));
941
1183
  }
942
1184
 
943
- // Scale to radius range
944
- return minR + (normalized * (maxR - minR));
1185
+ // Default fallback for other node types
1186
+ return sizeConfig.minRadius;
945
1187
  }
946
1188
 
947
1189
  // ============================================================================
@@ -997,9 +1239,62 @@ function renderLinearTree() {
997
1239
  // Create hierarchy from tree data
998
1240
  // D3 hierarchy automatically respects children vs _children
999
1241
  console.log('Creating D3 hierarchy...');
1242
+
1243
+ // DEBUG: Check if treeData children have content property BEFORE D3 processes them
1244
+ console.log('=== PRE-D3 HIERARCHY DEBUG ===');
1245
+ if (treeData.children && treeData.children.length > 0) {
1246
+ const firstChild = treeData.children[0];
1247
+ console.log('First root child:', firstChild.name, 'type:', firstChild.type);
1248
+ console.log('First child keys:', Object.keys(firstChild));
1249
+ console.log('First child has content:', 'content' in firstChild);
1250
+
1251
+ // Find a chunk node in the tree
1252
+ function findFirstChunk(node) {
1253
+ if (chunkTypes.includes(node.type)) {
1254
+ return node;
1255
+ }
1256
+ if (node.children) {
1257
+ for (const child of node.children) {
1258
+ const found = findFirstChunk(child);
1259
+ if (found) return found;
1260
+ }
1261
+ }
1262
+ if (node._children) {
1263
+ for (const child of node._children) {
1264
+ const found = findFirstChunk(child);
1265
+ if (found) return found;
1266
+ }
1267
+ }
1268
+ return null;
1269
+ }
1270
+
1271
+ const sampleChunk = findFirstChunk(treeData);
1272
+ if (sampleChunk) {
1273
+ console.log('Sample chunk node BEFORE D3:', sampleChunk.name, 'type:', sampleChunk.type);
1274
+ console.log('Sample chunk keys:', Object.keys(sampleChunk));
1275
+ console.log('Sample chunk has content:', 'content' in sampleChunk);
1276
+ console.log('Sample chunk content length:', sampleChunk.content ? sampleChunk.content.length : 0);
1277
+ }
1278
+ }
1279
+ console.log('=== END PRE-D3 HIERARCHY DEBUG ===');
1280
+
1000
1281
  const root = d3.hierarchy(treeData, d => d.children);
1001
1282
  console.log(`Hierarchy created: ${root.descendants().length} nodes`);
1002
1283
 
1284
+ // DEBUG: Check if content is preserved AFTER D3 processes them
1285
+ console.log('=== POST-D3 HIERARCHY DEBUG ===');
1286
+ const debugDescendants = root.descendants();
1287
+ const chunkDescendants = debugDescendants.filter(d => chunkTypes.includes(d.data.type));
1288
+ console.log(`Found ${chunkDescendants.length} chunk nodes in D3 hierarchy`);
1289
+ if (chunkDescendants.length > 0) {
1290
+ const firstChunkD3 = chunkDescendants[0];
1291
+ console.log('First chunk in D3 hierarchy:', firstChunkD3.data.name, 'type:', firstChunkD3.data.type);
1292
+ console.log('First chunk d.data keys:', Object.keys(firstChunkD3.data));
1293
+ console.log('First chunk has content in d.data:', 'content' in firstChunkD3.data);
1294
+ console.log('First chunk content length:', firstChunkD3.data.content ? firstChunkD3.data.content.length : 0);
1295
+ }
1296
+ console.log('=== END POST-D3 HIERARCHY DEBUG ===');
1297
+
1003
1298
  // Apply tree layout
1004
1299
  console.log('Applying tree layout...');
1005
1300
  treeLayout(root);
@@ -1047,8 +1342,17 @@ function renderLinearTree() {
1047
1342
  nodes.append('circle')
1048
1343
  .attr('r', d => getNodeRadius(d)) // Dynamic size based on content
1049
1344
  .attr('fill', d => getNodeFillColor(d)) // Complexity-based coloring
1050
- .attr('stroke', '#fff')
1051
- .attr('stroke-width', 2);
1345
+ .attr('stroke', d => getNodeStrokeColor(d)) // Red/orange for high complexity
1346
+ .attr('stroke-width', d => getNodeStrokeWidth(d))
1347
+ .attr('class', d => {
1348
+ // Add complexity grade class if available
1349
+ const grade = d.data.complexity_grade || '';
1350
+ const hasSmells = (d.data.smell_count && d.data.smell_count > 0) || (d.data.smells && d.data.smells.length > 0);
1351
+ const classes = [];
1352
+ if (grade) classes.push(`grade-${grade}`);
1353
+ if (hasSmells) classes.push('has-smells');
1354
+ return classes.join(' ');
1355
+ });
1052
1356
 
1053
1357
  // Add external call arrow indicators (only for chunk nodes)
1054
1358
  nodes.each(function(d) {
@@ -1187,8 +1491,17 @@ function renderCircularTree() {
1187
1491
  nodes.append('circle')
1188
1492
  .attr('r', d => getNodeRadius(d)) // Dynamic size based on content
1189
1493
  .attr('fill', d => getNodeFillColor(d)) // Complexity-based coloring
1190
- .attr('stroke', '#fff')
1191
- .attr('stroke-width', 2);
1494
+ .attr('stroke', d => getNodeStrokeColor(d)) // Red/orange for high complexity
1495
+ .attr('stroke-width', d => getNodeStrokeWidth(d))
1496
+ .attr('class', d => {
1497
+ // Add complexity grade class if available
1498
+ const grade = d.data.complexity_grade || '';
1499
+ const hasSmells = (d.data.smell_count && d.data.smell_count > 0) || (d.data.smells && d.data.smells.length > 0);
1500
+ const classes = [];
1501
+ if (grade) classes.push(`grade-${grade}`);
1502
+ if (hasSmells) classes.push('has-smells');
1503
+ return classes.join(' ');
1504
+ });
1192
1505
 
1193
1506
  // Add external call arrow indicators (only for chunk nodes)
1194
1507
  nodes.each(function(d) {
@@ -1523,6 +1836,11 @@ function displayFileInfo(fileData, addToHistory = true) {
1523
1836
  function displayChunkContent(chunkData, addToHistory = true) {
1524
1837
  openViewerPanel();
1525
1838
 
1839
+ // Expand path to node and highlight it in tree
1840
+ if (chunkData.id) {
1841
+ expandAndHighlightNode(chunkData.id);
1842
+ }
1843
+
1526
1844
  // Add to navigation history
1527
1845
  if (addToHistory) {
1528
1846
  addToNavHistory({type: 'chunk', data: chunkData});
@@ -1861,19 +2179,70 @@ function findPathToNode(node, targetId, path = []) {
1861
2179
  return [];
1862
2180
  }
1863
2181
 
2182
+ // Expand path to a node and highlight it (without triggering content display)
2183
+ function expandAndHighlightNode(nodeId) {
2184
+ console.log('=== EXPAND AND HIGHLIGHT ===');
2185
+ console.log('Target nodeId:', nodeId);
2186
+
2187
+ // Find the path to this node in the tree structure
2188
+ const pathToNode = findPathToNode(treeData, nodeId);
2189
+
2190
+ if (pathToNode.length > 0) {
2191
+ console.log('Found path:', pathToNode.map(n => n.name).join(' -> '));
2192
+
2193
+ // Expand all nodes along the path (except the target node itself)
2194
+ let needsRerender = false;
2195
+ pathToNode.slice(0, -1).forEach(node => {
2196
+ if (node._children) {
2197
+ console.log('Expanding:', node.name);
2198
+ node.children = node._children;
2199
+ node._children = null;
2200
+ needsRerender = true;
2201
+ }
2202
+ });
2203
+
2204
+ if (needsRerender) {
2205
+ renderVisualization();
2206
+ }
2207
+
2208
+ // Highlight after a short delay to allow render to complete
2209
+ setTimeout(() => {
2210
+ highlightNodeInTree(nodeId);
2211
+ }, 50);
2212
+ } else {
2213
+ console.log('Path not found - trying direct highlight');
2214
+ highlightNodeInTree(nodeId);
2215
+ }
2216
+ }
2217
+
1864
2218
  // Highlight and scroll to a node in the rendered tree
1865
- function highlightNodeInTree(nodeId) {
2219
+ function highlightNodeInTree(nodeId, persistent = true) {
2220
+ console.log('=== HIGHLIGHT NODE ===');
2221
+ console.log('Looking for nodeId:', nodeId);
2222
+
1866
2223
  // Remove any existing highlight
1867
2224
  d3.selectAll('.node-highlight').classed('node-highlight', false);
2225
+ if (persistent) {
2226
+ d3.selectAll('.node-selected').classed('node-selected', false);
2227
+ }
1868
2228
 
1869
2229
  // Find and highlight the target node in the rendered SVG
1870
2230
  const svg = d3.select('#graph');
1871
- const targetNode = svg.selectAll('.node')
1872
- .filter(d => d.data.id === nodeId);
2231
+ const allNodes = svg.selectAll('.node');
2232
+ console.log('Total nodes in SVG:', allNodes.size());
2233
+
2234
+ const targetNode = allNodes.filter(d => d.data.id === nodeId);
2235
+ console.log('Matching nodes found:', targetNode.size());
1873
2236
 
1874
2237
  if (!targetNode.empty()) {
1875
- // Add highlight class
1876
- targetNode.classed('node-highlight', true);
2238
+ // Add highlight class (persistent = orange glow that stays)
2239
+ if (persistent) {
2240
+ targetNode.classed('node-selected', true);
2241
+ console.log('Applied .node-selected class');
2242
+ } else {
2243
+ targetNode.classed('node-highlight', true);
2244
+ console.log('Applied .node-highlight class');
2245
+ }
1877
2246
 
1878
2247
  // Pulse the node circle - scale up from current size
1879
2248
  targetNode.select('circle')
@@ -2033,6 +2402,127 @@ function toggleLayout() {
2033
2402
  renderVisualization();
2034
2403
  }
2035
2404
 
2405
+ // ============================================================================
2406
+ // FILE TYPE FILTER
2407
+ // ============================================================================
2408
+
2409
+ // Code file extensions
2410
+ const codeExtensions = new Set([
2411
+ '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.cpp', '.h', '.hpp',
2412
+ '.cs', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.scala', '.r',
2413
+ '.sh', '.bash', '.zsh', '.ps1', '.bat', '.sql', '.html', '.css', '.scss',
2414
+ '.sass', '.less', '.vue', '.svelte', '.astro', '.elm', '.clj', '.ex', '.exs',
2415
+ '.hs', '.ml', '.lua', '.pl', '.pm', '.m', '.mm', '.f', '.f90', '.for',
2416
+ '.asm', '.s', '.v', '.vhd', '.sv', '.nim', '.zig', '.d', '.dart', '.groovy',
2417
+ '.coffee', '.litcoffee', '.purs', '.rkt', '.scm', '.lisp', '.cl'
2418
+ ]);
2419
+
2420
+ // Doc file extensions
2421
+ const docExtensions = new Set([
2422
+ '.md', '.markdown', '.rst', '.txt', '.adoc', '.asciidoc', '.org', '.tex',
2423
+ '.rtf', '.doc', '.docx', '.pdf', '.json', '.yaml', '.yml', '.toml', '.ini',
2424
+ '.cfg', '.conf', '.xml', '.csv', '.tsv', '.log', '.man', '.info', '.pod',
2425
+ '.rdoc', '.textile', '.wiki'
2426
+ ]);
2427
+
2428
+ function getFileType(filename) {
2429
+ if (!filename) return 'unknown';
2430
+ const ext = '.' + filename.split('.').pop().toLowerCase();
2431
+ if (codeExtensions.has(ext)) return 'code';
2432
+ if (docExtensions.has(ext)) return 'docs';
2433
+ return 'unknown';
2434
+ }
2435
+
2436
+ function setFileFilter(filter) {
2437
+ currentFileFilter = filter;
2438
+
2439
+ // Update button states
2440
+ document.querySelectorAll('.filter-btn').forEach(btn => {
2441
+ btn.classList.toggle('active', btn.dataset.filter === filter);
2442
+ });
2443
+
2444
+ // Apply filter to the tree
2445
+ applyFileFilter();
2446
+
2447
+ console.log(`File filter set to: ${filter}`);
2448
+ }
2449
+
2450
+ function applyFileFilter() {
2451
+ if (!treeData) return;
2452
+
2453
+ console.log('=== APPLYING FILE FILTER (VISIBILITY) ===');
2454
+ console.log('Current filter:', currentFileFilter);
2455
+
2456
+ // Get all node elements in the visualization
2457
+ const nodeElements = d3.selectAll('.node');
2458
+ const linkElements = d3.selectAll('.link');
2459
+
2460
+ if (currentFileFilter === 'all') {
2461
+ // Show everything
2462
+ nodeElements.style('display', null);
2463
+ linkElements.style('display', null);
2464
+ console.log('Showing all nodes');
2465
+ return;
2466
+ }
2467
+
2468
+ // Build set of visible node IDs based on filter
2469
+ const visibleIds = new Set();
2470
+
2471
+ // Recursive function to check if a node or any descendant matches filter
2472
+ function checkNodeAndDescendants(node) {
2473
+ let hasMatchingDescendant = false;
2474
+
2475
+ // Check children (both visible and collapsed)
2476
+ const children = node.children || node._children || [];
2477
+ children.forEach(child => {
2478
+ if (checkNodeAndDescendants(child)) {
2479
+ hasMatchingDescendant = true;
2480
+ }
2481
+ });
2482
+
2483
+ // Check if this node itself matches
2484
+ let matches = false;
2485
+ if (node.type === 'directory') {
2486
+ // Directories are visible if they have matching descendants
2487
+ matches = hasMatchingDescendant;
2488
+ } else if (node.type === 'file') {
2489
+ const fileType = getFileType(node.name);
2490
+ matches = (fileType === currentFileFilter) || (fileType === 'unknown');
2491
+ } else if (chunkTypes.includes(node.type)) {
2492
+ // Chunks match if their parent file matches
2493
+ // For simplicity, check file_path extension
2494
+ if (node.file_path) {
2495
+ const ext = node.file_path.substring(node.file_path.lastIndexOf('.'));
2496
+ const fileType = getFileType(node.file_path);
2497
+ matches = (fileType === currentFileFilter) || (fileType === 'unknown');
2498
+ }
2499
+ }
2500
+
2501
+ if (matches || hasMatchingDescendant) {
2502
+ visibleIds.add(node.id);
2503
+ return true;
2504
+ }
2505
+ return false;
2506
+ }
2507
+
2508
+ checkNodeAndDescendants(treeData);
2509
+ console.log(`Filter found ${visibleIds.size} visible nodes`);
2510
+
2511
+ // Apply visibility to DOM elements
2512
+ nodeElements.style('display', function(d) {
2513
+ return visibleIds.has(d.data.id) ? null : 'none';
2514
+ });
2515
+
2516
+ // Hide links where either end is hidden
2517
+ linkElements.style('display', function(d) {
2518
+ const sourceVisible = visibleIds.has(d.source.data.id);
2519
+ const targetVisible = visibleIds.has(d.target.data.id);
2520
+ return (sourceVisible && targetVisible) ? null : 'none';
2521
+ });
2522
+
2523
+ console.log('=== FILTER COMPLETE (VISIBILITY) ===');
2524
+ }
2525
+
2036
2526
  // ============================================================================
2037
2527
  // VIEWER PANEL CONTROLS
2038
2528
  // ============================================================================
@@ -2475,13 +2965,1558 @@ function closeSearchResults() {
2475
2965
  }
2476
2966
 
2477
2967
  // ============================================================================
2478
- // INITIALIZATION
2968
+ // THEME TOGGLE
2479
2969
  // ============================================================================
2480
2970
 
2481
- // Load data and initialize UI when page loads
2482
- document.addEventListener('DOMContentLoaded', () => {
2483
- console.log('=== PAGE INITIALIZATION ===');
2484
- console.log('DOMContentLoaded event fired');
2971
+ function toggleTheme() {
2972
+ const html = document.documentElement;
2973
+ const currentTheme = html.getAttribute('data-theme') || 'dark';
2974
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
2975
+
2976
+ // Update theme
2977
+ if (newTheme === 'light') {
2978
+ html.setAttribute('data-theme', 'light');
2979
+ } else {
2980
+ html.removeAttribute('data-theme');
2981
+ }
2982
+
2983
+ // Save preference
2984
+ localStorage.setItem('theme', newTheme);
2985
+
2986
+ // Update icon only
2987
+ const themeIcon = document.getElementById('theme-icon');
2988
+
2989
+ if (newTheme === 'light') {
2990
+ themeIcon.textContent = '☀️';
2991
+ } else {
2992
+ themeIcon.textContent = '🌙';
2993
+ }
2994
+
2995
+ console.log(`Theme toggled to: ${newTheme}`);
2996
+ }
2997
+
2998
+ function loadThemePreference() {
2999
+ const savedTheme = localStorage.getItem('theme') || 'dark';
3000
+ const html = document.documentElement;
3001
+
3002
+ if (savedTheme === 'light') {
3003
+ html.setAttribute('data-theme', 'light');
3004
+ const themeIcon = document.getElementById('theme-icon');
3005
+ if (themeIcon) themeIcon.textContent = '☀️';
3006
+ }
3007
+
3008
+ console.log(`Loaded theme preference: ${savedTheme}`);
3009
+ }
3010
+
3011
+ // ============================================================================
3012
+ // ANALYSIS REPORTS
3013
+ // ============================================================================
3014
+
3015
+ function getComplexityGrade(complexity) {
3016
+ if (complexity === undefined || complexity === null) return 'N/A';
3017
+ if (complexity <= 5) return 'A';
3018
+ if (complexity <= 10) return 'B';
3019
+ if (complexity <= 15) return 'C';
3020
+ if (complexity <= 20) return 'D';
3021
+ return 'F';
3022
+ }
3023
+
3024
+ function getGradeColor(grade) {
3025
+ const colors = {
3026
+ 'A': '#2ea043',
3027
+ 'B': '#1f6feb',
3028
+ 'C': '#d29922',
3029
+ 'D': '#f0883e',
3030
+ 'F': '#da3633',
3031
+ 'N/A': '#6e7681'
3032
+ };
3033
+ return colors[grade] || colors['N/A'];
3034
+ }
3035
+
3036
+ function showComplexityReport() {
3037
+ openViewerPanel();
3038
+
3039
+ const viewerTitle = document.getElementById('viewer-title');
3040
+ const viewerContent = document.getElementById('viewer-content');
3041
+
3042
+ viewerTitle.textContent = '📊 Complexity Report';
3043
+
3044
+ // Collect all chunk nodes with complexity data
3045
+ const chunksWithComplexity = [];
3046
+
3047
+ function collectChunks(node) {
3048
+ if (chunkTypes.includes(node.type)) {
3049
+ const complexity = node.complexity !== undefined ? node.complexity : null;
3050
+ chunksWithComplexity.push({
3051
+ name: node.name,
3052
+ type: node.type,
3053
+ file_path: node.file_path || 'Unknown',
3054
+ start_line: node.start_line || 0,
3055
+ end_line: node.end_line || 0,
3056
+ complexity: complexity,
3057
+ grade: getComplexityGrade(complexity),
3058
+ node: node
3059
+ });
3060
+ }
3061
+
3062
+ // Recursively process children
3063
+ const children = node.children || node._children || [];
3064
+ children.forEach(child => collectChunks(child));
3065
+ }
3066
+
3067
+ // Start from treeData root
3068
+ if (treeData) {
3069
+ collectChunks(treeData);
3070
+ }
3071
+
3072
+ // Calculate statistics
3073
+ const totalFunctions = chunksWithComplexity.length;
3074
+ const validComplexity = chunksWithComplexity.filter(c => c.complexity !== null);
3075
+ const avgComplexity = validComplexity.length > 0
3076
+ ? (validComplexity.reduce((sum, c) => sum + c.complexity, 0) / validComplexity.length).toFixed(2)
3077
+ : 'N/A';
3078
+
3079
+ // Count by grade
3080
+ const gradeCounts = {
3081
+ 'A': 0, 'B': 0, 'C': 0, 'D': 0, 'F': 0, 'N/A': 0
3082
+ };
3083
+ chunksWithComplexity.forEach(c => {
3084
+ gradeCounts[c.grade]++;
3085
+ });
3086
+
3087
+ // Sort by complexity (highest first)
3088
+ const sortedChunks = [...chunksWithComplexity].sort((a, b) => {
3089
+ if (a.complexity === null) return 1;
3090
+ if (b.complexity === null) return -1;
3091
+ return b.complexity - a.complexity;
3092
+ });
3093
+
3094
+ // Build HTML
3095
+ let html = '<div class="complexity-report">';
3096
+
3097
+ // Summary Stats
3098
+ html += '<div class="complexity-summary">';
3099
+ html += '<div class="summary-grid">';
3100
+ html += `<div class="summary-card">
3101
+ <div class="summary-label">Total Functions</div>
3102
+ <div class="summary-value">${totalFunctions}</div>
3103
+ </div>`;
3104
+ html += `<div class="summary-card">
3105
+ <div class="summary-label">Average Complexity</div>
3106
+ <div class="summary-value">${avgComplexity}</div>
3107
+ </div>`;
3108
+ html += `<div class="summary-card">
3109
+ <div class="summary-label">With Complexity Data</div>
3110
+ <div class="summary-value">${validComplexity.length}</div>
3111
+ </div>`;
3112
+ html += '</div>';
3113
+
3114
+ // Grade Distribution
3115
+ html += '<div class="grade-distribution">';
3116
+ html += '<div class="distribution-title">Grade Distribution</div>';
3117
+ html += '<div class="distribution-bars">';
3118
+
3119
+ const maxCount = Math.max(...Object.values(gradeCounts));
3120
+ ['A', 'B', 'C', 'D', 'F', 'N/A'].forEach(grade => {
3121
+ const count = gradeCounts[grade];
3122
+ const percentage = totalFunctions > 0 ? (count / totalFunctions * 100) : 0;
3123
+ const barWidth = maxCount > 0 ? (count / maxCount * 100) : 0;
3124
+ html += `
3125
+ <div class="distribution-row">
3126
+ <div class="distribution-grade" style="color: ${getGradeColor(grade)}">${grade}</div>
3127
+ <div class="distribution-bar-container">
3128
+ <div class="distribution-bar" style="width: ${barWidth}%; background: ${getGradeColor(grade)}"></div>
3129
+ </div>
3130
+ <div class="distribution-count">${count} (${percentage.toFixed(1)}%)</div>
3131
+ </div>
3132
+ `;
3133
+ });
3134
+ html += '</div></div>';
3135
+ html += '</div>';
3136
+
3137
+ // Complexity Hotspots Table
3138
+ html += '<div class="complexity-hotspots">';
3139
+ html += '<h3 class="section-title">Complexity Hotspots</h3>';
3140
+
3141
+ if (sortedChunks.length === 0) {
3142
+ html += '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No functions found with complexity data.</p>';
3143
+ } else {
3144
+ html += '<div class="hotspots-table-container">';
3145
+ html += '<table class="hotspots-table">';
3146
+ html += `
3147
+ <thead>
3148
+ <tr>
3149
+ <th>Name</th>
3150
+ <th>File</th>
3151
+ <th>Lines</th>
3152
+ <th>Complexity</th>
3153
+ <th>Grade</th>
3154
+ </tr>
3155
+ </thead>
3156
+ <tbody>
3157
+ `;
3158
+
3159
+ sortedChunks.forEach(chunk => {
3160
+ const lines = chunk.end_line > 0 ? chunk.end_line - chunk.start_line + 1 : 'N/A';
3161
+ const complexityDisplay = chunk.complexity !== null ? chunk.complexity.toFixed(1) : 'N/A';
3162
+ const gradeColor = getGradeColor(chunk.grade);
3163
+
3164
+ // Get relative file path
3165
+ const fileName = chunk.file_path.split('/').pop() || chunk.file_path;
3166
+ const lineRange = chunk.start_line > 0 ? `L${chunk.start_line}-${chunk.end_line}` : '';
3167
+
3168
+ html += `
3169
+ <tr class="hotspot-row" onclick='navigateToChunk(${JSON.stringify(chunk.name)})'>
3170
+ <td class="hotspot-name">${escapeHtml(chunk.name)}</td>
3171
+ <td class="hotspot-file" title="${escapeHtml(chunk.file_path)}">${escapeHtml(fileName)}</td>
3172
+ <td class="hotspot-lines">${lines}</td>
3173
+ <td class="hotspot-complexity">${complexityDisplay}</td>
3174
+ <td class="hotspot-grade">
3175
+ <span class="grade-badge" style="background: ${gradeColor}">${chunk.grade}</span>
3176
+ </td>
3177
+ </tr>
3178
+ `;
3179
+ });
3180
+
3181
+ html += '</tbody></table></div>';
3182
+ }
3183
+
3184
+ html += '</div>'; // complexity-hotspots
3185
+ html += '</div>'; // complexity-report
3186
+
3187
+ viewerContent.innerHTML = html;
3188
+
3189
+ // Hide section dropdown for reports (no code sections)
3190
+ const sectionNav = document.getElementById('section-nav');
3191
+ if (sectionNav) {
3192
+ sectionNav.style.display = 'none';
3193
+ }
3194
+ }
3195
+
3196
+ function navigateToChunk(chunkName) {
3197
+ // Find the chunk node in the tree
3198
+ function findChunk(node) {
3199
+ if (chunkTypes.includes(node.type) && node.name === chunkName) {
3200
+ return node;
3201
+ }
3202
+ const children = node.children || node._children || [];
3203
+ for (const child of children) {
3204
+ const found = findChunk(child);
3205
+ if (found) return found;
3206
+ }
3207
+ return null;
3208
+ }
3209
+
3210
+ if (treeData) {
3211
+ const chunk = findChunk(treeData);
3212
+ if (chunk) {
3213
+ // Display the chunk content
3214
+ displayChunkContent(chunk);
3215
+
3216
+ // Highlight the node in the visualization
3217
+ highlightNode(chunk.id);
3218
+ }
3219
+ }
3220
+ }
3221
+
3222
+ function escapeHtml(text) {
3223
+ const div = document.createElement('div');
3224
+ div.textContent = text;
3225
+ return div.innerHTML;
3226
+ }
3227
+
3228
+ function highlightNode(nodeId) {
3229
+ // Remove existing highlights
3230
+ d3.selectAll('.node').classed('node-highlight', false);
3231
+
3232
+ // Add highlight to the target node
3233
+ d3.selectAll('.node')
3234
+ .filter(d => d.data.id === nodeId)
3235
+ .classed('node-highlight', true);
3236
+ }
3237
+
3238
+ // ============================================================================
3239
+ // CODE SMELLS DETECTION AND REPORTING
3240
+ // ============================================================================
3241
+
3242
+ function detectCodeSmells(nodes) {
3243
+ const smells = [];
3244
+
3245
+ function analyzeNode(node) {
3246
+ // Only analyze code chunks
3247
+ if (!chunkTypes.includes(node.type)) {
3248
+ const children = node.children || node._children || [];
3249
+ children.forEach(child => analyzeNode(child));
3250
+ return;
3251
+ }
3252
+
3253
+ const lineCount = (node.end_line && node.start_line)
3254
+ ? node.end_line - node.start_line + 1
3255
+ : 0;
3256
+ const complexity = node.complexity || 0;
3257
+
3258
+ // 1. Long Method - Functions with > 50 lines
3259
+ if (node.type === 'function' || node.type === 'method') {
3260
+ if (lineCount > 100) {
3261
+ smells.push({
3262
+ type: 'Long Method',
3263
+ severity: 'error',
3264
+ node: node,
3265
+ details: `${lineCount} lines (very long)`
3266
+ });
3267
+ } else if (lineCount > 50) {
3268
+ smells.push({
3269
+ type: 'Long Method',
3270
+ severity: 'warning',
3271
+ node: node,
3272
+ details: `${lineCount} lines`
3273
+ });
3274
+ }
3275
+ }
3276
+
3277
+ // 2. High Complexity - Functions with complexity > 15
3278
+ if ((node.type === 'function' || node.type === 'method') && complexity > 0) {
3279
+ if (complexity > 20) {
3280
+ smells.push({
3281
+ type: 'High Complexity',
3282
+ severity: 'error',
3283
+ node: node,
3284
+ details: `Complexity: ${complexity.toFixed(1)} (very complex)`
3285
+ });
3286
+ } else if (complexity > 15) {
3287
+ smells.push({
3288
+ type: 'High Complexity',
3289
+ severity: 'warning',
3290
+ node: node,
3291
+ details: `Complexity: ${complexity.toFixed(1)}`
3292
+ });
3293
+ }
3294
+ }
3295
+
3296
+ // 3. Deep Nesting - Proxy using complexity > 20
3297
+ if ((node.type === 'function' || node.type === 'method') && complexity > 20) {
3298
+ smells.push({
3299
+ type: 'Deep Nesting',
3300
+ severity: complexity > 25 ? 'error' : 'warning',
3301
+ node: node,
3302
+ details: `Complexity: ${complexity.toFixed(1)} (likely deep nesting)`
3303
+ });
3304
+ }
3305
+
3306
+ // 4. God Class - Classes with > 20 methods or > 500 lines
3307
+ if (node.type === 'class') {
3308
+ const children = node.children || node._children || [];
3309
+ const methodCount = children.filter(c => c.type === 'method').length;
3310
+
3311
+ if (methodCount > 30 || lineCount > 800) {
3312
+ smells.push({
3313
+ type: 'God Class',
3314
+ severity: 'error',
3315
+ node: node,
3316
+ details: `${methodCount} methods, ${lineCount} lines (very large)`
3317
+ });
3318
+ } else if (methodCount > 20 || lineCount > 500) {
3319
+ smells.push({
3320
+ type: 'God Class',
3321
+ severity: 'warning',
3322
+ node: node,
3323
+ details: `${methodCount} methods, ${lineCount} lines`
3324
+ });
3325
+ }
3326
+ }
3327
+
3328
+ // Recursively process children
3329
+ const children = node.children || node._children || [];
3330
+ children.forEach(child => analyzeNode(child));
3331
+ }
3332
+
3333
+ if (nodes) {
3334
+ analyzeNode(nodes);
3335
+ }
3336
+
3337
+ return smells;
3338
+ }
3339
+
3340
+ function showCodeSmells() {
3341
+ openViewerPanel();
3342
+
3343
+ const viewerTitle = document.getElementById('viewer-title');
3344
+ const viewerContent = document.getElementById('viewer-content');
3345
+
3346
+ viewerTitle.textContent = '🔍 Code Smells';
3347
+
3348
+ // Detect all code smells
3349
+ const allSmells = detectCodeSmells(treeData);
3350
+
3351
+ // Count by type and severity
3352
+ const smellCounts = {
3353
+ 'Long Method': { total: 0, warning: 0, error: 0 },
3354
+ 'High Complexity': { total: 0, warning: 0, error: 0 },
3355
+ 'Deep Nesting': { total: 0, warning: 0, error: 0 },
3356
+ 'God Class': { total: 0, warning: 0, error: 0 }
3357
+ };
3358
+
3359
+ let totalWarnings = 0;
3360
+ let totalErrors = 0;
3361
+
3362
+ allSmells.forEach(smell => {
3363
+ smellCounts[smell.type].total++;
3364
+ smellCounts[smell.type][smell.severity]++;
3365
+ if (smell.severity === 'warning') totalWarnings++;
3366
+ if (smell.severity === 'error') totalErrors++;
3367
+ });
3368
+
3369
+ // Build HTML
3370
+ let html = '<div class="code-smells-report">';
3371
+
3372
+ // Summary Cards
3373
+ html += '<div class="smell-summary-grid">';
3374
+ html += `
3375
+ <div class="smell-summary-card">
3376
+ <div class="smell-card-header">
3377
+ <span class="smell-card-icon">🔍</span>
3378
+ <div class="smell-card-title">Total Smells</div>
3379
+ </div>
3380
+ <div class="smell-card-count">${allSmells.length}</div>
3381
+ </div>
3382
+ <div class="smell-summary-card warning">
3383
+ <div class="smell-card-header">
3384
+ <span class="smell-card-icon">⚠️</span>
3385
+ <div class="smell-card-title">Warnings</div>
3386
+ </div>
3387
+ <div class="smell-card-count" style="color: var(--warning)">${totalWarnings}</div>
3388
+ </div>
3389
+ <div class="smell-summary-card error">
3390
+ <div class="smell-card-header">
3391
+ <span class="smell-card-icon">🚨</span>
3392
+ <div class="smell-card-title">Errors</div>
3393
+ </div>
3394
+ <div class="smell-card-count" style="color: var(--error)">${totalErrors}</div>
3395
+ </div>
3396
+ `;
3397
+ html += '</div>';
3398
+
3399
+ // Filters
3400
+ html += '<div class="smell-filters">';
3401
+ html += '<div class="filter-title">Filter by Type</div>';
3402
+ html += '<div class="filter-checkboxes">';
3403
+
3404
+ Object.keys(smellCounts).forEach(type => {
3405
+ const count = smellCounts[type].total;
3406
+ html += `
3407
+ <div class="filter-checkbox-item">
3408
+ <input type="checkbox" id="filter-${type.replace(/\\s+/g, '-')}"
3409
+ checked onchange="filterCodeSmells()">
3410
+ <label class="filter-checkbox-label" for="filter-${type.replace(/\\s+/g, '-')}">${type}</label>
3411
+ <span class="filter-checkbox-count">${count}</span>
3412
+ </div>
3413
+ `;
3414
+ });
3415
+
3416
+ html += '</div></div>';
3417
+
3418
+ // Smells Table
3419
+ html += '<div id="smells-table-wrapper">';
3420
+ html += '<h3 class="section-title">Detected Code Smells</h3>';
3421
+
3422
+ if (allSmells.length === 0) {
3423
+ html += '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No code smells detected! Great job! 🎉</p>';
3424
+ } else {
3425
+ html += '<div class="smells-table-container">';
3426
+ html += '<table class="smells-table">';
3427
+ html += `
3428
+ <thead>
3429
+ <tr>
3430
+ <th>Type</th>
3431
+ <th>Severity</th>
3432
+ <th>Name</th>
3433
+ <th>File</th>
3434
+ <th>Details</th>
3435
+ </tr>
3436
+ </thead>
3437
+ <tbody id="smells-table-body">
3438
+ `;
3439
+
3440
+ // Sort by severity (error first) then by type
3441
+ const sortedSmells = [...allSmells].sort((a, b) => {
3442
+ if (a.severity !== b.severity) {
3443
+ return a.severity === 'error' ? -1 : 1;
3444
+ }
3445
+ return a.type.localeCompare(b.type);
3446
+ });
3447
+
3448
+ sortedSmells.forEach(smell => {
3449
+ const fileName = smell.node.file_path ? smell.node.file_path.split('/').pop() : 'Unknown';
3450
+ const severityIcon = smell.severity === 'error' ? '🚨' : '⚠️';
3451
+
3452
+ html += `
3453
+ <tr class="smell-row" data-smell-type="${smell.type.replace(/\\s+/g, '-')}"
3454
+ onclick='navigateToChunk(${JSON.stringify(smell.node.name)})'>
3455
+ <td><span class="smell-type-badge">${escapeHtml(smell.type)}</span></td>
3456
+ <td><span class="severity-badge ${smell.severity}">${severityIcon} ${smell.severity.toUpperCase()}</span></td>
3457
+ <td class="smell-name">${escapeHtml(smell.node.name)}</td>
3458
+ <td class="smell-file" title="${escapeHtml(smell.node.file_path || '')}">${escapeHtml(fileName)}</td>
3459
+ <td class="smell-details">${escapeHtml(smell.details)}</td>
3460
+ </tr>
3461
+ `;
3462
+ });
3463
+
3464
+ html += '</tbody></table></div>';
3465
+ }
3466
+
3467
+ html += '</div>'; // smells-table-wrapper
3468
+ html += '</div>'; // code-smells-report
3469
+
3470
+ viewerContent.innerHTML = html;
3471
+
3472
+ // Hide section dropdown for reports (no code sections)
3473
+ const sectionNav = document.getElementById('section-nav');
3474
+ if (sectionNav) {
3475
+ sectionNav.style.display = 'none';
3476
+ }
3477
+ }
3478
+
3479
+ function filterCodeSmells() {
3480
+ const rows = document.querySelectorAll('.smell-row');
3481
+
3482
+ rows.forEach(row => {
3483
+ const smellType = row.getAttribute('data-smell-type');
3484
+ const checkbox = document.getElementById(`filter-${smellType}`);
3485
+
3486
+ if (checkbox && checkbox.checked) {
3487
+ row.style.display = '';
3488
+ } else {
3489
+ row.style.display = 'none';
3490
+ }
3491
+ });
3492
+ }
3493
+
3494
+ // ============================================================================
3495
+ // DEPENDENCIES ANALYSIS AND REPORTING
3496
+ // ============================================================================
3497
+
3498
+ function showDependencies() {
3499
+ openViewerPanel();
3500
+
3501
+ const viewerTitle = document.getElementById('viewer-title');
3502
+ const viewerContent = document.getElementById('viewer-content');
3503
+
3504
+ viewerTitle.textContent = '🔗 Code Structure';
3505
+
3506
+ // Code file extensions (exclude docs like .md, .txt, .rst)
3507
+ const codeExtensions = ['.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.swift', '.kt', '.scala', '.ex', '.exs', '.clj', '.vue', '.svelte'];
3508
+
3509
+ function isCodeFile(filePath) {
3510
+ const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
3511
+ return codeExtensions.includes(ext);
3512
+ }
3513
+
3514
+ // Build directory structure from nodes (code files only)
3515
+ const dirStructure = new Map();
3516
+
3517
+ allNodes.forEach(node => {
3518
+ if (node.type === 'file' && node.file_path && isCodeFile(node.file_path)) {
3519
+ const parts = node.file_path.split('/');
3520
+ const fileName = parts.pop();
3521
+ const dirPath = parts.join('/') || '/';
3522
+
3523
+ if (!dirStructure.has(dirPath)) {
3524
+ dirStructure.set(dirPath, []);
3525
+ }
3526
+ dirStructure.get(dirPath).push({
3527
+ name: fileName,
3528
+ path: node.file_path,
3529
+ chunks: allNodes.filter(n => n.file_path === node.file_path && n.type !== 'file').length
3530
+ });
3531
+ }
3532
+ });
3533
+
3534
+ // Calculate stats
3535
+ const totalDirs = dirStructure.size;
3536
+ const totalFiles = Array.from(dirStructure.values()).reduce((sum, files) => sum + files.length, 0);
3537
+ const totalChunks = allNodes.filter(n => n.type !== 'file' && n.type !== 'directory').length;
3538
+
3539
+ let html = `
3540
+ <div class="report-section">
3541
+ <h3>📁 Directory Overview</h3>
3542
+ <p style="color: var(--text-secondary); margin-bottom: 15px;">Showing code organization by directory structure.</p>
3543
+ <div class="metrics-grid">
3544
+ <div class="metric-card">
3545
+ <div class="metric-value">${totalDirs}</div>
3546
+ <div class="metric-label">Directories</div>
3547
+ </div>
3548
+ <div class="metric-card">
3549
+ <div class="metric-value">${totalFiles}</div>
3550
+ <div class="metric-label">Files</div>
3551
+ </div>
3552
+ <div class="metric-card">
3553
+ <div class="metric-value">${totalChunks}</div>
3554
+ <div class="metric-label">Code Chunks</div>
3555
+ </div>
3556
+ </div>
3557
+ </div>
3558
+ <div class="report-section">
3559
+ <h3>📂 Directory Structure</h3>
3560
+ `;
3561
+
3562
+ // Sort directories
3563
+ const sortedDirs = Array.from(dirStructure.entries()).sort((a, b) => a[0].localeCompare(b[0]));
3564
+
3565
+ sortedDirs.forEach(([dir, files]) => {
3566
+ const dirDisplay = dir === '/' ? 'Root' : dir;
3567
+ const totalChunksInDir = files.reduce((sum, f) => sum + f.chunks, 0);
3568
+
3569
+ html += `
3570
+ <div class="dependency-item" style="margin-bottom: 20px; padding: 12px; background: var(--bg-secondary); border-radius: 6px; border-left: 3px solid var(--accent-blue);">
3571
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
3572
+ <strong style="color: var(--accent-blue);">📁 ${escapeHtml(dirDisplay)}</strong>
3573
+ <span style="color: var(--text-secondary); font-size: 12px;">${files.length} files, ${totalChunksInDir} chunks</span>
3574
+ </div>
3575
+ <ul style="margin: 0; padding: 0 0 0 20px; list-style: none;">
3576
+ `;
3577
+
3578
+ files.sort((a, b) => a.name.localeCompare(b.name)).forEach(file => {
3579
+ html += `<li style="padding: 4px 0; color: var(--text-primary);">
3580
+ 📄 ${escapeHtml(file.name)}
3581
+ <span style="color: var(--text-secondary); font-size: 11px;">(${file.chunks} chunks)</span>
3582
+ </li>`;
3583
+ });
3584
+
3585
+ html += `</ul></div>`;
3586
+ });
3587
+
3588
+ html += '</div>';
3589
+
3590
+ viewerContent.innerHTML = html;
3591
+
3592
+ // Hide section dropdown for reports (no code sections)
3593
+ const sectionNav = document.getElementById('section-nav');
3594
+ if (sectionNav) {
3595
+ sectionNav.style.display = 'none';
3596
+ }
3597
+ }
3598
+
3599
+ function buildFileDependencyGraph() {
3600
+ // Map: file_path -> { dependsOn: Set<file_path>, usedBy: Set<file_path> }
3601
+ const fileDeps = new Map();
3602
+
3603
+ allLinks.forEach(link => {
3604
+ if (link.type === 'caller') {
3605
+ const sourceNode = allNodes.find(n => n.id === link.source);
3606
+ const targetNode = allNodes.find(n => n.id === link.target);
3607
+
3608
+ if (sourceNode && targetNode && sourceNode.file_path && targetNode.file_path) {
3609
+ // Skip self-references (same file)
3610
+ if (sourceNode.file_path === targetNode.file_path) {
3611
+ return;
3612
+ }
3613
+
3614
+ // sourceNode calls targetNode → source depends on target
3615
+ if (!fileDeps.has(sourceNode.file_path)) {
3616
+ fileDeps.set(sourceNode.file_path, { dependsOn: new Set(), usedBy: new Set() });
3617
+ }
3618
+ if (!fileDeps.has(targetNode.file_path)) {
3619
+ fileDeps.set(targetNode.file_path, { dependsOn: new Set(), usedBy: new Set() });
3620
+ }
3621
+
3622
+ fileDeps.get(sourceNode.file_path).dependsOn.add(targetNode.file_path);
3623
+ fileDeps.get(targetNode.file_path).usedBy.add(sourceNode.file_path);
3624
+ }
3625
+ }
3626
+ });
3627
+
3628
+ return fileDeps;
3629
+ }
3630
+
3631
+ function findCircularDeps(fileDeps) {
3632
+ // Simple cycle detection using DFS
3633
+ const cycles = [];
3634
+ const visited = new Set();
3635
+ const recStack = new Set();
3636
+ const pathStack = [];
3637
+
3638
+ function dfs(filePath) {
3639
+ visited.add(filePath);
3640
+ recStack.add(filePath);
3641
+ pathStack.push(filePath);
3642
+
3643
+ const deps = fileDeps.get(filePath);
3644
+ if (deps && deps.dependsOn) {
3645
+ for (const depFile of deps.dependsOn) {
3646
+ if (!visited.has(depFile)) {
3647
+ dfs(depFile);
3648
+ } else if (recStack.has(depFile)) {
3649
+ // Found a cycle
3650
+ const cycleStartIndex = pathStack.indexOf(depFile);
3651
+ if (cycleStartIndex !== -1) {
3652
+ const cycle = pathStack.slice(cycleStartIndex);
3653
+ cycle.push(depFile); // Complete the cycle
3654
+ // Check if this cycle is already recorded
3655
+ const cycleStr = cycle.sort().join('|');
3656
+ if (!cycles.some(c => c.sort().join('|') === cycleStr)) {
3657
+ cycles.push([...cycle]);
3658
+ }
3659
+ }
3660
+ }
3661
+ }
3662
+ }
3663
+
3664
+ pathStack.pop();
3665
+ recStack.delete(filePath);
3666
+ }
3667
+
3668
+ for (const filePath of fileDeps.keys()) {
3669
+ if (!visited.has(filePath)) {
3670
+ dfs(filePath);
3671
+ }
3672
+ }
3673
+
3674
+ return cycles;
3675
+ }
3676
+
3677
+ function toggleDependencyDetails(rowId, index) {
3678
+ const detailsRow = document.getElementById(`${rowId}-details`);
3679
+ const btn = document.querySelector(`#${rowId} .expand-btn`);
3680
+
3681
+ if (detailsRow.style.display === 'none') {
3682
+ detailsRow.style.display = '';
3683
+ btn.textContent = '▲';
3684
+ } else {
3685
+ detailsRow.style.display = 'none';
3686
+ btn.textContent = '▼';
3687
+ }
3688
+ }
3689
+
3690
+ // ============================================================================
3691
+ // TRENDS / METRICS SNAPSHOT
3692
+ // ============================================================================
3693
+
3694
+ function showTrends() {
3695
+ openViewerPanel();
3696
+
3697
+ const viewerTitle = document.getElementById('viewer-title');
3698
+ const viewerContent = document.getElementById('viewer-content');
3699
+
3700
+ viewerTitle.textContent = '📈 Codebase Metrics Snapshot';
3701
+
3702
+ // Calculate metrics from current codebase
3703
+ const metrics = calculateCodebaseMetrics();
3704
+
3705
+ // Build HTML
3706
+ let html = '<div class="trends-report">';
3707
+
3708
+ // Snapshot Banner
3709
+ html += '<div class="snapshot-banner">';
3710
+ html += '<div class="snapshot-header">';
3711
+ html += '<div class="snapshot-icon">📊</div>';
3712
+ html += '<div class="snapshot-info">';
3713
+ html += '<div class="snapshot-title">Codebase Metrics Snapshot</div>';
3714
+ html += `<div class="snapshot-timestamp">Generated: ${new Date().toLocaleString('en-US', {
3715
+ month: 'short',
3716
+ day: 'numeric',
3717
+ year: 'numeric',
3718
+ hour: 'numeric',
3719
+ minute: '2-digit',
3720
+ hour12: true
3721
+ })}</div>`;
3722
+ html += '</div></div>';
3723
+ html += '<div class="snapshot-description">';
3724
+ html += 'This snapshot serves as the baseline for future trend tracking. ';
3725
+ html += 'With git history analysis, you could track how these metrics evolve over time.';
3726
+ html += '</div>';
3727
+ html += '</div>';
3728
+
3729
+ // Key Metrics Cards
3730
+ html += '<div class="metrics-section">';
3731
+ html += '<h3 class="section-title">Key Metrics</h3>';
3732
+ html += '<div class="metrics-grid">';
3733
+
3734
+ html += `<div class="metric-card">
3735
+ <div class="metric-icon">📝</div>
3736
+ <div class="metric-value">${metrics.totalLines.toLocaleString()}</div>
3737
+ <div class="metric-label">Lines of Code</div>
3738
+ </div>`;
3739
+
3740
+ html += `<div class="metric-card">
3741
+ <div class="metric-icon">⚡</div>
3742
+ <div class="metric-value">${metrics.totalFunctions}</div>
3743
+ <div class="metric-label">Functions/Methods</div>
3744
+ </div>`;
3745
+
3746
+ html += `<div class="metric-card">
3747
+ <div class="metric-icon">🎯</div>
3748
+ <div class="metric-value">${metrics.totalClasses}</div>
3749
+ <div class="metric-label">Classes</div>
3750
+ </div>`;
3751
+
3752
+ html += `<div class="metric-card">
3753
+ <div class="metric-icon">📄</div>
3754
+ <div class="metric-value">${metrics.totalFiles}</div>
3755
+ <div class="metric-label">Files</div>
3756
+ </div>`;
3757
+
3758
+ html += '</div></div>';
3759
+
3760
+ // Code Health Score
3761
+ html += '<div class="health-section">';
3762
+ html += '<h3 class="section-title">Code Health Score</h3>';
3763
+ html += '<div class="health-card">';
3764
+
3765
+ const healthInfo = getHealthScoreInfo(metrics.healthScore);
3766
+ html += `<div class="health-score-display">
3767
+ <div class="health-score-value">${metrics.healthScore}/100</div>
3768
+ <div class="health-score-label">${healthInfo.emoji} ${healthInfo.label}</div>
3769
+ </div>`;
3770
+
3771
+ html += '<div class="health-progress-container">';
3772
+ html += `<div class="health-progress-bar" style="width: ${metrics.healthScore}%; background: ${healthInfo.color}"></div>`;
3773
+ html += '</div>';
3774
+
3775
+ html += `<div class="health-description">${healthInfo.description}</div>`;
3776
+ html += '</div></div>';
3777
+
3778
+ // Complexity Distribution
3779
+ html += '<div class="distribution-section">';
3780
+ html += '<h3 class="section-title">Complexity Distribution</h3>';
3781
+ html += '<div class="distribution-chart">';
3782
+
3783
+ const complexityDist = metrics.complexityDistribution;
3784
+ const maxPct = Math.max(...Object.values(complexityDist));
3785
+
3786
+ ['A', 'B', 'C', 'D', 'F'].forEach(grade => {
3787
+ const pct = complexityDist[grade] || 0;
3788
+ const barWidth = maxPct > 0 ? (pct / maxPct * 100) : 0;
3789
+ const color = getGradeColor(grade);
3790
+ const range = getComplexityRange(grade);
3791
+
3792
+ html += `<div class="distribution-bar-row">
3793
+ <div class="distribution-bar-label">
3794
+ <span class="distribution-grade" style="color: ${color}">${grade}</span>
3795
+ <span class="distribution-range">${range}</span>
3796
+ </div>
3797
+ <div class="distribution-bar-container">
3798
+ <div class="distribution-bar-fill" style="width: ${barWidth}%; background: ${color}"></div>
3799
+ </div>
3800
+ <div class="distribution-bar-value">${pct.toFixed(1)}%</div>
3801
+ </div>`;
3802
+ });
3803
+
3804
+ html += '</div></div>';
3805
+
3806
+ // Function Size Distribution
3807
+ html += '<div class="size-distribution-section">';
3808
+ html += '<h3 class="section-title">Function Size Distribution</h3>';
3809
+ html += '<div class="distribution-chart">';
3810
+
3811
+ const sizeDist = metrics.sizeDistribution;
3812
+ const maxSizePct = Math.max(...Object.values(sizeDist));
3813
+
3814
+ [
3815
+ { key: 'small', label: 'Small (1-20 lines)', color: '#238636' },
3816
+ { key: 'medium', label: 'Medium (21-50 lines)', color: '#1f6feb' },
3817
+ { key: 'large', label: 'Large (51-100 lines)', color: '#d29922' },
3818
+ { key: 'veryLarge', label: 'Very Large (100+ lines)', color: '#da3633' }
3819
+ ].forEach(({ key, label, color }) => {
3820
+ const pct = sizeDist[key] || 0;
3821
+ const barWidth = maxSizePct > 0 ? (pct / maxSizePct * 100) : 0;
3822
+
3823
+ html += `<div class="distribution-bar-row">
3824
+ <div class="distribution-bar-label">
3825
+ <span class="size-label">${label}</span>
3826
+ </div>
3827
+ <div class="distribution-bar-container">
3828
+ <div class="distribution-bar-fill" style="width: ${barWidth}%; background: ${color}"></div>
3829
+ </div>
3830
+ <div class="distribution-bar-value">${pct.toFixed(1)}%</div>
3831
+ </div>`;
3832
+ });
3833
+
3834
+ html += '</div></div>';
3835
+
3836
+ // Historical Trends Section
3837
+ html += '<div class="trends-section">';
3838
+ html += '<h3 class="section-title">📊 Historical Trends</h3>';
3839
+
3840
+ // Check if trend data is available
3841
+ if (window.graphTrendData && window.graphTrendData.entries && window.graphTrendData.entries.length > 0) {
3842
+ const trendEntries = window.graphTrendData.entries;
3843
+
3844
+ // Render trend charts
3845
+ html += '<div class="trends-container">';
3846
+ html += '<div id="health-score-chart" class="trend-chart"></div>';
3847
+ html += '<div id="complexity-chart" class="trend-chart"></div>';
3848
+ html += '<div id="files-chunks-chart" class="trend-chart"></div>';
3849
+ html += '</div>';
3850
+
3851
+ html += `<div class="trend-info">Showing ${trendEntries.length} data points from ${trendEntries[0].date} to ${trendEntries[trendEntries.length - 1].date}</div>`;
3852
+ } else {
3853
+ // No trend data yet - show placeholder
3854
+ html += '<div class="future-placeholder">';
3855
+ html += '<div class="future-icon">📊</div>';
3856
+ html += '<div class="future-title">No Historical Data Yet</div>';
3857
+ html += '<div class="future-description">';
3858
+ html += 'Trend data will be collected automatically after each indexing operation. ';
3859
+ html += 'Run <code>mcp-vector-search index</code> to generate the first snapshot.';
3860
+ html += '</div>';
3861
+ html += '</div>';
3862
+ }
3863
+
3864
+ html += '</div>'; // trends-section
3865
+
3866
+ html += '</div>'; // trends-report
3867
+
3868
+ viewerContent.innerHTML = html;
3869
+
3870
+ // Hide section dropdown for reports (no code sections)
3871
+ const sectionNav = document.getElementById('section-nav');
3872
+ if (sectionNav) {
3873
+ sectionNav.style.display = 'none';
3874
+ }
3875
+
3876
+ // Render D3 charts if trend data is available
3877
+ if (window.graphTrendData && window.graphTrendData.entries && window.graphTrendData.entries.length > 0) {
3878
+ renderTrendCharts(window.graphTrendData.entries);
3879
+ }
3880
+ }
3881
+
3882
+ // ============================================================================
3883
+ // REMEDIATION REPORT GENERATION
3884
+ // ============================================================================
3885
+
3886
+ function generateRemediationReport() {
3887
+ // Gather complexity data (metrics are stored directly on nodes, not in a metrics object)
3888
+ const complexityData = [];
3889
+ allNodes.forEach(node => {
3890
+ if (node.complexity !== undefined && node.complexity !== null) {
3891
+ const complexity = node.complexity;
3892
+ let grade = 'A';
3893
+ if (complexity > 40) grade = 'F';
3894
+ else if (complexity > 30) grade = 'D';
3895
+ else if (complexity > 20) grade = 'C';
3896
+ else if (complexity > 10) grade = 'B';
3897
+
3898
+ // Calculate lines from start_line and end_line
3899
+ const lines = (node.end_line && node.start_line) ? (node.end_line - node.start_line + 1) : 0;
3900
+
3901
+ if (grade !== 'A' && grade !== 'B') { // Only include C, D, F
3902
+ complexityData.push({
3903
+ name: node.name || node.id,
3904
+ file: node.file_path || 'Unknown',
3905
+ type: node.type || 'unknown',
3906
+ complexity: complexity,
3907
+ grade: grade,
3908
+ lines: lines
3909
+ });
3910
+ }
3911
+ }
3912
+ });
3913
+
3914
+ // Gather code smell data
3915
+ const smells = [];
3916
+ allNodes.forEach(node => {
3917
+ const file = node.file_path || 'Unknown';
3918
+ const name = node.name || node.id;
3919
+ const lines = (node.end_line && node.start_line) ? (node.end_line - node.start_line + 1) : 0;
3920
+ const complexity = node.complexity || 0;
3921
+ const depth = node.depth || 0;
3922
+
3923
+ // Long Method
3924
+ if (lines > 50) {
3925
+ smells.push({
3926
+ file, name,
3927
+ smell: 'Long Method',
3928
+ severity: lines > 100 ? 'error' : 'warning',
3929
+ detail: `${lines} lines (recommended: <50)`
3930
+ });
3931
+ }
3932
+
3933
+ // High Complexity
3934
+ if (complexity > 15) {
3935
+ smells.push({
3936
+ file, name,
3937
+ smell: 'High Complexity',
3938
+ severity: complexity > 25 ? 'error' : 'warning',
3939
+ detail: `Complexity: ${complexity} (recommended: <15)`
3940
+ });
3941
+ }
3942
+
3943
+ // Deep Nesting (using depth field)
3944
+ if (depth > 4) {
3945
+ smells.push({
3946
+ file, name,
3947
+ smell: 'Deep Nesting',
3948
+ severity: depth > 6 ? 'error' : 'warning',
3949
+ detail: `Depth: ${depth} (recommended: <4)`
3950
+ });
3951
+ }
3952
+
3953
+ // God Class (for classes only)
3954
+ if (node.type === 'class' && lines > 300) {
3955
+ smells.push({
3956
+ file, name,
3957
+ smell: 'God Class',
3958
+ severity: 'error',
3959
+ detail: `${lines} lines - consider breaking into smaller classes`
3960
+ });
3961
+ }
3962
+ });
3963
+
3964
+ // Sort by severity (errors first) then by file
3965
+ complexityData.sort((a, b) => {
3966
+ const gradeOrder = { F: 0, D: 1, C: 2 };
3967
+ return (gradeOrder[a.grade] || 99) - (gradeOrder[b.grade] || 99);
3968
+ });
3969
+
3970
+ smells.sort((a, b) => {
3971
+ if (a.severity !== b.severity) {
3972
+ return a.severity === 'error' ? -1 : 1;
3973
+ }
3974
+ return a.file.localeCompare(b.file);
3975
+ });
3976
+
3977
+ // Generate Markdown
3978
+ const date = new Date().toISOString().split('T')[0];
3979
+ let markdown = `# Code Remediation Report
3980
+ Generated: ${date}
3981
+
3982
+ ## Summary
3983
+
3984
+ - **High Complexity Items**: ${complexityData.length}
3985
+ - **Code Smells Detected**: ${smells.length}
3986
+ - **Critical Issues (Errors)**: ${smells.filter(s => s.severity === 'error').length}
3987
+
3988
+ ---
3989
+
3990
+ ## 🔴 Priority: High Complexity Code
3991
+
3992
+ These functions/methods have complexity scores that make them difficult to maintain and test.
3993
+
3994
+ | Grade | Name | File | Complexity | Lines |
3995
+ |-------|------|------|------------|-------|
3996
+ `;
3997
+
3998
+ complexityData.forEach(item => {
3999
+ const gradeEmoji = item.grade === 'F' ? '🔴' : item.grade === 'D' ? '🟠' : '🟡';
4000
+ markdown += `| ${gradeEmoji} ${item.grade} | \\`${item.name}\\` | ${item.file} | ${item.complexity} | ${item.lines} |\\n`;
4001
+ });
4002
+
4003
+ markdown += `
4004
+ ---
4005
+
4006
+ ## 🔍 Code Smells
4007
+
4008
+ ### Critical Issues (Errors)
4009
+
4010
+ `;
4011
+
4012
+ const errors = smells.filter(s => s.severity === 'error');
4013
+ if (errors.length === 0) {
4014
+ markdown += '_No critical issues found._\\n';
4015
+ } else {
4016
+ markdown += '| Smell | Name | File | Detail |\\n|-------|------|------|--------|\\n';
4017
+ errors.forEach(s => {
4018
+ markdown += `| 🔴 ${s.smell} | \\`${s.name}\\` | ${s.file} | ${s.detail} |\\n`;
4019
+ });
4020
+ }
4021
+
4022
+ markdown += `
4023
+ ### Warnings
4024
+
4025
+ `;
4026
+
4027
+ const warnings = smells.filter(s => s.severity === 'warning');
4028
+ if (warnings.length === 0) {
4029
+ markdown += '_No warnings found._\\n';
4030
+ } else {
4031
+ markdown += '| Smell | Name | File | Detail |\\n|-------|------|------|--------|\\n';
4032
+ warnings.forEach(s => {
4033
+ markdown += `| 🟡 ${s.smell} | \\`${s.name}\\` | ${s.file} | ${s.detail} |\\n`;
4034
+ });
4035
+ }
4036
+
4037
+ markdown += `
4038
+ ---
4039
+
4040
+ ## Recommended Actions
4041
+
4042
+ 1. **Start with Grade F items** - These have the highest complexity and are hardest to maintain
4043
+ 2. **Address Critical code smells** - God Classes and deeply nested code should be refactored
4044
+ 3. **Break down long methods** - Extract helper functions to reduce complexity
4045
+ 4. **Add tests before refactoring** - Ensure behavior is preserved
4046
+
4047
+ ---
4048
+
4049
+ _Generated by MCP Vector Search Visualization_
4050
+ `;
4051
+
4052
+ // Save the file with dialog (or fallback to download)
4053
+ const blob = new Blob([markdown], { type: 'text/markdown' });
4054
+ const defaultFilename = `remediation-report-${date}.md`;
4055
+
4056
+ async function saveWithDialog() {
4057
+ try {
4058
+ // Use File System Access API if available (shows save dialog)
4059
+ if ('showSaveFilePicker' in window) {
4060
+ const handle = await window.showSaveFilePicker({
4061
+ suggestedName: defaultFilename,
4062
+ types: [{
4063
+ description: 'Markdown files',
4064
+ accept: { 'text/markdown': ['.md'] }
4065
+ }]
4066
+ });
4067
+ const writable = await handle.createWritable();
4068
+ await writable.write(blob);
4069
+ await writable.close();
4070
+ return handle.name;
4071
+ }
4072
+ } catch (err) {
4073
+ if (err.name === 'AbortError') {
4074
+ return null; // User cancelled
4075
+ }
4076
+ console.warn('Save dialog failed, falling back to download:', err);
4077
+ }
4078
+
4079
+ // Fallback: standard download
4080
+ const url = URL.createObjectURL(blob);
4081
+ const a = document.createElement('a');
4082
+ a.href = url;
4083
+ a.download = defaultFilename;
4084
+ document.body.appendChild(a);
4085
+ a.click();
4086
+ document.body.removeChild(a);
4087
+ URL.revokeObjectURL(url);
4088
+ return defaultFilename;
4089
+ }
4090
+
4091
+ saveWithDialog().then(savedFilename => {
4092
+ if (savedFilename === null) {
4093
+ // User cancelled - don't show confirmation
4094
+ return;
4095
+ }
4096
+
4097
+ // Show confirmation
4098
+ openViewerPanel();
4099
+ document.getElementById('viewer-title').textContent = '📋 Report Saved';
4100
+ document.getElementById('viewer-content').innerHTML = `
4101
+ <div class="report-section">
4102
+ <h3>✅ Remediation Report Generated</h3>
4103
+ <p>The report has been saved as <code>${escapeHtml(savedFilename)}</code></p>
4104
+ <div class="metrics-grid">
4105
+ <div class="metric-card">
4106
+ <div class="metric-value">${complexityData.length}</div>
4107
+ <div class="metric-label">Complexity Issues</div>
4108
+ </div>
4109
+ <div class="metric-card">
4110
+ <div class="metric-value">${smells.length}</div>
4111
+ <div class="metric-label">Code Smells</div>
4112
+ </div>
4113
+ <div class="metric-card">
4114
+ <div class="metric-value">${errors.length}</div>
4115
+ <div class="metric-label">Critical Errors</div>
4116
+ </div>
4117
+ </div>
4118
+ <p style="margin-top: 15px; color: var(--text-secondary);">Share this report with your team for prioritized remediation.</p>
4119
+ </div>
4120
+ `;
4121
+
4122
+ // Hide section dropdown for reports (no code sections)
4123
+ const sectionNav = document.getElementById('section-nav');
4124
+ if (sectionNav) {
4125
+ sectionNav.style.display = 'none';
4126
+ }
4127
+ });
4128
+ }
4129
+
4130
+ // Render trend line charts using D3
4131
+ function renderTrendCharts(entries) {
4132
+ // Chart dimensions
4133
+ const margin = {top: 20, right: 30, bottom: 40, left: 50};
4134
+ const width = 600 - margin.left - margin.right;
4135
+ const height = 250 - margin.top - margin.bottom;
4136
+
4137
+ // Parse dates
4138
+ const parseDate = d3.timeParse('%Y-%m-%d');
4139
+ entries.forEach(d => {
4140
+ d.parsedDate = parseDate(d.date);
4141
+ });
4142
+
4143
+ // 1. Health Score Chart
4144
+ renderLineChart('#health-score-chart', entries, {
4145
+ title: 'Health Score Over Time',
4146
+ width, height, margin,
4147
+ yAccessor: d => d.metrics.health_score || 0,
4148
+ yLabel: 'Health Score',
4149
+ color: '#238636',
4150
+ yDomain: [0, 100]
4151
+ });
4152
+
4153
+ // 2. Average Complexity Chart
4154
+ renderLineChart('#complexity-chart', entries, {
4155
+ title: 'Average Complexity Over Time',
4156
+ width, height, margin,
4157
+ yAccessor: d => d.metrics.avg_complexity || 0,
4158
+ yLabel: 'Avg Complexity',
4159
+ color: '#d29922',
4160
+ yDomain: [0, d3.max(entries, d => d.metrics.avg_complexity || 0) * 1.1]
4161
+ });
4162
+
4163
+ // 3. Files and Chunks Chart (dual line)
4164
+ renderDualLineChart('#files-chunks-chart', entries, {
4165
+ title: 'Files and Chunks Over Time',
4166
+ width, height, margin,
4167
+ y1Accessor: d => d.metrics.total_files || 0,
4168
+ y2Accessor: d => d.metrics.total_chunks || 0,
4169
+ y1Label: 'Files',
4170
+ y2Label: 'Chunks',
4171
+ color1: '#1f6feb',
4172
+ color2: '#8957e5'
4173
+ });
4174
+ }
4175
+
4176
+ // Render single line chart
4177
+ function renderLineChart(selector, data, config) {
4178
+ const svg = d3.select(selector)
4179
+ .append('svg')
4180
+ .attr('width', config.width + config.margin.left + config.margin.right)
4181
+ .attr('height', config.height + config.margin.top + config.margin.bottom)
4182
+ .append('g')
4183
+ .attr('transform', `translate(${config.margin.left},${config.margin.top})`);
4184
+
4185
+ // Add title
4186
+ svg.append('text')
4187
+ .attr('x', config.width / 2)
4188
+ .attr('y', -5)
4189
+ .attr('text-anchor', 'middle')
4190
+ .style('font-size', '14px')
4191
+ .style('font-weight', 'bold')
4192
+ .text(config.title);
4193
+
4194
+ // Create scales
4195
+ const xScale = d3.scaleTime()
4196
+ .domain(d3.extent(data, d => d.parsedDate))
4197
+ .range([0, config.width]);
4198
+
4199
+ const yScale = d3.scaleLinear()
4200
+ .domain(config.yDomain || [0, d3.max(data, config.yAccessor)])
4201
+ .range([config.height, 0]);
4202
+
4203
+ // Create line generator
4204
+ const line = d3.line()
4205
+ .x(d => xScale(d.parsedDate))
4206
+ .y(d => yScale(config.yAccessor(d)))
4207
+ .curve(d3.curveMonotoneX);
4208
+
4209
+ // Add X axis
4210
+ svg.append('g')
4211
+ .attr('transform', `translate(0,${config.height})`)
4212
+ .call(d3.axisBottom(xScale).ticks(5).tickFormat(d3.timeFormat('%b %d')))
4213
+ .selectAll('text')
4214
+ .style('font-size', '11px');
4215
+
4216
+ // Add Y axis
4217
+ svg.append('g')
4218
+ .call(d3.axisLeft(yScale).ticks(5))
4219
+ .selectAll('text')
4220
+ .style('font-size', '11px');
4221
+
4222
+ // Add Y axis label
4223
+ svg.append('text')
4224
+ .attr('transform', 'rotate(-90)')
4225
+ .attr('y', 0 - config.margin.left + 10)
4226
+ .attr('x', 0 - (config.height / 2))
4227
+ .attr('dy', '1em')
4228
+ .style('text-anchor', 'middle')
4229
+ .style('font-size', '12px')
4230
+ .text(config.yLabel);
4231
+
4232
+ // Add line path
4233
+ svg.append('path')
4234
+ .datum(data)
4235
+ .attr('fill', 'none')
4236
+ .attr('stroke', config.color)
4237
+ .attr('stroke-width', 2)
4238
+ .attr('d', line);
4239
+
4240
+ // Add dots
4241
+ svg.selectAll('.dot')
4242
+ .data(data)
4243
+ .enter().append('circle')
4244
+ .attr('class', 'dot')
4245
+ .attr('cx', d => xScale(d.parsedDate))
4246
+ .attr('cy', d => yScale(config.yAccessor(d)))
4247
+ .attr('r', 4)
4248
+ .attr('fill', config.color)
4249
+ .style('cursor', 'pointer')
4250
+ .append('title')
4251
+ .text(d => `${d.date}: ${config.yAccessor(d).toFixed(1)}`);
4252
+ }
4253
+
4254
+ // Render dual line chart (two Y axes)
4255
+ function renderDualLineChart(selector, data, config) {
4256
+ const svg = d3.select(selector)
4257
+ .append('svg')
4258
+ .attr('width', config.width + config.margin.left + config.margin.right)
4259
+ .attr('height', config.height + config.margin.top + config.margin.bottom)
4260
+ .append('g')
4261
+ .attr('transform', `translate(${config.margin.left},${config.margin.top})`);
4262
+
4263
+ // Add title
4264
+ svg.append('text')
4265
+ .attr('x', config.width / 2)
4266
+ .attr('y', -5)
4267
+ .attr('text-anchor', 'middle')
4268
+ .style('font-size', '14px')
4269
+ .style('font-weight', 'bold')
4270
+ .text(config.title);
4271
+
4272
+ // Create scales
4273
+ const xScale = d3.scaleTime()
4274
+ .domain(d3.extent(data, d => d.parsedDate))
4275
+ .range([0, config.width]);
4276
+
4277
+ const y1Scale = d3.scaleLinear()
4278
+ .domain([0, d3.max(data, config.y1Accessor) * 1.1])
4279
+ .range([config.height, 0]);
4280
+
4281
+ const y2Scale = d3.scaleLinear()
4282
+ .domain([0, d3.max(data, config.y2Accessor) * 1.1])
4283
+ .range([config.height, 0]);
4284
+
4285
+ // Create line generators
4286
+ const line1 = d3.line()
4287
+ .x(d => xScale(d.parsedDate))
4288
+ .y(d => y1Scale(config.y1Accessor(d)))
4289
+ .curve(d3.curveMonotoneX);
4290
+
4291
+ const line2 = d3.line()
4292
+ .x(d => xScale(d.parsedDate))
4293
+ .y(d => y2Scale(config.y2Accessor(d)))
4294
+ .curve(d3.curveMonotoneX);
4295
+
4296
+ // Add X axis
4297
+ svg.append('g')
4298
+ .attr('transform', `translate(0,${config.height})`)
4299
+ .call(d3.axisBottom(xScale).ticks(5).tickFormat(d3.timeFormat('%b %d')))
4300
+ .selectAll('text')
4301
+ .style('font-size', '11px');
4302
+
4303
+ // Add Y1 axis (left)
4304
+ svg.append('g')
4305
+ .call(d3.axisLeft(y1Scale).ticks(5))
4306
+ .selectAll('text')
4307
+ .style('font-size', '11px')
4308
+ .style('fill', config.color1);
4309
+
4310
+ // Add Y2 axis (right)
4311
+ svg.append('g')
4312
+ .attr('transform', `translate(${config.width},0)`)
4313
+ .call(d3.axisRight(y2Scale).ticks(5))
4314
+ .selectAll('text')
4315
+ .style('font-size', '11px')
4316
+ .style('fill', config.color2);
4317
+
4318
+ // Add line 1
4319
+ svg.append('path')
4320
+ .datum(data)
4321
+ .attr('fill', 'none')
4322
+ .attr('stroke', config.color1)
4323
+ .attr('stroke-width', 2)
4324
+ .attr('d', line1);
4325
+
4326
+ // Add line 2
4327
+ svg.append('path')
4328
+ .datum(data)
4329
+ .attr('fill', 'none')
4330
+ .attr('stroke', config.color2)
4331
+ .attr('stroke-width', 2)
4332
+ .attr('stroke-dasharray', '5,5')
4333
+ .attr('d', line2);
4334
+
4335
+ // Add legend
4336
+ const legend = svg.append('g')
4337
+ .attr('transform', `translate(${config.width - 120}, 10)`);
4338
+
4339
+ legend.append('line')
4340
+ .attr('x1', 0).attr('x2', 20)
4341
+ .attr('y1', 5).attr('y2', 5)
4342
+ .attr('stroke', config.color1)
4343
+ .attr('stroke-width', 2);
4344
+ legend.append('text')
4345
+ .attr('x', 25).attr('y', 9)
4346
+ .style('font-size', '11px')
4347
+ .text(config.y1Label);
4348
+
4349
+ legend.append('line')
4350
+ .attr('x1', 0).attr('x2', 20)
4351
+ .attr('y1', 20).attr('y2', 20)
4352
+ .attr('stroke', config.color2)
4353
+ .attr('stroke-width', 2)
4354
+ .attr('stroke-dasharray', '5,5');
4355
+ legend.append('text')
4356
+ .attr('x', 25).attr('y', 24)
4357
+ .style('font-size', '11px')
4358
+ .text(config.y2Label);
4359
+ }
4360
+
4361
+ function calculateCodebaseMetrics() {
4362
+ const metrics = {
4363
+ totalLines: 0,
4364
+ totalFunctions: 0,
4365
+ totalClasses: 0,
4366
+ totalFiles: 0,
4367
+ complexityDistribution: { A: 0, B: 0, C: 0, D: 0, F: 0 },
4368
+ sizeDistribution: { small: 0, medium: 0, large: 0, veryLarge: 0 },
4369
+ healthScore: 0
4370
+ };
4371
+
4372
+ const chunksWithComplexity = [];
4373
+
4374
+ function analyzeNode(node) {
4375
+ // Count files
4376
+ if (node.type === 'file') {
4377
+ metrics.totalFiles++;
4378
+ }
4379
+
4380
+ // Count classes
4381
+ if (node.type === 'class') {
4382
+ metrics.totalClasses++;
4383
+ }
4384
+
4385
+ // Count functions and methods
4386
+ if (node.type === 'function' || node.type === 'method') {
4387
+ metrics.totalFunctions++;
4388
+
4389
+ // Calculate lines
4390
+ const lineCount = (node.end_line && node.start_line)
4391
+ ? node.end_line - node.start_line + 1
4392
+ : 0;
4393
+ metrics.totalLines += lineCount;
4394
+
4395
+ // Size distribution
4396
+ if (lineCount <= 20) {
4397
+ metrics.sizeDistribution.small++;
4398
+ } else if (lineCount <= 50) {
4399
+ metrics.sizeDistribution.medium++;
4400
+ } else if (lineCount <= 100) {
4401
+ metrics.sizeDistribution.large++;
4402
+ } else {
4403
+ metrics.sizeDistribution.veryLarge++;
4404
+ }
4405
+
4406
+ // Complexity distribution
4407
+ if (node.complexity !== undefined && node.complexity !== null) {
4408
+ const grade = getComplexityGrade(node.complexity);
4409
+ if (grade in metrics.complexityDistribution) {
4410
+ metrics.complexityDistribution[grade]++;
4411
+ }
4412
+ chunksWithComplexity.push(node);
4413
+ }
4414
+ }
4415
+
4416
+ // Recursively process children
4417
+ const children = node.children || node._children || [];
4418
+ children.forEach(child => analyzeNode(child));
4419
+ }
4420
+
4421
+ if (treeData) {
4422
+ analyzeNode(treeData);
4423
+ }
4424
+
4425
+ // Convert complexity counts to percentages
4426
+ const totalWithComplexity = chunksWithComplexity.length;
4427
+ if (totalWithComplexity > 0) {
4428
+ Object.keys(metrics.complexityDistribution).forEach(grade => {
4429
+ metrics.complexityDistribution[grade] =
4430
+ (metrics.complexityDistribution[grade] / totalWithComplexity) * 100;
4431
+ });
4432
+ }
4433
+
4434
+ // Convert size counts to percentages
4435
+ const totalFuncs = metrics.totalFunctions;
4436
+ if (totalFuncs > 0) {
4437
+ Object.keys(metrics.sizeDistribution).forEach(size => {
4438
+ metrics.sizeDistribution[size] =
4439
+ (metrics.sizeDistribution[size] / totalFuncs) * 100;
4440
+ });
4441
+ }
4442
+
4443
+ // Calculate health score
4444
+ metrics.healthScore = calculateHealthScore(chunksWithComplexity);
4445
+
4446
+ return metrics;
4447
+ }
4448
+
4449
+ function calculateHealthScore(chunks) {
4450
+ if (chunks.length === 0) return 100;
4451
+
4452
+ let score = 0;
4453
+ chunks.forEach(chunk => {
4454
+ const grade = getComplexityGrade(chunk.complexity);
4455
+ const gradeScores = { A: 100, B: 80, C: 60, D: 40, F: 20 };
4456
+ score += gradeScores[grade] || 50;
4457
+ });
4458
+
4459
+ return Math.round(score / chunks.length);
4460
+ }
4461
+
4462
+ function getHealthScoreInfo(score) {
4463
+ if (score >= 80) {
4464
+ return {
4465
+ emoji: '🟢',
4466
+ label: 'Excellent',
4467
+ color: '#238636',
4468
+ description: 'Your codebase has excellent complexity distribution with most code in the A-B range.'
4469
+ };
4470
+ } else if (score >= 60) {
4471
+ return {
4472
+ emoji: '🟡',
4473
+ label: 'Good',
4474
+ color: '#d29922',
4475
+ description: 'Your codebase is in good shape, but could benefit from refactoring some complex functions.'
4476
+ };
4477
+ } else if (score >= 40) {
4478
+ return {
4479
+ emoji: '🟠',
4480
+ label: 'Needs Attention',
4481
+ color: '#f0883e',
4482
+ description: 'Your codebase has significant complexity issues that should be addressed soon.'
4483
+ };
4484
+ } else {
4485
+ return {
4486
+ emoji: '🔴',
4487
+ label: 'Critical',
4488
+ color: '#da3633',
4489
+ description: 'Your codebase has critical complexity issues requiring immediate refactoring.'
4490
+ };
4491
+ }
4492
+ }
4493
+
4494
+ function getComplexityRange(grade) {
4495
+ const ranges = {
4496
+ 'A': '1-5',
4497
+ 'B': '6-10',
4498
+ 'C': '11-15',
4499
+ 'D': '16-20',
4500
+ 'F': '21+'
4501
+ };
4502
+ return ranges[grade] || '';
4503
+ }
4504
+
4505
+ function showComingSoon(reportName) {
4506
+ alert(`${reportName} - Coming Soon!\n\nThis feature will display detailed ${reportName.toLowerCase()} in a future release.`);
4507
+ }
4508
+
4509
+ // ============================================================================
4510
+ // INITIALIZATION
4511
+ // ============================================================================
4512
+
4513
+ // Load data and initialize UI when page loads
4514
+ document.addEventListener('DOMContentLoaded', () => {
4515
+ console.log('=== PAGE INITIALIZATION ===');
4516
+ console.log('DOMContentLoaded event fired');
4517
+
4518
+ // Load theme preference before anything else
4519
+ loadThemePreference();
2485
4520
 
2486
4521
  // Initialize toggle label highlighting
2487
4522
  const labels = document.querySelectorAll('.toggle-label');