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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +48 -1
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +35 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +74 -1
- mcp_vector_search/analysis/reporters/__init__.py +3 -1
- mcp_vector_search/analysis/reporters/console.py +424 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +665 -11
- mcp_vector_search/cli/commands/chat.py +193 -0
- mcp_vector_search/cli/commands/index.py +600 -2
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/search.py +194 -1
- mcp_vector_search/cli/commands/setup.py +64 -13
- mcp_vector_search/cli/commands/status.py +302 -3
- mcp_vector_search/cli/commands/visualize/cli.py +26 -10
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
- mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
- mcp_vector_search/cli/commands/visualize/server.py +304 -15
- mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
- mcp_vector_search/cli/didyoumean.py +5 -0
- mcp_vector_search/cli/main.py +16 -5
- mcp_vector_search/cli/output.py +134 -5
- mcp_vector_search/config/thresholds.py +89 -1
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/database.py +39 -2
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/indexer.py +445 -84
- mcp_vector_search/core/llm_client.py +9 -4
- mcp_vector_search/core/models.py +88 -1
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/search.py +1 -1
- mcp_vector_search/mcp/server.py +795 -4
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/gitignore.py +0 -3
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
- mcp_vector_search/cli/commands/visualize.py.original +0 -2536
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
- {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:
|
|
61
|
-
chunkMaxRadius:
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
206
|
-
//
|
|
207
|
-
category = '
|
|
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(`
|
|
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
|
|
383
|
-
// This
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
913
|
-
let minR, maxR;
|
|
1052
|
+
// Only chunks have direct complexity
|
|
914
1053
|
if (chunkTypes.includes(nodeData.type)) {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
928
|
-
return (minR + maxR) / 2; // Default if no range
|
|
1153
|
+
return fileMinRadius + (normalized * (fileMaxRadius - fileMinRadius));
|
|
929
1154
|
}
|
|
930
1155
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
//
|
|
940
|
-
|
|
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
|
-
//
|
|
944
|
-
return
|
|
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',
|
|
1051
|
-
.attr('stroke-width',
|
|
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',
|
|
1191
|
-
.attr('stroke-width',
|
|
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
|
|
1872
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2968
|
+
// THEME TOGGLE
|
|
2479
2969
|
// ============================================================================
|
|
2480
2970
|
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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');
|