@rangerchaz/aimem 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +380 -0
- package/dist/cli/commands/git.d.ts +6 -0
- package/dist/cli/commands/git.d.ts.map +1 -0
- package/dist/cli/commands/git.js +298 -0
- package/dist/cli/commands/git.js.map +1 -0
- package/dist/cli/commands/hook-session-end.d.ts +7 -0
- package/dist/cli/commands/hook-session-end.d.ts.map +1 -0
- package/dist/cli/commands/hook-session-end.js +109 -0
- package/dist/cli/commands/hook-session-end.js.map +1 -0
- package/dist/cli/commands/hook-session-start.d.ts +7 -0
- package/dist/cli/commands/hook-session-start.d.ts.map +1 -0
- package/dist/cli/commands/hook-session-start.js +116 -0
- package/dist/cli/commands/hook-session-start.js.map +1 -0
- package/dist/cli/commands/import.d.ts +14 -0
- package/dist/cli/commands/import.d.ts.map +1 -0
- package/dist/cli/commands/import.js +527 -0
- package/dist/cli/commands/import.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +32 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/mcp-serve.d.ts +2 -0
- package/dist/cli/commands/mcp-serve.d.ts.map +1 -0
- package/dist/cli/commands/mcp-serve.js +5 -0
- package/dist/cli/commands/mcp-serve.js.map +1 -0
- package/dist/cli/commands/query.d.ts +8 -0
- package/dist/cli/commands/query.d.ts.map +1 -0
- package/dist/cli/commands/query.js +83 -0
- package/dist/cli/commands/query.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +10 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +504 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/start.d.ts +8 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +90 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +85 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +7 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +46 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/visualize.d.ts +8 -0
- package/dist/cli/commands/visualize.d.ts.map +1 -0
- package/dist/cli/commands/visualize.js +96 -0
- package/dist/cli/commands/visualize.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +114 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/db/index.d.ts +55 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +464 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +4 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +200 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/extractor/index.d.ts +27 -0
- package/dist/extractor/index.d.ts.map +1 -0
- package/dist/extractor/index.js +227 -0
- package/dist/extractor/index.js.map +1 -0
- package/dist/git/extractor.d.ts +30 -0
- package/dist/git/extractor.d.ts.map +1 -0
- package/dist/git/extractor.js +126 -0
- package/dist/git/extractor.js.map +1 -0
- package/dist/git/hooks.d.ts +36 -0
- package/dist/git/hooks.d.ts.map +1 -0
- package/dist/git/hooks.js +142 -0
- package/dist/git/hooks.js.map +1 -0
- package/dist/git/index.d.ts +69 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +250 -0
- package/dist/git/index.js.map +1 -0
- package/dist/indexer/index.d.ts +20 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +173 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/parsers/base.d.ts +19 -0
- package/dist/indexer/parsers/base.d.ts.map +1 -0
- package/dist/indexer/parsers/base.js +46 -0
- package/dist/indexer/parsers/base.js.map +1 -0
- package/dist/indexer/parsers/cpp.d.ts +3 -0
- package/dist/indexer/parsers/cpp.d.ts.map +1 -0
- package/dist/indexer/parsers/cpp.js +180 -0
- package/dist/indexer/parsers/cpp.js.map +1 -0
- package/dist/indexer/parsers/go.d.ts +3 -0
- package/dist/indexer/parsers/go.d.ts.map +1 -0
- package/dist/indexer/parsers/go.js +98 -0
- package/dist/indexer/parsers/go.js.map +1 -0
- package/dist/indexer/parsers/java.d.ts +3 -0
- package/dist/indexer/parsers/java.d.ts.map +1 -0
- package/dist/indexer/parsers/java.js +204 -0
- package/dist/indexer/parsers/java.js.map +1 -0
- package/dist/indexer/parsers/javascript.d.ts +3 -0
- package/dist/indexer/parsers/javascript.d.ts.map +1 -0
- package/dist/indexer/parsers/javascript.js +157 -0
- package/dist/indexer/parsers/javascript.js.map +1 -0
- package/dist/indexer/parsers/kotlin.d.ts +3 -0
- package/dist/indexer/parsers/kotlin.d.ts.map +1 -0
- package/dist/indexer/parsers/kotlin.js +182 -0
- package/dist/indexer/parsers/kotlin.js.map +1 -0
- package/dist/indexer/parsers/php.d.ts +3 -0
- package/dist/indexer/parsers/php.d.ts.map +1 -0
- package/dist/indexer/parsers/php.js +190 -0
- package/dist/indexer/parsers/php.js.map +1 -0
- package/dist/indexer/parsers/python.d.ts +3 -0
- package/dist/indexer/parsers/python.d.ts.map +1 -0
- package/dist/indexer/parsers/python.js +101 -0
- package/dist/indexer/parsers/python.js.map +1 -0
- package/dist/indexer/parsers/ruby.d.ts +3 -0
- package/dist/indexer/parsers/ruby.d.ts.map +1 -0
- package/dist/indexer/parsers/ruby.js +92 -0
- package/dist/indexer/parsers/ruby.js.map +1 -0
- package/dist/indexer/parsers/rust.d.ts +3 -0
- package/dist/indexer/parsers/rust.d.ts.map +1 -0
- package/dist/indexer/parsers/rust.js +190 -0
- package/dist/indexer/parsers/rust.js.map +1 -0
- package/dist/indexer/watcher-daemon.d.ts +2 -0
- package/dist/indexer/watcher-daemon.d.ts.map +1 -0
- package/dist/indexer/watcher-daemon.js +27 -0
- package/dist/indexer/watcher-daemon.js.map +1 -0
- package/dist/indexer/watcher.d.ts +7 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +77 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +241 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/proxy/interceptor-mockttp.d.ts +27 -0
- package/dist/proxy/interceptor-mockttp.d.ts.map +1 -0
- package/dist/proxy/interceptor-mockttp.js +274 -0
- package/dist/proxy/interceptor-mockttp.js.map +1 -0
- package/dist/proxy/proxy-daemon.d.ts +5 -0
- package/dist/proxy/proxy-daemon.d.ts.map +1 -0
- package/dist/proxy/proxy-daemon.js +26 -0
- package/dist/proxy/proxy-daemon.js.map +1 -0
- package/dist/query/index.d.ts +32 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +135 -0
- package/dist/query/index.js.map +1 -0
- package/dist/types/index.d.ts +89 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/visualize/index.d.ts +144 -0
- package/dist/visualize/index.d.ts.map +1 -0
- package/dist/visualize/index.js +707 -0
- package/dist/visualize/index.js.map +1 -0
- package/dist/visualize/server.d.ts +7 -0
- package/dist/visualize/server.d.ts.map +1 -0
- package/dist/visualize/server.js +77 -0
- package/dist/visualize/server.js.map +1 -0
- package/dist/visualize/template.d.ts +3 -0
- package/dist/visualize/template.d.ts.map +1 -0
- package/dist/visualize/template.js +3465 -0
- package/dist/visualize/template.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import { generateDashboardHTML } from './template.js';
|
|
2
|
+
// Build graph data from database records
|
|
3
|
+
export function buildVisualizationData(project, structures, files, links, extractions, conversations = []) {
|
|
4
|
+
// Count structures by type
|
|
5
|
+
const byType = {};
|
|
6
|
+
for (const s of structures) {
|
|
7
|
+
byType[s.type] = (byType[s.type] || 0) + 1;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
project,
|
|
11
|
+
graphs: {
|
|
12
|
+
overview: buildOverviewGraph(structures, files),
|
|
13
|
+
callGraph: buildCallGraph(structures, links),
|
|
14
|
+
dependencies: buildDependencyGraph(structures, files),
|
|
15
|
+
classes: buildClassGraph(structures, links),
|
|
16
|
+
decisions: buildDecisionGraph(structures, extractions, links),
|
|
17
|
+
},
|
|
18
|
+
// New visualization data
|
|
19
|
+
smells: buildCodeSmellsData(structures, files, links),
|
|
20
|
+
hotspots: buildHotspotsData(structures, links),
|
|
21
|
+
gallery: buildGalleryData(extractions, links, structures, conversations),
|
|
22
|
+
timeline: buildTimelineData(conversations, links, structures, extractions),
|
|
23
|
+
treemap: buildTreemapData(structures),
|
|
24
|
+
stats: {
|
|
25
|
+
totalStructures: structures.length,
|
|
26
|
+
totalFiles: files.length,
|
|
27
|
+
totalLinks: links.length,
|
|
28
|
+
totalDecisions: extractions.filter(e => e.type === 'decision').length,
|
|
29
|
+
totalConversations: conversations.length,
|
|
30
|
+
byType,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Overview: Files only (cleaner view) - structures shown on click via details panel
|
|
35
|
+
function buildOverviewGraph(structures, files) {
|
|
36
|
+
const nodes = [];
|
|
37
|
+
const edges = [];
|
|
38
|
+
const fileMap = new Map();
|
|
39
|
+
// Count structures per file
|
|
40
|
+
for (const s of structures) {
|
|
41
|
+
if (!fileMap.has(s.file_path)) {
|
|
42
|
+
fileMap.set(s.file_path, { count: 0, types: new Set() });
|
|
43
|
+
}
|
|
44
|
+
const info = fileMap.get(s.file_path);
|
|
45
|
+
info.count++;
|
|
46
|
+
info.types.add(s.type);
|
|
47
|
+
}
|
|
48
|
+
// Group files by directory for edges
|
|
49
|
+
const dirMap = new Map();
|
|
50
|
+
for (const filePath of fileMap.keys()) {
|
|
51
|
+
const dir = getDirectory(filePath);
|
|
52
|
+
if (!dirMap.has(dir)) {
|
|
53
|
+
dirMap.set(dir, []);
|
|
54
|
+
}
|
|
55
|
+
dirMap.get(dir).push(filePath);
|
|
56
|
+
}
|
|
57
|
+
// Add file nodes with structure counts
|
|
58
|
+
for (const [filePath, info] of fileMap) {
|
|
59
|
+
nodes.push({
|
|
60
|
+
data: {
|
|
61
|
+
id: `file:${filePath}`,
|
|
62
|
+
label: `${getFileName(filePath)} (${info.count})`,
|
|
63
|
+
type: 'file',
|
|
64
|
+
file: filePath,
|
|
65
|
+
content: `Contains ${info.count} structures: ${Array.from(info.types).join(', ')}`,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return { nodes, edges };
|
|
70
|
+
}
|
|
71
|
+
// Call graph: Function/method call relationships
|
|
72
|
+
function buildCallGraph(structures, links) {
|
|
73
|
+
const nodes = [];
|
|
74
|
+
const edges = [];
|
|
75
|
+
const structureMap = new Map();
|
|
76
|
+
// Build lookup map for functions/methods only
|
|
77
|
+
for (const s of structures) {
|
|
78
|
+
if (s.type === 'function' || s.type === 'method') {
|
|
79
|
+
structureMap.set(s.id, s);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Collect which structures are involved in calls
|
|
83
|
+
const includedIds = new Set();
|
|
84
|
+
// Find call relationships - only include if both source and target exist
|
|
85
|
+
for (const link of links) {
|
|
86
|
+
if ((link.link_type === 'calls' || link.link_type === 'called_by') &&
|
|
87
|
+
link.source_type === 'structure' && link.target_type === 'structure') {
|
|
88
|
+
const sourceId = link.link_type === 'calls' ? link.source_id : link.target_id;
|
|
89
|
+
const targetId = link.link_type === 'calls' ? link.target_id : link.source_id;
|
|
90
|
+
// Only add edge if both nodes exist in our map
|
|
91
|
+
if (structureMap.has(sourceId) && structureMap.has(targetId) && sourceId !== targetId) {
|
|
92
|
+
includedIds.add(sourceId);
|
|
93
|
+
includedIds.add(targetId);
|
|
94
|
+
edges.push({
|
|
95
|
+
data: {
|
|
96
|
+
id: `call:${sourceId}:${targetId}`,
|
|
97
|
+
source: `structure:${sourceId}`,
|
|
98
|
+
target: `structure:${targetId}`,
|
|
99
|
+
type: 'calls',
|
|
100
|
+
label: 'calls',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Calculate connection weights for each node
|
|
107
|
+
const connectionCount = new Map();
|
|
108
|
+
for (const edge of edges) {
|
|
109
|
+
const sourceId = parseInt(edge.data.source.replace('structure:', ''));
|
|
110
|
+
const targetId = parseInt(edge.data.target.replace('structure:', ''));
|
|
111
|
+
connectionCount.set(sourceId, (connectionCount.get(sourceId) || 0) + 1);
|
|
112
|
+
connectionCount.set(targetId, (connectionCount.get(targetId) || 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
// Add nodes for structures involved in calls
|
|
115
|
+
for (const id of includedIds) {
|
|
116
|
+
const s = structureMap.get(id);
|
|
117
|
+
const connections = connectionCount.get(id) || 0;
|
|
118
|
+
nodes.push({
|
|
119
|
+
data: {
|
|
120
|
+
id: `structure:${s.id}`,
|
|
121
|
+
label: s.name,
|
|
122
|
+
type: s.type,
|
|
123
|
+
file: s.file_path,
|
|
124
|
+
line: s.line_start,
|
|
125
|
+
signature: s.signature || undefined,
|
|
126
|
+
content: s.raw_content,
|
|
127
|
+
weight: connections, // Used for node sizing
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// If no call links, show all functions/methods
|
|
132
|
+
if (nodes.length === 0) {
|
|
133
|
+
for (const s of structures) {
|
|
134
|
+
if (s.type === 'function' || s.type === 'method') {
|
|
135
|
+
nodes.push({
|
|
136
|
+
data: {
|
|
137
|
+
id: `structure:${s.id}`,
|
|
138
|
+
label: s.name,
|
|
139
|
+
type: s.type,
|
|
140
|
+
file: s.file_path,
|
|
141
|
+
line: s.line_start,
|
|
142
|
+
signature: s.signature || undefined,
|
|
143
|
+
content: s.raw_content,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { nodes, edges };
|
|
150
|
+
}
|
|
151
|
+
// Dependency graph: File-level relationships
|
|
152
|
+
function buildDependencyGraph(structures, files) {
|
|
153
|
+
const nodes = [];
|
|
154
|
+
const edges = [];
|
|
155
|
+
const fileSet = new Set();
|
|
156
|
+
// Get unique files from structures
|
|
157
|
+
for (const s of structures) {
|
|
158
|
+
fileSet.add(s.file_path);
|
|
159
|
+
}
|
|
160
|
+
// Add file nodes
|
|
161
|
+
for (const filePath of fileSet) {
|
|
162
|
+
nodes.push({
|
|
163
|
+
data: {
|
|
164
|
+
id: `file:${filePath}`,
|
|
165
|
+
label: getFileName(filePath),
|
|
166
|
+
type: 'file',
|
|
167
|
+
file: filePath,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// For now, just show files - import analysis would require parsing
|
|
172
|
+
return { nodes, edges };
|
|
173
|
+
}
|
|
174
|
+
// Class hierarchy: Classes and their methods
|
|
175
|
+
function buildClassGraph(structures, links) {
|
|
176
|
+
const nodes = [];
|
|
177
|
+
const edges = [];
|
|
178
|
+
const classIds = new Set();
|
|
179
|
+
const structureMap = new Map();
|
|
180
|
+
for (const s of structures) {
|
|
181
|
+
structureMap.set(s.id, s);
|
|
182
|
+
}
|
|
183
|
+
// Find classes and interfaces
|
|
184
|
+
for (const s of structures) {
|
|
185
|
+
if (s.type === 'class' || s.type === 'interface') {
|
|
186
|
+
classIds.add(s.id);
|
|
187
|
+
nodes.push({
|
|
188
|
+
data: {
|
|
189
|
+
id: `structure:${s.id}`,
|
|
190
|
+
label: s.name,
|
|
191
|
+
type: s.type,
|
|
192
|
+
file: s.file_path,
|
|
193
|
+
line: s.line_start,
|
|
194
|
+
signature: s.signature || undefined,
|
|
195
|
+
content: s.raw_content,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Find methods that belong to classes (by file proximity or metadata)
|
|
201
|
+
// This is a simplified approach - methods appear near their class
|
|
202
|
+
for (const s of structures) {
|
|
203
|
+
if (s.type === 'method') {
|
|
204
|
+
nodes.push({
|
|
205
|
+
data: {
|
|
206
|
+
id: `structure:${s.id}`,
|
|
207
|
+
label: s.name,
|
|
208
|
+
type: s.type,
|
|
209
|
+
file: s.file_path,
|
|
210
|
+
line: s.line_start,
|
|
211
|
+
signature: s.signature || undefined,
|
|
212
|
+
content: s.raw_content,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { nodes, edges };
|
|
218
|
+
}
|
|
219
|
+
// Decision graph: Decisions and affected code
|
|
220
|
+
function buildDecisionGraph(structures, extractions, links) {
|
|
221
|
+
const nodes = [];
|
|
222
|
+
const edges = [];
|
|
223
|
+
const structureMap = new Map();
|
|
224
|
+
const affectedStructures = new Set();
|
|
225
|
+
for (const s of structures) {
|
|
226
|
+
structureMap.set(s.id, s);
|
|
227
|
+
}
|
|
228
|
+
// Add extraction nodes (decisions, patterns, rejections)
|
|
229
|
+
for (const e of extractions) {
|
|
230
|
+
if (e.type === 'decision' || e.type === 'pattern' || e.type === 'rejection') {
|
|
231
|
+
nodes.push({
|
|
232
|
+
data: {
|
|
233
|
+
id: `extraction:${e.id}`,
|
|
234
|
+
label: truncate(e.content, 50),
|
|
235
|
+
type: e.type,
|
|
236
|
+
content: e.content,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Find links between extractions and structures
|
|
242
|
+
for (const link of links) {
|
|
243
|
+
if (link.source_type === 'extraction' && link.target_type === 'structure') {
|
|
244
|
+
affectedStructures.add(link.target_id);
|
|
245
|
+
edges.push({
|
|
246
|
+
data: {
|
|
247
|
+
id: `decision-link:${link.source_id}:${link.target_id}`,
|
|
248
|
+
source: `extraction:${link.source_id}`,
|
|
249
|
+
target: `structure:${link.target_id}`,
|
|
250
|
+
type: link.link_type,
|
|
251
|
+
label: link.link_type,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Add structure nodes that are affected by decisions
|
|
257
|
+
for (const id of affectedStructures) {
|
|
258
|
+
const s = structureMap.get(id);
|
|
259
|
+
if (s) {
|
|
260
|
+
nodes.push({
|
|
261
|
+
data: {
|
|
262
|
+
id: `structure:${s.id}`,
|
|
263
|
+
label: s.name,
|
|
264
|
+
type: s.type,
|
|
265
|
+
file: s.file_path,
|
|
266
|
+
line: s.line_start,
|
|
267
|
+
content: s.raw_content,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { nodes, edges };
|
|
273
|
+
}
|
|
274
|
+
// Helper: Get filename from path
|
|
275
|
+
function getFileName(filePath) {
|
|
276
|
+
const parts = filePath.split(/[/\\]/);
|
|
277
|
+
return parts[parts.length - 1] || filePath;
|
|
278
|
+
}
|
|
279
|
+
// Helper: Get directory from path
|
|
280
|
+
function getDirectory(filePath) {
|
|
281
|
+
const parts = filePath.split(/[/\\]/);
|
|
282
|
+
parts.pop();
|
|
283
|
+
return parts.join('/') || '.';
|
|
284
|
+
}
|
|
285
|
+
// Helper: Truncate string
|
|
286
|
+
function truncate(str, maxLen) {
|
|
287
|
+
if (str.length <= maxLen)
|
|
288
|
+
return str;
|
|
289
|
+
return str.slice(0, maxLen - 3) + '...';
|
|
290
|
+
}
|
|
291
|
+
// ============================================
|
|
292
|
+
// Code Smells Builder
|
|
293
|
+
// ============================================
|
|
294
|
+
function buildCodeSmellsData(structures, files, links) {
|
|
295
|
+
const thresholds = {
|
|
296
|
+
largeFile: 10, // >10 structures in a file
|
|
297
|
+
longFunction: 100, // >100 lines
|
|
298
|
+
tooManyCallers: 10, // >10 inbound calls
|
|
299
|
+
tooManyCallees: 10, // >10 outbound calls
|
|
300
|
+
};
|
|
301
|
+
const smells = [];
|
|
302
|
+
// Count structures per file
|
|
303
|
+
const structuresPerFile = new Map();
|
|
304
|
+
for (const s of structures) {
|
|
305
|
+
structuresPerFile.set(s.file_path, (structuresPerFile.get(s.file_path) || 0) + 1);
|
|
306
|
+
}
|
|
307
|
+
// Check for large files
|
|
308
|
+
for (const [filePath, count] of structuresPerFile) {
|
|
309
|
+
if (count > thresholds.largeFile) {
|
|
310
|
+
const severity = count > thresholds.largeFile * 2 ? 'high' : count > thresholds.largeFile * 1.5 ? 'medium' : 'low';
|
|
311
|
+
smells.push({
|
|
312
|
+
type: 'large-file',
|
|
313
|
+
severity,
|
|
314
|
+
filePath,
|
|
315
|
+
name: getFileName(filePath),
|
|
316
|
+
metric: count,
|
|
317
|
+
threshold: thresholds.largeFile,
|
|
318
|
+
description: `File has ${count} structures (threshold: ${thresholds.largeFile})`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Count inbound/outbound links per structure
|
|
323
|
+
const inboundCount = new Map();
|
|
324
|
+
const outboundCount = new Map();
|
|
325
|
+
for (const link of links) {
|
|
326
|
+
if (link.link_type === 'calls' && link.source_type === 'structure' && link.target_type === 'structure') {
|
|
327
|
+
outboundCount.set(link.source_id, (outboundCount.get(link.source_id) || 0) + 1);
|
|
328
|
+
inboundCount.set(link.target_id, (inboundCount.get(link.target_id) || 0) + 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Build structure map for lookups
|
|
332
|
+
const structureMap = new Map();
|
|
333
|
+
for (const s of structures) {
|
|
334
|
+
structureMap.set(s.id, s);
|
|
335
|
+
}
|
|
336
|
+
// Check each structure for smells
|
|
337
|
+
for (const s of structures) {
|
|
338
|
+
if (s.type !== 'function' && s.type !== 'method')
|
|
339
|
+
continue;
|
|
340
|
+
const lines = (s.line_end || s.line_start) - s.line_start + 1;
|
|
341
|
+
const inbound = inboundCount.get(s.id) || 0;
|
|
342
|
+
const outbound = outboundCount.get(s.id) || 0;
|
|
343
|
+
// Long function
|
|
344
|
+
if (lines > thresholds.longFunction) {
|
|
345
|
+
const severity = lines > thresholds.longFunction * 2 ? 'high' : lines > thresholds.longFunction * 1.5 ? 'medium' : 'low';
|
|
346
|
+
smells.push({
|
|
347
|
+
type: 'long-function',
|
|
348
|
+
severity,
|
|
349
|
+
structureId: s.id,
|
|
350
|
+
filePath: s.file_path,
|
|
351
|
+
name: s.name,
|
|
352
|
+
metric: lines,
|
|
353
|
+
threshold: thresholds.longFunction,
|
|
354
|
+
description: `Function has ${lines} lines (threshold: ${thresholds.longFunction})`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// Too many callers
|
|
358
|
+
if (inbound > thresholds.tooManyCallers) {
|
|
359
|
+
const severity = inbound > thresholds.tooManyCallers * 2 ? 'high' : inbound > thresholds.tooManyCallers * 1.5 ? 'medium' : 'low';
|
|
360
|
+
smells.push({
|
|
361
|
+
type: 'too-many-callers',
|
|
362
|
+
severity,
|
|
363
|
+
structureId: s.id,
|
|
364
|
+
filePath: s.file_path,
|
|
365
|
+
name: s.name,
|
|
366
|
+
metric: inbound,
|
|
367
|
+
threshold: thresholds.tooManyCallers,
|
|
368
|
+
description: `Function is called by ${inbound} others (threshold: ${thresholds.tooManyCallers})`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Too many callees
|
|
372
|
+
if (outbound > thresholds.tooManyCallees) {
|
|
373
|
+
const severity = outbound > thresholds.tooManyCallees * 2 ? 'high' : outbound > thresholds.tooManyCallees * 1.5 ? 'medium' : 'low';
|
|
374
|
+
smells.push({
|
|
375
|
+
type: 'too-many-callees',
|
|
376
|
+
severity,
|
|
377
|
+
structureId: s.id,
|
|
378
|
+
filePath: s.file_path,
|
|
379
|
+
name: s.name,
|
|
380
|
+
metric: outbound,
|
|
381
|
+
threshold: thresholds.tooManyCallees,
|
|
382
|
+
description: `Function calls ${outbound} others (threshold: ${thresholds.tooManyCallees})`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// Orphan code (not called by anything, and not an entry point)
|
|
386
|
+
// Note: Orphan detection has limited accuracy - cross-file and intra-file calls may not be tracked
|
|
387
|
+
const isEntryPoint = outbound > 0 && inbound === 0;
|
|
388
|
+
if (inbound === 0 && outbound === 0) {
|
|
389
|
+
// Skip likely false positives
|
|
390
|
+
const filePath = s.file_path.toLowerCase();
|
|
391
|
+
const funcName = s.name;
|
|
392
|
+
// Skip test files and fixtures
|
|
393
|
+
if (filePath.includes('/test/') || filePath.includes('.test.') || filePath.includes('.spec.') ||
|
|
394
|
+
filePath.includes('/fixtures/') || filePath.includes('/__tests__/')) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// Skip build output directories (check both /dist/ and paths starting with dist/)
|
|
398
|
+
if (filePath.includes('/dist/') || filePath.includes('/dist-test/') || filePath.includes('/build/') ||
|
|
399
|
+
filePath.includes('/node_modules/') || filePath.startsWith('dist/') || filePath.startsWith('dist-test/')) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// Skip common framework hooks (mitmproxy, express, etc.)
|
|
403
|
+
const frameworkHooks = ['request', 'response', 'load', 'configure', 'handler', 'middleware', 'setup', 'teardown'];
|
|
404
|
+
if (frameworkHooks.includes(funcName.toLowerCase())) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
// Skip parser artifacts and built-in property names
|
|
408
|
+
const parserArtifacts = ['name', 'return', 'constructor', 'toString', 'valueOf', 'length'];
|
|
409
|
+
if (parserArtifacts.includes(funcName)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// Skip Python private helper functions (likely called internally)
|
|
413
|
+
if (funcName.startsWith('_') && !funcName.startsWith('__')) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
// Skip functions with common utility prefixes (likely exported)
|
|
417
|
+
const utilityPrefixes = ['get', 'set', 'is', 'has', 'create', 'build', 'parse', 'format', 'validate', 'extract', 'find', 'ensure', 'init', 'setup', 'open', 'close', 'read', 'write', 'insert', 'update', 'delete', 'start', 'stop', 'store', 'escape', 'clean', 'deduplicate', 'encode', 'decode', 'render', 'load', 'save', 'fetch', 'send', 'handle', 'process', 'transform', 'convert', 'generate', 'calculate', 'compute'];
|
|
418
|
+
const lowerName = funcName.toLowerCase();
|
|
419
|
+
if (utilityPrefixes.some(p => lowerName.startsWith(p))) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
smells.push({
|
|
423
|
+
type: 'orphan',
|
|
424
|
+
severity: 'low',
|
|
425
|
+
structureId: s.id,
|
|
426
|
+
filePath: s.file_path,
|
|
427
|
+
name: s.name,
|
|
428
|
+
metric: 0,
|
|
429
|
+
threshold: 1,
|
|
430
|
+
description: 'No detected callers or callees (may be called dynamically or cross-file)',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Sort by severity (high first)
|
|
435
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
436
|
+
smells.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
437
|
+
return {
|
|
438
|
+
smells,
|
|
439
|
+
summary: {
|
|
440
|
+
high: smells.filter(s => s.severity === 'high').length,
|
|
441
|
+
medium: smells.filter(s => s.severity === 'medium').length,
|
|
442
|
+
low: smells.filter(s => s.severity === 'low').length,
|
|
443
|
+
},
|
|
444
|
+
thresholds,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
// ============================================
|
|
448
|
+
// Hotspots Builder
|
|
449
|
+
// ============================================
|
|
450
|
+
function buildHotspotsData(structures, links) {
|
|
451
|
+
// Count inbound/outbound links per structure
|
|
452
|
+
const inboundCount = new Map();
|
|
453
|
+
const outboundCount = new Map();
|
|
454
|
+
for (const link of links) {
|
|
455
|
+
if (link.link_type === 'calls' && link.source_type === 'structure' && link.target_type === 'structure') {
|
|
456
|
+
outboundCount.set(link.source_id, (outboundCount.get(link.source_id) || 0) + 1);
|
|
457
|
+
inboundCount.set(link.target_id, (inboundCount.get(link.target_id) || 0) + 1);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Count structures per file
|
|
461
|
+
const structuresPerFile = new Map();
|
|
462
|
+
for (const s of structures) {
|
|
463
|
+
structuresPerFile.set(s.file_path, (structuresPerFile.get(s.file_path) || 0) + 1);
|
|
464
|
+
}
|
|
465
|
+
// Largest functions by line count
|
|
466
|
+
const functionsWithLines = structures
|
|
467
|
+
.filter(s => s.type === 'function' || s.type === 'method')
|
|
468
|
+
.map(s => ({
|
|
469
|
+
id: s.id,
|
|
470
|
+
name: s.name,
|
|
471
|
+
file: s.file_path,
|
|
472
|
+
type: s.type,
|
|
473
|
+
lines: (s.line_end || s.line_start) - s.line_start + 1,
|
|
474
|
+
}))
|
|
475
|
+
.sort((a, b) => b.lines - a.lines)
|
|
476
|
+
.slice(0, 10);
|
|
477
|
+
// Most connected functions
|
|
478
|
+
const functionsWithConnections = structures
|
|
479
|
+
.filter(s => s.type === 'function' || s.type === 'method')
|
|
480
|
+
.map(s => ({
|
|
481
|
+
id: s.id,
|
|
482
|
+
name: s.name,
|
|
483
|
+
file: s.file_path,
|
|
484
|
+
type: s.type,
|
|
485
|
+
inbound: inboundCount.get(s.id) || 0,
|
|
486
|
+
outbound: outboundCount.get(s.id) || 0,
|
|
487
|
+
total: (inboundCount.get(s.id) || 0) + (outboundCount.get(s.id) || 0),
|
|
488
|
+
}))
|
|
489
|
+
.filter(s => s.total > 0)
|
|
490
|
+
.sort((a, b) => b.total - a.total)
|
|
491
|
+
.slice(0, 10);
|
|
492
|
+
// Densest files
|
|
493
|
+
const densestFiles = Array.from(structuresPerFile.entries())
|
|
494
|
+
.map(([file, count]) => ({
|
|
495
|
+
id: 0,
|
|
496
|
+
name: getFileName(file),
|
|
497
|
+
file: file,
|
|
498
|
+
type: 'file',
|
|
499
|
+
structureCount: count,
|
|
500
|
+
}))
|
|
501
|
+
.sort((a, b) => b.structureCount - a.structureCount)
|
|
502
|
+
.slice(0, 10);
|
|
503
|
+
// Hub functions (called by many)
|
|
504
|
+
const hubFunctions = structures
|
|
505
|
+
.filter(s => s.type === 'function' || s.type === 'method')
|
|
506
|
+
.map(s => ({
|
|
507
|
+
id: s.id,
|
|
508
|
+
name: s.name,
|
|
509
|
+
file: s.file_path,
|
|
510
|
+
type: s.type,
|
|
511
|
+
inbound: inboundCount.get(s.id) || 0,
|
|
512
|
+
}))
|
|
513
|
+
.filter(s => s.inbound > 0)
|
|
514
|
+
.sort((a, b) => b.inbound - a.inbound)
|
|
515
|
+
.slice(0, 10);
|
|
516
|
+
return {
|
|
517
|
+
largestFunctions: functionsWithLines,
|
|
518
|
+
mostConnected: functionsWithConnections,
|
|
519
|
+
densestFiles,
|
|
520
|
+
hubFunctions,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
// ============================================
|
|
524
|
+
// Gallery Builder
|
|
525
|
+
// ============================================
|
|
526
|
+
function buildGalleryData(extractions, links, structures, conversations) {
|
|
527
|
+
const structureMap = new Map();
|
|
528
|
+
for (const s of structures) {
|
|
529
|
+
structureMap.set(s.id, s);
|
|
530
|
+
}
|
|
531
|
+
const conversationMap = new Map();
|
|
532
|
+
for (const c of conversations) {
|
|
533
|
+
conversationMap.set(c.id, c);
|
|
534
|
+
}
|
|
535
|
+
// Build extraction to structure links
|
|
536
|
+
const extractionLinks = new Map();
|
|
537
|
+
for (const link of links) {
|
|
538
|
+
if (link.source_type === 'extraction' && link.target_type === 'structure') {
|
|
539
|
+
const s = structureMap.get(link.target_id);
|
|
540
|
+
if (s) {
|
|
541
|
+
if (!extractionLinks.has(link.source_id)) {
|
|
542
|
+
extractionLinks.set(link.source_id, []);
|
|
543
|
+
}
|
|
544
|
+
extractionLinks.get(link.source_id).push({
|
|
545
|
+
id: s.id,
|
|
546
|
+
name: s.name,
|
|
547
|
+
file: s.file_path,
|
|
548
|
+
type: s.type,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const items = [];
|
|
554
|
+
let decisions = 0, patterns = 0, rejections = 0;
|
|
555
|
+
for (const e of extractions) {
|
|
556
|
+
if (e.type !== 'decision' && e.type !== 'pattern' && e.type !== 'rejection')
|
|
557
|
+
continue;
|
|
558
|
+
const conv = conversationMap.get(e.conversation_id);
|
|
559
|
+
items.push({
|
|
560
|
+
id: e.id,
|
|
561
|
+
type: e.type,
|
|
562
|
+
content: e.content,
|
|
563
|
+
timestamp: conv?.timestamp || '',
|
|
564
|
+
affectedCode: extractionLinks.get(e.id) || [],
|
|
565
|
+
});
|
|
566
|
+
if (e.type === 'decision')
|
|
567
|
+
decisions++;
|
|
568
|
+
else if (e.type === 'pattern')
|
|
569
|
+
patterns++;
|
|
570
|
+
else if (e.type === 'rejection')
|
|
571
|
+
rejections++;
|
|
572
|
+
}
|
|
573
|
+
// Sort by timestamp descending
|
|
574
|
+
items.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
575
|
+
return {
|
|
576
|
+
items,
|
|
577
|
+
byType: { decision: decisions, pattern: patterns, rejection: rejections },
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// ============================================
|
|
581
|
+
// Timeline Builder
|
|
582
|
+
// ============================================
|
|
583
|
+
function buildTimelineData(conversations, links, structures, extractions) {
|
|
584
|
+
if (conversations.length === 0) {
|
|
585
|
+
return { entries: [], dateRange: null };
|
|
586
|
+
}
|
|
587
|
+
const structureMap = new Map();
|
|
588
|
+
for (const s of structures) {
|
|
589
|
+
structureMap.set(s.id, s);
|
|
590
|
+
}
|
|
591
|
+
// Build conversation to structure links
|
|
592
|
+
const conversationStructures = new Map();
|
|
593
|
+
for (const link of links) {
|
|
594
|
+
if (link.source_type === 'conversation' && link.target_type === 'structure') {
|
|
595
|
+
const s = structureMap.get(link.target_id);
|
|
596
|
+
if (s) {
|
|
597
|
+
if (!conversationStructures.has(link.source_id)) {
|
|
598
|
+
conversationStructures.set(link.source_id, []);
|
|
599
|
+
}
|
|
600
|
+
conversationStructures.get(link.source_id).push({
|
|
601
|
+
id: s.id,
|
|
602
|
+
name: s.name,
|
|
603
|
+
file: s.file_path,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Count extractions per conversation
|
|
609
|
+
const extractionCounts = new Map();
|
|
610
|
+
for (const e of extractions) {
|
|
611
|
+
extractionCounts.set(e.conversation_id, (extractionCounts.get(e.conversation_id) || 0) + 1);
|
|
612
|
+
}
|
|
613
|
+
const entries = conversations.map(c => ({
|
|
614
|
+
id: c.id,
|
|
615
|
+
timestamp: c.timestamp,
|
|
616
|
+
summary: c.summary,
|
|
617
|
+
model: c.model,
|
|
618
|
+
tool: c.tool,
|
|
619
|
+
touchedStructures: conversationStructures.get(c.id) || [],
|
|
620
|
+
extractionCount: extractionCounts.get(c.id) || 0,
|
|
621
|
+
}));
|
|
622
|
+
// Sort by timestamp
|
|
623
|
+
entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
624
|
+
const timestamps = entries.map(e => e.timestamp);
|
|
625
|
+
const dateRange = {
|
|
626
|
+
start: timestamps[0],
|
|
627
|
+
end: timestamps[timestamps.length - 1],
|
|
628
|
+
};
|
|
629
|
+
return { entries, dateRange };
|
|
630
|
+
}
|
|
631
|
+
// ============================================
|
|
632
|
+
// Treemap Builder
|
|
633
|
+
// ============================================
|
|
634
|
+
function buildTreemapData(structures) {
|
|
635
|
+
// Build directory tree
|
|
636
|
+
const root = {
|
|
637
|
+
name: 'root',
|
|
638
|
+
path: '',
|
|
639
|
+
type: 'directory',
|
|
640
|
+
value: 0,
|
|
641
|
+
children: [],
|
|
642
|
+
};
|
|
643
|
+
// Group structures by file, then by directory
|
|
644
|
+
const fileStructures = new Map();
|
|
645
|
+
for (const s of structures) {
|
|
646
|
+
if (!fileStructures.has(s.file_path)) {
|
|
647
|
+
fileStructures.set(s.file_path, []);
|
|
648
|
+
}
|
|
649
|
+
fileStructures.get(s.file_path).push(s);
|
|
650
|
+
}
|
|
651
|
+
// Build tree from file paths
|
|
652
|
+
for (const [filePath, fileStructs] of fileStructures) {
|
|
653
|
+
const parts = filePath.split(/[/\\]/).filter(p => p);
|
|
654
|
+
let current = root;
|
|
655
|
+
// Navigate/create directory nodes
|
|
656
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
657
|
+
const dirName = parts[i];
|
|
658
|
+
const dirPath = parts.slice(0, i + 1).join('/');
|
|
659
|
+
let child = current.children?.find(c => c.name === dirName && c.type === 'directory');
|
|
660
|
+
if (!child) {
|
|
661
|
+
child = {
|
|
662
|
+
name: dirName,
|
|
663
|
+
path: dirPath,
|
|
664
|
+
type: 'directory',
|
|
665
|
+
value: 0,
|
|
666
|
+
children: [],
|
|
667
|
+
};
|
|
668
|
+
current.children = current.children || [];
|
|
669
|
+
current.children.push(child);
|
|
670
|
+
}
|
|
671
|
+
current = child;
|
|
672
|
+
}
|
|
673
|
+
// Add file node
|
|
674
|
+
const fileName = parts[parts.length - 1];
|
|
675
|
+
const fileValue = fileStructs.reduce((sum, s) => sum + ((s.line_end || s.line_start) - s.line_start + 1), 0);
|
|
676
|
+
const fileNode = {
|
|
677
|
+
name: fileName,
|
|
678
|
+
path: filePath,
|
|
679
|
+
type: 'file',
|
|
680
|
+
value: fileValue,
|
|
681
|
+
children: fileStructs.map(s => ({
|
|
682
|
+
name: s.name,
|
|
683
|
+
path: `${filePath}:${s.line_start}`,
|
|
684
|
+
type: 'structure',
|
|
685
|
+
structureType: s.type,
|
|
686
|
+
value: (s.line_end || s.line_start) - s.line_start + 1,
|
|
687
|
+
})),
|
|
688
|
+
};
|
|
689
|
+
current.children = current.children || [];
|
|
690
|
+
current.children.push(fileNode);
|
|
691
|
+
}
|
|
692
|
+
// Calculate directory values (sum of children)
|
|
693
|
+
function calculateValue(node) {
|
|
694
|
+
if (!node.children || node.children.length === 0) {
|
|
695
|
+
return node.value;
|
|
696
|
+
}
|
|
697
|
+
node.value = node.children.reduce((sum, child) => sum + calculateValue(child), 0);
|
|
698
|
+
return node.value;
|
|
699
|
+
}
|
|
700
|
+
calculateValue(root);
|
|
701
|
+
return root;
|
|
702
|
+
}
|
|
703
|
+
// Generate HTML dashboard
|
|
704
|
+
export function generateDashboard(data) {
|
|
705
|
+
return generateDashboardHTML(data);
|
|
706
|
+
}
|
|
707
|
+
//# sourceMappingURL=index.js.map
|