thorns 5.1.9
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/.thornsignore +581 -0
- package/README.md +175 -0
- package/advanced-metrics.js +203 -0
- package/analyzer.js +257 -0
- package/compact-formatter.js +960 -0
- package/dependency-analyzer.js +252 -0
- package/ignore-parser.js +150 -0
- package/index.js +6 -0
- package/lib.js +537 -0
- package/one-liner.sh +24 -0
- package/package.json +71 -0
- package/queries.js +102 -0
- package/run.sh +81 -0
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
export function formatUltraCompact(aggregated) {
|
|
2
|
+
const { stats, entities, metrics, depGraph, duplicates, circular, fileSizes, identifiers, funcLengths, funcParams, fileMetrics, projectContext, deadCode } = aggregated;
|
|
3
|
+
|
|
4
|
+
const totalFn = Object.values(stats.byLanguage).reduce((s, l) => s + l.functions, 0);
|
|
5
|
+
const totalCls = Object.values(stats.byLanguage).reduce((s, l) => s + l.classes, 0);
|
|
6
|
+
const avgCx = totalFn > 0 ? (Object.values(stats.byLanguage).reduce((s, l) => s + l.complexity, 0) / totalFn).toFixed(1) : 0;
|
|
7
|
+
|
|
8
|
+
let output = '';
|
|
9
|
+
|
|
10
|
+
const projectInfo = getProjectInfo(projectContext);
|
|
11
|
+
if (projectInfo) {
|
|
12
|
+
output += `## 🎯 ${projectInfo}\n\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const quickStart = getQuickStart(projectContext);
|
|
16
|
+
if (quickStart) {
|
|
17
|
+
output += `## 🚀 Quick Start\n\n${quickStart}\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
output += `# ${stats.files}f ${k(stats.totalLines)}L ${totalFn}fn ${totalCls}cls cx${avgCx}\n`;
|
|
21
|
+
output += `*Legend: f=files L=lines fn=functions cls=classes cx=avg-complexity | file:line:name(NL)=location Np=params | ↑N=imports-from ↓N=imported-by (N)=occurrences (+N)=more | 🔄circular 🏝️isolated 🔥complex 📋duplicated 📁large*\n\n`;
|
|
22
|
+
|
|
23
|
+
const langAbbrev = { JavaScript: 'JS', TypeScript: 'TS', Python: 'Py', Rust: 'Rs', Java: 'Java', 'C++': 'C++', 'C#': 'C#', Ruby: 'Rb', PHP: 'PHP', Go: 'Go', C: 'C', JSON: 'JSON' };
|
|
24
|
+
const langs = Object.entries(stats.byLanguage)
|
|
25
|
+
.sort((a, b) => b[1].lines - a[1].lines)
|
|
26
|
+
.slice(0, 4)
|
|
27
|
+
.map(([lang, data]) => `${langAbbrev[lang] || lang.slice(0,4)}:${(data.lines/stats.totalLines*100).toFixed(0)}%`)
|
|
28
|
+
.join(' ');
|
|
29
|
+
output += `**Langs:** ${langs}\n\n`;
|
|
30
|
+
|
|
31
|
+
const techStack = getTechStack(entities, identifiers);
|
|
32
|
+
if (techStack) {
|
|
33
|
+
output += `## 🛠️ Tech Stack\n\n${techStack}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const codePatterns = getCodePatterns(entities);
|
|
37
|
+
if (codePatterns) {
|
|
38
|
+
output += `## ⚡ Code Patterns\n\n${codePatterns}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ioPatterns = getIOPatterns(entities);
|
|
42
|
+
if (ioPatterns) {
|
|
43
|
+
output += `## 🔗 I/O & Integration\n\n${ioPatterns}\n`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const features = getFeatures(fileMetrics, identifiers);
|
|
47
|
+
if (features) {
|
|
48
|
+
output += `## 🎯 Features\n\n${features}\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const codeOrg = getCodeOrganization(fileSizes, funcLengths, funcParams, totalFn, totalCls, stats.files, fileMetrics);
|
|
52
|
+
if (codeOrg) {
|
|
53
|
+
output += `## 📊 Code Organization\n\n${codeOrg}\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const flow = generateDetailedFlow(depGraph, entities, stats.files, circular);
|
|
57
|
+
if (flow) {
|
|
58
|
+
output += `## 🔄 Architecture\n\n${flow}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const apiSurface = getAPISurface(entities, depGraph, fileMetrics);
|
|
62
|
+
if (apiSurface) {
|
|
63
|
+
output += `## 🔌 API Surface\n\n${apiSurface}\n`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const issues = getCompactIssues(circular, duplicates, metrics, fileSizes, fileMetrics);
|
|
67
|
+
if (issues.length > 0) {
|
|
68
|
+
output += `## 🚨 Issues\n\n`;
|
|
69
|
+
issues.forEach(issue => output += `- ${issue}\n`);
|
|
70
|
+
output += '\n';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const deadCodeSection = getDeadCodeSection(deadCode);
|
|
74
|
+
if (deadCodeSection) {
|
|
75
|
+
output += `## 🧹 Dead Code & Tests\n\n${deadCodeSection}\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const modules = getModuleStructure(depGraph, stats.files, fileSizes);
|
|
79
|
+
if (modules.length > 0) {
|
|
80
|
+
output += `## 📦 Modules\n\n`;
|
|
81
|
+
modules.forEach(mod => output += `- ${mod}\n`);
|
|
82
|
+
output += '\n';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const fileIndex = getFileIndex(fileMetrics, depGraph, entities);
|
|
86
|
+
if (fileIndex) {
|
|
87
|
+
output += `## 📄 File Index\n\n${fileIndex}\n`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return output.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateDetailedFlow(depGraph, entities, totalFiles, circular) {
|
|
94
|
+
if (!depGraph?.coupling || totalFiles < 3) return '';
|
|
95
|
+
|
|
96
|
+
const flow = [];
|
|
97
|
+
const connections = Array.from(depGraph.coupling.entries())
|
|
98
|
+
.map(([file, coupling]) => ({
|
|
99
|
+
file: file.split('/').pop().replace(/\.\w+$/, ''),
|
|
100
|
+
fullPath: file,
|
|
101
|
+
dir: file.split('/')[0] || 'root',
|
|
102
|
+
in: coupling.in,
|
|
103
|
+
out: coupling.out,
|
|
104
|
+
total: coupling.in + coupling.out
|
|
105
|
+
}))
|
|
106
|
+
.sort((a, b) => b.total - a.total);
|
|
107
|
+
|
|
108
|
+
const layeredFlow = buildLayeredFlow(depGraph, connections);
|
|
109
|
+
if (layeredFlow.length > 0) {
|
|
110
|
+
flow.push(...layeredFlow);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const modules = {};
|
|
114
|
+
connections.forEach(c => {
|
|
115
|
+
if (!modules[c.dir]) modules[c.dir] = { count: 0, connections: 0, in: 0, out: 0 };
|
|
116
|
+
modules[c.dir].count++;
|
|
117
|
+
modules[c.dir].connections += c.total;
|
|
118
|
+
modules[c.dir].in += c.in;
|
|
119
|
+
modules[c.dir].out += c.out;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const crossModuleDeps = analyzeCrossModuleDeps(depGraph, modules);
|
|
123
|
+
if (crossModuleDeps.length > 0) {
|
|
124
|
+
flow.push(`**Cross-module:** ${crossModuleDeps.slice(0, 6).join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const moduleFlow = buildModuleFlow(modules);
|
|
128
|
+
if (moduleFlow) {
|
|
129
|
+
flow.push(`**Module flow:** ${moduleFlow}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const externals = getTopExternalDeps(entities);
|
|
133
|
+
if (externals.length > 0) {
|
|
134
|
+
flow.push(`**External:** ${externals.slice(0, 6).join(', ')}${externals.length > 6 ? ` (+${externals.length - 6})` : ''}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const specificDeps = buildSpecificDependencies(depGraph, connections);
|
|
138
|
+
if (specificDeps.length > 0) {
|
|
139
|
+
flow.push(`**Key deps:** ${specificDeps.slice(0, 8).join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const topHubs = connections.filter(c => c.total >= 5).slice(0, 5);
|
|
143
|
+
if (topHubs.length > 0) {
|
|
144
|
+
const hubDesc = topHubs.map(h => `${h.file}(${h.out}↑${h.in}↓)`).join(', ');
|
|
145
|
+
flow.push(`**Hubs:** ${hubDesc}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const leaves = connections.filter(c => c.in === 0 && c.out > 0).sort((a, b) => b.out - a.out).slice(0, 8);
|
|
149
|
+
const totalLeaves = connections.filter(c => c.in === 0 && c.out > 0).length;
|
|
150
|
+
if (leaves.length > 0 && totalLeaves <= 20) {
|
|
151
|
+
const leafDesc = leaves.map(l => `${l.file}(${l.out}↑)`).join(', ');
|
|
152
|
+
flow.push(`**Leaf nodes:** ${leafDesc}${totalLeaves > 8 ? ` (+${totalLeaves - 8})` : ''}`);
|
|
153
|
+
} else if (totalLeaves > 20) {
|
|
154
|
+
const topLeaves = leaves.slice(0, 4).map(l => `${l.file}(${l.out}↑)`).join(', ');
|
|
155
|
+
flow.push(`**Leaf nodes:** ${topLeaves} (+${totalLeaves - 4} entry points)`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const trueOrphans = connections.filter(c => c.total === 0 && !isObviousEntryPoint(c.fullPath));
|
|
159
|
+
if (trueOrphans.length > 0) {
|
|
160
|
+
const orphanNames = trueOrphans.slice(0, 6).map(o => o.file).join(', ');
|
|
161
|
+
flow.push(`**Orphans:** ${orphanNames}${trueOrphans.length > 6 ? ` (+${trueOrphans.length - 6})` : ''}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const concerns = [];
|
|
165
|
+
const cycles = circular?.length || 0;
|
|
166
|
+
const isolated = connections.filter(c => c.total === 0).length;
|
|
167
|
+
const megaHubs = connections.filter(c => c.total >= 15).length;
|
|
168
|
+
|
|
169
|
+
if (cycles > 0) concerns.push(`🔄${cycles} cycles`);
|
|
170
|
+
if (isolated > 10) concerns.push(`🏝️${isolated} isolated`);
|
|
171
|
+
if (megaHubs > 0) concerns.push(`🔥${megaHubs} mega-hubs`);
|
|
172
|
+
|
|
173
|
+
if (concerns.length > 0) {
|
|
174
|
+
flow.push(`**Concerns:** ${concerns.join(' ')}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return flow.join('\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildLayeredFlow(depGraph, connections) {
|
|
181
|
+
const layers = [];
|
|
182
|
+
const nodeMap = new Map();
|
|
183
|
+
|
|
184
|
+
connections.forEach(c => nodeMap.set(c.fullPath, c));
|
|
185
|
+
|
|
186
|
+
const layer0 = connections.filter(c => c.out === 0 && c.in > 0).sort((a, b) => b.in - a.in).slice(0, 8);
|
|
187
|
+
if (layer0.length > 0) {
|
|
188
|
+
const names = layer0.map(n => `${n.file}(${n.in}↓)`).join(', ');
|
|
189
|
+
layers.push(`**L0 [pure exports]:** ${names}${connections.filter(c => c.out === 0 && c.in > 0).length > 8 ? ` (+${connections.filter(c => c.out === 0 && c.in > 0).length - 8})` : ''}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const layer1 = connections.filter(c => c.out >= 1 && c.out <= 3 && c.in >= 3).sort((a, b) => b.in - a.in).slice(0, 8);
|
|
193
|
+
if (layer1.length > 0) {
|
|
194
|
+
const names = layer1.map(n => `${n.file}(${n.out}↑${n.in}↓)`).join(', ');
|
|
195
|
+
layers.push(`**L1 [low imports]:** ${names}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const layer2 = connections.filter(c => c.out >= 4 && c.in >= 2).sort((a, b) => (b.in + b.out) - (a.in + a.out)).slice(0, 8);
|
|
199
|
+
if (layer2.length > 0) {
|
|
200
|
+
const names = layer2.map(n => `${n.file}(${n.out}↑${n.in}↓)`).join(', ');
|
|
201
|
+
layers.push(`**L2 [mid flow]:** ${names}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const layer3 = connections.filter(c => c.in === 0 && c.out > 0).sort((a, b) => b.out - a.out).slice(0, 8);
|
|
205
|
+
if (layer3.length > 0) {
|
|
206
|
+
const names = layer3.map(n => `${n.file}(${n.out}↑)`).join(', ');
|
|
207
|
+
layers.push(`**L3 [pure imports]:** ${names}${connections.filter(c => c.in === 0 && c.out > 0).length > 8 ? ` (+${connections.filter(c => c.in === 0 && c.out > 0).length - 8})` : ''}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return layers;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildModuleFlow(modules) {
|
|
214
|
+
const sorted = Object.entries(modules)
|
|
215
|
+
.filter(([_, data]) => data.in > 0 || data.out > 0)
|
|
216
|
+
.sort((a, b) => {
|
|
217
|
+
const aRatio = a[1].out > 0 ? a[1].in / a[1].out : 999;
|
|
218
|
+
const bRatio = b[1].out > 0 ? b[1].in / b[1].out : 999;
|
|
219
|
+
return bRatio - aRatio;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (sorted.length === 0) return '';
|
|
223
|
+
|
|
224
|
+
const exporters = sorted.filter(([_, d]) => d.in > d.out).slice(0, 3);
|
|
225
|
+
const importers = sorted.filter(([_, d]) => d.out > d.in).slice(0, 3);
|
|
226
|
+
const balanced = sorted.filter(([_, d]) => Math.abs(d.in - d.out) <= 2 && d.in > 0 && d.out > 0).slice(0, 2);
|
|
227
|
+
|
|
228
|
+
const parts = [];
|
|
229
|
+
if (exporters.length > 0) {
|
|
230
|
+
parts.push(exporters.map(([name, d]) => `${name}(${d.out}↑${d.in}↓)`).join('→'));
|
|
231
|
+
}
|
|
232
|
+
if (balanced.length > 0) {
|
|
233
|
+
parts.push(balanced.map(([name, d]) => `${name}(${d.out}↑${d.in}↓)`).join('↔'));
|
|
234
|
+
}
|
|
235
|
+
if (importers.length > 0) {
|
|
236
|
+
parts.push(importers.map(([name, d]) => `${name}(${d.out}↑${d.in}↓)`).join('←'));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return parts.join(' | ');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildSpecificDependencies(depGraph, connections) {
|
|
243
|
+
if (!depGraph?.edges || depGraph.edges.length === 0) return [];
|
|
244
|
+
|
|
245
|
+
const depCounts = new Map();
|
|
246
|
+
|
|
247
|
+
for (const edge of depGraph.edges) {
|
|
248
|
+
const fromFile = edge.from.split('/').pop().replace(/\.\w+$/, '');
|
|
249
|
+
const toFile = edge.to.split('/').pop().replace(/\.\w+$/, '');
|
|
250
|
+
const key = `${fromFile}→${toFile}`;
|
|
251
|
+
depCounts.set(key, (depCounts.get(key) || 0) + 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return Array.from(depCounts.entries())
|
|
255
|
+
.filter(([_, count]) => count >= 1)
|
|
256
|
+
.sort((a, b) => b[1] - a[1])
|
|
257
|
+
.slice(0, 12)
|
|
258
|
+
.map(([dep]) => dep);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function analyzeCrossModuleDeps(depGraph, moduleStats) {
|
|
262
|
+
const crossDeps = new Map();
|
|
263
|
+
|
|
264
|
+
if (!depGraph?.edges) return [];
|
|
265
|
+
|
|
266
|
+
for (const edge of depGraph.edges) {
|
|
267
|
+
const fromModule = edge.from.split('/')[0] || 'root';
|
|
268
|
+
const toModule = edge.to.split('/')[0] || 'root';
|
|
269
|
+
|
|
270
|
+
if (fromModule !== toModule) {
|
|
271
|
+
const key = `${fromModule}→${toModule}`;
|
|
272
|
+
crossDeps.set(key, (crossDeps.get(key) || 0) + 1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return Array.from(crossDeps.entries())
|
|
277
|
+
.sort((a, b) => b[1] - a[1])
|
|
278
|
+
.map(([dep, count]) => `${dep}(${count})`)
|
|
279
|
+
.slice(0, 5);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Get top external dependencies
|
|
283
|
+
function getTopExternalDeps(entities) {
|
|
284
|
+
const externals = new Map();
|
|
285
|
+
const internalPatterns = ['./', '../', '/src/', '/lib/', '/components/', '/utils/', '/services/', '/functions/'];
|
|
286
|
+
|
|
287
|
+
for (const [lang, langEntities] of Object.entries(entities)) {
|
|
288
|
+
for (const importStatement of langEntities.imports) {
|
|
289
|
+
let packageName = null;
|
|
290
|
+
if (importStatement.includes('from ')) {
|
|
291
|
+
const match = importStatement.match(/from ['"]([^'"]+)['"]/);
|
|
292
|
+
if (match) packageName = match[1];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (packageName && !internalPatterns.some(pattern => packageName.includes(pattern))) {
|
|
296
|
+
const isNodeModule = packageName.includes('node_modules') || !packageName.startsWith('.');
|
|
297
|
+
if (isNodeModule && packageName.split('/').length <= 2) {
|
|
298
|
+
externals.set(packageName.split('/').shift(), (externals.get(packageName.split('/').shift()) || 0) + 1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return Array.from(externals)
|
|
305
|
+
.sort((a, b) => b[1] - a[1])
|
|
306
|
+
.slice(0, 6)
|
|
307
|
+
.map(([name]) => name);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getCompactIssues(circular, duplicates, metrics, fileSizes, fileMetrics) {
|
|
311
|
+
const issues = [];
|
|
312
|
+
|
|
313
|
+
if (circular?.length > 0) {
|
|
314
|
+
const cycleDesc = circular.slice(0, 2).map(cycle =>
|
|
315
|
+
cycle.slice(0, 3).map(f => f.split('/').pop().replace(/\.\w+$/, '')).join('→')
|
|
316
|
+
).join(' | ');
|
|
317
|
+
issues.push(`🔄 Circular: ${cycleDesc}${circular.length > 2 ? ` (+${circular.length - 2} cycles)` : ''}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (duplicates?.length > 0) {
|
|
321
|
+
const majorDupes = duplicates.filter(d => d.count >= 3).slice(0, 3);
|
|
322
|
+
if (majorDupes.length > 0) {
|
|
323
|
+
const dupeDesc = majorDupes.map(d => {
|
|
324
|
+
const files = d.instances.slice(0, 2).map(inst => inst.file.split('/').pop()).join(', ');
|
|
325
|
+
return `${files}(${d.count}×)`;
|
|
326
|
+
}).join(' | ');
|
|
327
|
+
issues.push(`📋 Duplication: ${dupeDesc}${duplicates.filter(d => d.count >= 3).length > 3 ? ` (+${duplicates.filter(d => d.count >= 3).length - 3})` : ''}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const complexFuncs = [];
|
|
332
|
+
for (const [file, data] of Object.entries(fileMetrics)) {
|
|
333
|
+
if (data.functions) {
|
|
334
|
+
for (const func of data.functions) {
|
|
335
|
+
if (func.lines > 100) {
|
|
336
|
+
complexFuncs.push({
|
|
337
|
+
file: file.split('/').pop(),
|
|
338
|
+
name: func.name,
|
|
339
|
+
lines: func.lines,
|
|
340
|
+
startLine: func.startLine
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (complexFuncs.length > 0) {
|
|
348
|
+
complexFuncs.sort((a, b) => b.lines - a.lines);
|
|
349
|
+
const funcList = complexFuncs.slice(0, 4).map(f => `${f.file}:${f.startLine}:${f.name}(${f.lines}L)`).join(', ');
|
|
350
|
+
issues.push(`🔥 Complex funcs: ${funcList}${complexFuncs.length > 4 ? ` (+${complexFuncs.length - 4})` : ''}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const largeFiles = fileSizes?.largest?.filter(f => f.lines > 500);
|
|
354
|
+
if (largeFiles?.length > 0) {
|
|
355
|
+
const fileNames = largeFiles.slice(0, 3).map(f => `${f.file.split('/').pop()}:${f.lines}L`).join(', ');
|
|
356
|
+
issues.push(`📁 Large files: ${fileNames}${largeFiles.length > 3 ? ` (+${largeFiles.length - 3})` : ''}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return issues;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getFeatures(fileMetrics, identifiers) {
|
|
363
|
+
const featureAreas = new Map();
|
|
364
|
+
|
|
365
|
+
for (const [filePath, metrics] of Object.entries(fileMetrics)) {
|
|
366
|
+
const fileName = filePath.split('/').pop().replace(/\.\w+$/, '');
|
|
367
|
+
|
|
368
|
+
const keywords = [
|
|
369
|
+
'auth', 'admin', 'marketplace', 'mcp', 'conversation', 'document',
|
|
370
|
+
'streaming', 'server', 'client', 'api', 'test', 'tool', 'handler',
|
|
371
|
+
'manager', 'provider', 'service', 'component', 'hook', 'util', 'config'
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
for (const keyword of keywords) {
|
|
375
|
+
if (fileName.toLowerCase().includes(keyword) || filePath.toLowerCase().includes(keyword)) {
|
|
376
|
+
if (!featureAreas.has(keyword)) {
|
|
377
|
+
featureAreas.set(keyword, { files: [], functions: 0, lines: 0 });
|
|
378
|
+
}
|
|
379
|
+
const area = featureAreas.get(keyword);
|
|
380
|
+
area.files.push(filePath.split('/').pop());
|
|
381
|
+
area.functions += metrics.functions?.length || 0;
|
|
382
|
+
area.lines += metrics.loc || 0;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const sorted = Array.from(featureAreas.entries())
|
|
388
|
+
.filter(([_, data]) => data.files.length >= 2)
|
|
389
|
+
.sort((a, b) => b[1].lines - a[1].lines)
|
|
390
|
+
.slice(0, 8);
|
|
391
|
+
|
|
392
|
+
if (sorted.length === 0) return '';
|
|
393
|
+
|
|
394
|
+
const featureList = sorted.map(([name, data]) => {
|
|
395
|
+
const topFiles = [...new Set(data.files)].slice(0, 3).join(', ');
|
|
396
|
+
return `**${name}:** ${data.files.length}f, ${data.functions}fn (${topFiles})`;
|
|
397
|
+
}).join('\n');
|
|
398
|
+
|
|
399
|
+
return featureList;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getIOPatterns(entities) {
|
|
403
|
+
const patterns = [];
|
|
404
|
+
|
|
405
|
+
const allEnvVars = new Set();
|
|
406
|
+
const allUrls = new Set();
|
|
407
|
+
const allRoutes = [];
|
|
408
|
+
let totalFetches = 0, totalAxios = 0;
|
|
409
|
+
let totalSql = 0, totalFileOps = 0, totalJson = 0;
|
|
410
|
+
let totalEmitters = 0, totalListeners = 0;
|
|
411
|
+
|
|
412
|
+
for (const [lang, langEntities] of Object.entries(entities)) {
|
|
413
|
+
if (langEntities.envVars) {
|
|
414
|
+
for (const v of langEntities.envVars) allEnvVars.add(v);
|
|
415
|
+
}
|
|
416
|
+
if (langEntities.urls) {
|
|
417
|
+
for (const u of langEntities.urls) allUrls.add(u);
|
|
418
|
+
}
|
|
419
|
+
if (langEntities.httpPatterns) {
|
|
420
|
+
totalFetches += langEntities.httpPatterns.fetches;
|
|
421
|
+
totalAxios += langEntities.httpPatterns.axios;
|
|
422
|
+
allRoutes.push(...langEntities.httpPatterns.routes);
|
|
423
|
+
}
|
|
424
|
+
if (langEntities.storagePatterns) {
|
|
425
|
+
totalSql += langEntities.storagePatterns.sql;
|
|
426
|
+
totalFileOps += langEntities.storagePatterns.fileOps;
|
|
427
|
+
totalJson += langEntities.storagePatterns.json;
|
|
428
|
+
}
|
|
429
|
+
if (langEntities.eventPatterns) {
|
|
430
|
+
totalEmitters += langEntities.eventPatterns.emitters;
|
|
431
|
+
totalListeners += langEntities.eventPatterns.listeners;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (allEnvVars.size > 0) {
|
|
436
|
+
const vars = Array.from(allEnvVars).slice(0, 8).join(', ');
|
|
437
|
+
patterns.push(`**Env vars:** ${vars}${allEnvVars.size > 8 ? ` (+${allEnvVars.size - 8})` : ''}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (allUrls.size > 0) {
|
|
441
|
+
const urls = Array.from(allUrls).slice(0, 4).map(u => u.replace(/https?:\/\//, '').slice(0, 30)).join(', ');
|
|
442
|
+
patterns.push(`**URLs:** ${urls}${allUrls.size > 4 ? ` (+${allUrls.size - 4})` : ''}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const httpParts = [];
|
|
446
|
+
if (totalFetches > 0) httpParts.push(`fetch(${totalFetches})`);
|
|
447
|
+
if (totalAxios > 0) httpParts.push(`axios(${totalAxios})`);
|
|
448
|
+
if (allRoutes.length > 0) {
|
|
449
|
+
const routes = [...new Set(allRoutes)].slice(0, 4).join(', ');
|
|
450
|
+
httpParts.push(`routes: ${routes}`);
|
|
451
|
+
}
|
|
452
|
+
if (httpParts.length > 0) {
|
|
453
|
+
patterns.push(`**HTTP:** ${httpParts.join(', ')}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const storageParts = [];
|
|
457
|
+
if (totalSql > 0) storageParts.push(`SQL(${totalSql})`);
|
|
458
|
+
if (totalFileOps > 0) storageParts.push(`files(${totalFileOps})`);
|
|
459
|
+
if (totalJson > 0) storageParts.push(`JSON(${totalJson})`);
|
|
460
|
+
if (storageParts.length > 0) {
|
|
461
|
+
patterns.push(`**Storage:** ${storageParts.join(', ')}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (totalEmitters > 0 || totalListeners > 0) {
|
|
465
|
+
patterns.push(`**Events:** emit(${totalEmitters}), listen(${totalListeners})`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return patterns.length > 0 ? patterns.join('\n') : '';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function getCodePatterns(entities) {
|
|
472
|
+
const patterns = [];
|
|
473
|
+
|
|
474
|
+
let totalAsync = 0, totalAwait = 0, totalPromise = 0, totalCallback = 0, totalThenCatch = 0;
|
|
475
|
+
let totalTryCatch = 0, totalThrow = 0;
|
|
476
|
+
const allErrorTypes = new Set();
|
|
477
|
+
const allInternalCalls = new Map();
|
|
478
|
+
|
|
479
|
+
for (const [lang, langEntities] of Object.entries(entities)) {
|
|
480
|
+
if (langEntities.asyncPatterns) {
|
|
481
|
+
totalAsync += langEntities.asyncPatterns.async;
|
|
482
|
+
totalAwait += langEntities.asyncPatterns.await;
|
|
483
|
+
totalPromise += langEntities.asyncPatterns.promise;
|
|
484
|
+
totalCallback += langEntities.asyncPatterns.callback;
|
|
485
|
+
totalThenCatch += langEntities.asyncPatterns.thenCatch;
|
|
486
|
+
}
|
|
487
|
+
if (langEntities.errorPatterns) {
|
|
488
|
+
totalTryCatch += langEntities.errorPatterns.tryCatch;
|
|
489
|
+
totalThrow += langEntities.errorPatterns.throw;
|
|
490
|
+
for (const t of langEntities.errorPatterns.errorTypes) allErrorTypes.add(t);
|
|
491
|
+
}
|
|
492
|
+
if (langEntities.internalCalls) {
|
|
493
|
+
for (const [name, count] of langEntities.internalCalls) {
|
|
494
|
+
allInternalCalls.set(name, (allInternalCalls.get(name) || 0) + count);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const asyncParts = [];
|
|
500
|
+
if (totalAsync > 0) asyncParts.push(`async(${totalAsync})`);
|
|
501
|
+
if (totalAwait > 0) asyncParts.push(`await(${totalAwait})`);
|
|
502
|
+
if (totalPromise > 0) asyncParts.push(`Promise(${totalPromise})`);
|
|
503
|
+
if (totalCallback > 0) asyncParts.push(`callbacks(${totalCallback})`);
|
|
504
|
+
if (totalThenCatch > 0) asyncParts.push(`.then/.catch(${totalThenCatch})`);
|
|
505
|
+
|
|
506
|
+
if (asyncParts.length > 0) {
|
|
507
|
+
patterns.push(`**Async:** ${asyncParts.join(', ')}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const errorParts = [];
|
|
511
|
+
if (totalTryCatch > 0) errorParts.push(`try/catch(${totalTryCatch})`);
|
|
512
|
+
if (totalThrow > 0) errorParts.push(`throw(${totalThrow})`);
|
|
513
|
+
if (allErrorTypes.size > 0) {
|
|
514
|
+
const types = Array.from(allErrorTypes).slice(0, 4).join(', ');
|
|
515
|
+
errorParts.push(`types: ${types}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (errorParts.length > 0) {
|
|
519
|
+
patterns.push(`**Errors:** ${errorParts.join(', ')}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const topCalls = Array.from(allInternalCalls.entries())
|
|
523
|
+
.sort((a, b) => b[1] - a[1])
|
|
524
|
+
.slice(0, 8);
|
|
525
|
+
|
|
526
|
+
if (topCalls.length > 0) {
|
|
527
|
+
const callList = topCalls.map(([name, count]) => `${name}(${count})`).join(', ');
|
|
528
|
+
patterns.push(`**Internal calls:** ${callList}`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const allConstants = [];
|
|
532
|
+
const allGlobalState = [];
|
|
533
|
+
for (const [lang, langEntities] of Object.entries(entities)) {
|
|
534
|
+
if (langEntities.constants) allConstants.push(...langEntities.constants);
|
|
535
|
+
if (langEntities.globalState) allGlobalState.push(...langEntities.globalState);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (allConstants.length > 0) {
|
|
539
|
+
const constList = allConstants.slice(0, 6).map(c => {
|
|
540
|
+
const val = c.value.replace(/\s+/g, ' ').replace(/[{}\[\]]/g, '').slice(0, 12);
|
|
541
|
+
return `${c.name}`;
|
|
542
|
+
}).join(', ');
|
|
543
|
+
patterns.push(`**Constants:** ${constList}${allConstants.length > 6 ? ` (+${allConstants.length - 6})` : ''}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (allGlobalState.length > 0) {
|
|
547
|
+
const stateList = allGlobalState.slice(0, 6).join(', ');
|
|
548
|
+
patterns.push(`**Global state:** ${stateList}${allGlobalState.length > 6 ? ` (+${allGlobalState.length - 6})` : ''}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return patterns.length > 0 ? patterns.join('\n') : '';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getTechStack(entities, identifiers) {
|
|
555
|
+
const stack = [];
|
|
556
|
+
|
|
557
|
+
const topPatterns = [];
|
|
558
|
+
for (const [lang, langEntities] of Object.entries(entities)) {
|
|
559
|
+
if (langEntities.patterns && langEntities.patterns.size > 0) {
|
|
560
|
+
const patterns = Array.from(langEntities.patterns.entries())
|
|
561
|
+
.sort((a, b) => b[1] - a[1])
|
|
562
|
+
.slice(0, 8);
|
|
563
|
+
topPatterns.push(...patterns);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const dedupedPatterns = new Map();
|
|
568
|
+
topPatterns.forEach(([name, count]) => {
|
|
569
|
+
dedupedPatterns.set(name, (dedupedPatterns.get(name) || 0) + count);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const sortedPatterns = Array.from(dedupedPatterns.entries())
|
|
573
|
+
.sort((a, b) => b[1] - a[1])
|
|
574
|
+
.slice(0, 6);
|
|
575
|
+
|
|
576
|
+
if (sortedPatterns.length > 0) {
|
|
577
|
+
const patternNames = sortedPatterns.map(([name, count]) => {
|
|
578
|
+
const shortName = name.length > 20 ? name.slice(0, 17) + '...' : name;
|
|
579
|
+
return `${shortName}(${count})`;
|
|
580
|
+
}).join(', ');
|
|
581
|
+
stack.push(`**Patterns:** ${patternNames}`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const topIdentifiers = Array.from(identifiers.entries())
|
|
585
|
+
.filter(([name]) => name.length >= 3 && name.length <= 25)
|
|
586
|
+
.sort((a, b) => b[1] - a[1])
|
|
587
|
+
.slice(0, 6);
|
|
588
|
+
|
|
589
|
+
if (topIdentifiers.length > 0) {
|
|
590
|
+
const idNames = topIdentifiers.map(([name, count]) => `${name}(${count})`).join(', ');
|
|
591
|
+
stack.push(`**Top IDs:** ${idNames}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return stack.join('\n');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getCodeOrganization(fileSizes, funcLengths, funcParams, totalFn, totalCls, totalFiles, fileMetrics) {
|
|
598
|
+
const org = [];
|
|
599
|
+
|
|
600
|
+
if (fileSizes?.largest && fileSizes.largest.length > 0) {
|
|
601
|
+
const largeFiles = fileSizes.largest.filter(f => f.lines >= 200).slice(0, 6);
|
|
602
|
+
if (largeFiles.length > 0) {
|
|
603
|
+
const fileList = largeFiles.map(f => `${f.file.split('/').pop()}:${f.lines}L`).join(', ');
|
|
604
|
+
org.push(`**Large files:** ${fileList}${fileSizes.largest.filter(f => f.lines >= 200).length > 6 ? ` (+${fileSizes.largest.filter(f => f.lines >= 200).length - 6})` : ''}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const longFunctions = [];
|
|
609
|
+
for (const [file, metrics] of Object.entries(fileMetrics)) {
|
|
610
|
+
if (metrics.functions) {
|
|
611
|
+
for (const func of metrics.functions) {
|
|
612
|
+
if (func.lines > 50) {
|
|
613
|
+
longFunctions.push({
|
|
614
|
+
file: file.split('/').pop(),
|
|
615
|
+
name: func.name,
|
|
616
|
+
lines: func.lines,
|
|
617
|
+
startLine: func.startLine
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (longFunctions.length > 0) {
|
|
625
|
+
longFunctions.sort((a, b) => b.lines - a.lines);
|
|
626
|
+
const funcList = longFunctions.slice(0, 6).map(f => `${f.file}:${f.startLine}:${f.name}(${f.lines}L)`).join(', ');
|
|
627
|
+
org.push(`**Long funcs:** ${funcList}${longFunctions.length > 6 ? ` (+${longFunctions.length - 6})` : ''}`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const manyParamFuncs = [];
|
|
631
|
+
for (const [file, metrics] of Object.entries(fileMetrics)) {
|
|
632
|
+
if (metrics.functions) {
|
|
633
|
+
for (const func of metrics.functions) {
|
|
634
|
+
if (func.params > 5) {
|
|
635
|
+
manyParamFuncs.push({
|
|
636
|
+
file: file.split('/').pop(),
|
|
637
|
+
name: func.name,
|
|
638
|
+
params: func.params,
|
|
639
|
+
startLine: func.startLine
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (manyParamFuncs.length > 0) {
|
|
647
|
+
manyParamFuncs.sort((a, b) => b.params - a.params);
|
|
648
|
+
const paramList = manyParamFuncs.slice(0, 6).map(f => `${f.file}:${f.startLine}:${f.name}(${f.params}p)`).join(', ');
|
|
649
|
+
org.push(`**Many params:** ${paramList}${manyParamFuncs.length > 6 ? ` (+${manyParamFuncs.length - 6})` : ''}`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const allClasses = [];
|
|
653
|
+
for (const [file, metrics] of Object.entries(fileMetrics)) {
|
|
654
|
+
if (metrics.classes && metrics.classes.length > 0) {
|
|
655
|
+
for (const cls of metrics.classes) {
|
|
656
|
+
allClasses.push({
|
|
657
|
+
file: file.split('/').pop(),
|
|
658
|
+
name: cls.name,
|
|
659
|
+
startLine: cls.startLine
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (allClasses.length > 0) {
|
|
666
|
+
const classList = allClasses.slice(0, 8).map(c => `${c.file}:${c.startLine}:${c.name}`).join(', ');
|
|
667
|
+
org.push(`**Classes:** ${classList}${allClasses.length > 8 ? ` (+${allClasses.length - 8})` : ''}`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return org.join('\n');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function getAPISurface(entities, depGraph, fileMetrics) {
|
|
674
|
+
const surface = [];
|
|
675
|
+
|
|
676
|
+
const allFunctions = [];
|
|
677
|
+
const allClasses = [];
|
|
678
|
+
|
|
679
|
+
for (const [lang, langEntities] of Object.entries(entities)) {
|
|
680
|
+
if (langEntities.functions && langEntities.functions.size > 0) {
|
|
681
|
+
const fns = Array.from(langEntities.functions.entries())
|
|
682
|
+
.filter(([sig]) => !sig.includes('anonymous') && !sig.includes('anon'))
|
|
683
|
+
.sort((a, b) => b[1].count - a[1].count);
|
|
684
|
+
allFunctions.push(...fns);
|
|
685
|
+
}
|
|
686
|
+
if (langEntities.classes && langEntities.classes.size > 0) {
|
|
687
|
+
const classes = Array.from(langEntities.classes.entries())
|
|
688
|
+
.sort((a, b) => b[1].count - a[1].count);
|
|
689
|
+
allClasses.push(...classes);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (fileMetrics && depGraph?.nodes) {
|
|
694
|
+
const exportedFuncs = [];
|
|
695
|
+
for (const [file, metrics] of Object.entries(fileMetrics)) {
|
|
696
|
+
const node = depGraph.nodes.get(file);
|
|
697
|
+
const exportedNames = node ? new Set(node.exportedNames || []) : new Set();
|
|
698
|
+
if (metrics.functions) {
|
|
699
|
+
for (const func of metrics.functions) {
|
|
700
|
+
if (func.name && !func.name.includes('anonymous')) {
|
|
701
|
+
const isExported = exportedNames.has(func.name);
|
|
702
|
+
exportedFuncs.push({ file, ...func, isExported });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const exported = exportedFuncs.filter(f => f.isExported);
|
|
708
|
+
const topFuncs = (exported.length > 0 ? exported : exportedFuncs).slice(0, 12);
|
|
709
|
+
if (topFuncs.length > 0) {
|
|
710
|
+
const funcList = topFuncs.map(f => `${f.file.split('/').pop()}:${f.startLine}:${f.name}(${f.params}p)`).join(', ');
|
|
711
|
+
surface.push(`**Exported fns:** ${funcList}${exportedFuncs.length > 12 ? ` (+${exportedFuncs.length - 12})` : ''}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (allClasses.length > 0) {
|
|
716
|
+
const topClasses = allClasses.slice(0, 6).map(([name, data]) => `${name}(${data.count})`).join(', ');
|
|
717
|
+
surface.push(`**Classes:** ${topClasses}${allClasses.length > 6 ? ` (+${allClasses.length - 6})` : ''}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (depGraph?.entryPoints && depGraph.entryPoints.size > 0) {
|
|
721
|
+
const entries = Array.from(depGraph.entryPoints)
|
|
722
|
+
.slice(0, 5)
|
|
723
|
+
.map(e => e.split('/').pop().replace(/\.\w+$/, ''))
|
|
724
|
+
.join(', ');
|
|
725
|
+
surface.push(`**Entry files:** ${entries}${depGraph.entryPoints.size > 5 ? ` (+${depGraph.entryPoints.size - 5})` : ''}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return surface.length > 0 ? surface.join('\n') : '';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function getModuleStructure(depGraph, totalFiles, fileSizes) {
|
|
732
|
+
if (!depGraph?.coupling || totalFiles < 5) return [];
|
|
733
|
+
|
|
734
|
+
const modules = new Map();
|
|
735
|
+
let totalConnections = 0;
|
|
736
|
+
|
|
737
|
+
for (const [file, coupling] of depGraph.coupling) {
|
|
738
|
+
const module = file.split('/')[0] || 'root';
|
|
739
|
+
if (!modules.has(module)) {
|
|
740
|
+
modules.set(module, { files: 0, connections: 0, imports: 0, exports: 0, lines: 0 });
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const mod = modules.get(module);
|
|
744
|
+
mod.files++;
|
|
745
|
+
mod.connections += coupling.in + coupling.out;
|
|
746
|
+
mod.imports += coupling.out;
|
|
747
|
+
mod.exports += coupling.in;
|
|
748
|
+
totalConnections += coupling.in + coupling.out;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (fileSizes?.largest) {
|
|
752
|
+
for (const { file, lines } of fileSizes.largest) {
|
|
753
|
+
const module = file.split('/')[0] || 'root';
|
|
754
|
+
if (modules.has(module)) {
|
|
755
|
+
modules.get(module).lines += lines;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return Array.from(modules.entries())
|
|
761
|
+
.map(([name, data]) => ({
|
|
762
|
+
name,
|
|
763
|
+
...data,
|
|
764
|
+
pct: totalConnections > 0 ? (data.connections / totalConnections * 100).toFixed(0) : 0,
|
|
765
|
+
avgLines: data.files > 0 ? Math.round(data.lines / data.files) : 0
|
|
766
|
+
}))
|
|
767
|
+
.sort((a, b) => b.connections - a.connections)
|
|
768
|
+
.slice(0, 6)
|
|
769
|
+
.map(m => {
|
|
770
|
+
const parts = [`${m.name}: ${m.files}f, ${m.connections}cx`];
|
|
771
|
+
if (m.avgLines > 0) parts.push(`~${m.avgLines}L/f`);
|
|
772
|
+
if (m.imports > 0) parts.push(`${m.imports}↑`);
|
|
773
|
+
if (m.exports > 0) parts.push(`${m.exports}↓`);
|
|
774
|
+
return parts.join(', ');
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Check if file is obviously an entry point
|
|
779
|
+
function isObviousEntryPoint(filename) {
|
|
780
|
+
const entryPatterns = [
|
|
781
|
+
'index.', 'main.', 'app.', 'server.', 'client.', 'start.',
|
|
782
|
+
'cli.', 'bin.', 'boot.', 'init.', 'entry.'
|
|
783
|
+
];
|
|
784
|
+
return entryPatterns.some(pattern => filename.includes(pattern));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Analyze the import/export flow pattern
|
|
788
|
+
function analyzeFlowPattern(connections, entries) {
|
|
789
|
+
if (connections.length < 3) return '';
|
|
790
|
+
|
|
791
|
+
const totalImports = connections.reduce((sum, c) => sum + c.in, 0);
|
|
792
|
+
const totalExports = connections.reduce((sum, c) => sum + c.out, 0);
|
|
793
|
+
|
|
794
|
+
// Determine the dominant flow pattern
|
|
795
|
+
if (entries.length >= 2) {
|
|
796
|
+
return 'Multi-entry system';
|
|
797
|
+
} else if (entries.length === 1) {
|
|
798
|
+
const hubCount = connections.filter(c => c.total >= 5).length;
|
|
799
|
+
if (hubCount >= 2) {
|
|
800
|
+
return 'Hub-and-spoke architecture';
|
|
801
|
+
} else {
|
|
802
|
+
return 'Linear flow system';
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
// No clear entry points
|
|
806
|
+
const highlyConnected = connections.filter(c => c.total >= 5).length;
|
|
807
|
+
if (highlyConnected > 0) {
|
|
808
|
+
return 'Decentralized modules';
|
|
809
|
+
} else {
|
|
810
|
+
return 'Isolated components';
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function getQuickStart(projectContext) {
|
|
816
|
+
if (!projectContext) return '';
|
|
817
|
+
|
|
818
|
+
const cmds = [];
|
|
819
|
+
|
|
820
|
+
if (projectContext.type === 'cli' && projectContext.name) {
|
|
821
|
+
cmds.push(`\`npx ${projectContext.name}\``);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const scripts = projectContext.scripts || {};
|
|
825
|
+
|
|
826
|
+
if (scripts.dev) cmds.push(`\`npm run dev\``);
|
|
827
|
+
else if (scripts.start) cmds.push(`\`npm start\``);
|
|
828
|
+
|
|
829
|
+
if (scripts.build) cmds.push(`\`npm run build\``);
|
|
830
|
+
if (scripts.test) cmds.push(`\`npm test\``);
|
|
831
|
+
|
|
832
|
+
if (cmds.length === 0) return '';
|
|
833
|
+
|
|
834
|
+
return cmds.join(' | ');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function getProjectInfo(projectContext) {
|
|
838
|
+
if (!projectContext) return '';
|
|
839
|
+
|
|
840
|
+
const lines = [];
|
|
841
|
+
|
|
842
|
+
if (projectContext.name || projectContext.description) {
|
|
843
|
+
const nameVer = projectContext.name ? `**${projectContext.name}**` + (projectContext.version ? ` v${projectContext.version}` : '') : '';
|
|
844
|
+
const typeStr = projectContext.type && projectContext.type !== 'unknown' ? ` (${projectContext.type})` : '';
|
|
845
|
+
if (nameVer) lines.push(nameVer + typeStr);
|
|
846
|
+
if (projectContext.description) lines.push(projectContext.description);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (projectContext.readme && !projectContext.description) {
|
|
850
|
+
lines.push(projectContext.readme.slice(0, 200));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const meta = [];
|
|
854
|
+
if (projectContext.framework) meta.push(projectContext.framework);
|
|
855
|
+
if (projectContext.runtime) meta.push(projectContext.runtime);
|
|
856
|
+
|
|
857
|
+
const deps = Object.keys(projectContext.dependencies || {});
|
|
858
|
+
const keyDeps = [];
|
|
859
|
+
if (deps.includes('express')) keyDeps.push('Express');
|
|
860
|
+
if (deps.includes('fastify')) keyDeps.push('Fastify');
|
|
861
|
+
if (deps.includes('@supabase/supabase-js')) keyDeps.push('Supabase');
|
|
862
|
+
if (deps.includes('firebase')) keyDeps.push('Firebase');
|
|
863
|
+
if (deps.includes('prisma')) keyDeps.push('Prisma');
|
|
864
|
+
if (deps.includes('mongoose')) keyDeps.push('MongoDB');
|
|
865
|
+
if (deps.includes('tree-sitter')) keyDeps.push('tree-sitter');
|
|
866
|
+
if (deps.includes('playwright')) keyDeps.push('Playwright');
|
|
867
|
+
if (deps.includes('puppeteer')) keyDeps.push('Puppeteer');
|
|
868
|
+
|
|
869
|
+
if (keyDeps.length > 0) meta.push(...keyDeps);
|
|
870
|
+
|
|
871
|
+
if (meta.length > 0) lines.push(`Tech: ${meta.join(', ')}`);
|
|
872
|
+
|
|
873
|
+
return lines.join('\n');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function getDeadCodeSection(deadCode) {
|
|
877
|
+
if (!deadCode) return '';
|
|
878
|
+
|
|
879
|
+
const sections = [];
|
|
880
|
+
|
|
881
|
+
if (deadCode.testFiles?.length > 0) {
|
|
882
|
+
const testList = deadCode.testFiles.slice(0, 6).map(f => f.split('/').pop()).join(', ');
|
|
883
|
+
sections.push(`**Test files:** ${testList}${deadCode.testFiles.length > 6 ? ` (+${deadCode.testFiles.length - 6})` : ''}`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (deadCode.unusedExports?.length > 0) {
|
|
887
|
+
const unusedList = deadCode.unusedExports.slice(0, 4).map(item => {
|
|
888
|
+
const fileName = item.file.split('/').pop();
|
|
889
|
+
const exports = item.exports.join(', ');
|
|
890
|
+
return `${fileName}(${exports})`;
|
|
891
|
+
}).join(', ');
|
|
892
|
+
sections.push(`**Unused exports:** ${unusedList}${deadCode.unusedExports.length > 4 ? ` (+${deadCode.unusedExports.length - 4})` : ''}`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (deadCode.orphanedFiles?.length > 0) {
|
|
896
|
+
const orphanList = deadCode.orphanedFiles.slice(0, 6).map(f => f.split('/').pop()).join(', ');
|
|
897
|
+
sections.push(`**Orphaned:** ${orphanList}${deadCode.orphanedFiles.length > 6 ? ` (+${deadCode.orphanedFiles.length - 6})` : ''}`);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (deadCode.possiblyDead?.length > 0) {
|
|
901
|
+
const possiblyList = deadCode.possiblyDead.slice(0, 3).map(item => {
|
|
902
|
+
const fileName = item.file.split('/').pop();
|
|
903
|
+
const usedBy = item.usedBy.split('/').pop();
|
|
904
|
+
return `${fileName}←${usedBy}`;
|
|
905
|
+
}).join(', ');
|
|
906
|
+
sections.push(`**Single use:** ${possiblyList}${deadCode.possiblyDead.length > 3 ? ` (+${deadCode.possiblyDead.length - 3})` : ''}`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return sections.join('\n');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function getFileIndex(fileMetrics, depGraph, entities) {
|
|
913
|
+
if (!fileMetrics || Object.keys(fileMetrics).length < 2) return '';
|
|
914
|
+
|
|
915
|
+
const totalFiles = Object.keys(fileMetrics).length;
|
|
916
|
+
const limit = totalFiles <= 30 ? totalFiles : 30;
|
|
917
|
+
|
|
918
|
+
const files = Object.entries(fileMetrics)
|
|
919
|
+
.filter(([path]) => !path.includes('.json') && !path.includes('.test.') && !path.includes('.spec.'))
|
|
920
|
+
.map(([path, metrics]) => {
|
|
921
|
+
const coupling = depGraph?.coupling?.get(path);
|
|
922
|
+
const node = depGraph?.nodes?.get(path);
|
|
923
|
+
const exportedNames = node ? Array.from(node.exportedNames || []) : [];
|
|
924
|
+
const importsFrom = node ? Array.from(node.importsFrom || []) : [];
|
|
925
|
+
const allFuncs = metrics.functions?.map(f => f.name).filter(n => n && !n.includes('anonymous')) || [];
|
|
926
|
+
const classes = metrics.classes?.map(c => c.name) || [];
|
|
927
|
+
const inCount = coupling?.in || 0;
|
|
928
|
+
const outCount = coupling?.out || 0;
|
|
929
|
+
|
|
930
|
+
const parts = [`**${path}**`];
|
|
931
|
+
if (metrics.loc) parts.push(`${metrics.loc}L`);
|
|
932
|
+
if (outCount > 0) parts.push(`${outCount}↑`);
|
|
933
|
+
if (inCount > 0) parts.push(`${inCount}↓`);
|
|
934
|
+
|
|
935
|
+
const syms = [...classes.map(c => `[${c}]`), ...exportedNames.filter(e => !classes.includes(e))].slice(0, 6);
|
|
936
|
+
if (syms.length > 0) {
|
|
937
|
+
parts.push(`exports: ${syms.join(', ')}`);
|
|
938
|
+
} else if (allFuncs.length > 0) {
|
|
939
|
+
parts.push(`fn: ${allFuncs.slice(0, 5).join(', ')}`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (inCount >= 3 && importsFrom.length > 0) {
|
|
943
|
+
const fromNames = importsFrom.slice(0, 3).map(f => f.split('/').pop().replace(/\.\w+$/, ''));
|
|
944
|
+
parts.push(`uses: ${fromNames.join(', ')}`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return { line: parts.join(' '), total: inCount + outCount };
|
|
948
|
+
})
|
|
949
|
+
.sort((a, b) => b.total - a.total)
|
|
950
|
+
.slice(0, limit)
|
|
951
|
+
.map(f => f.line);
|
|
952
|
+
|
|
953
|
+
if (files.length === 0) return '';
|
|
954
|
+
|
|
955
|
+
return files.join('\n');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function k(num) {
|
|
959
|
+
return num >= 1000 ? `${(num / 1000).toFixed(1)}k` : num;
|
|
960
|
+
}
|